Version Control
About 5 min
Version Control
WebCAD provides a complete version control system with branch management, version tracking, collaborative editing, and conflict resolution.
Core Concepts
Version Structure
Drawing (mapid/version)
└── main branch
├── base (initial version)
├── patch-001
├── patch-002
└── patch-003 (current)
└── feature-xxx branch
├── base (forked from main)
└── patch-001Patch Version Chain
Each save creates a new Patch version:
base → patch-001 → patch-002 → ...Each patch records incremental changes relative to its parent version.
Patch Contents
- Added entities
- Modified entities
- Deleted entity IDs
- Layer changes
- Edit area info (tile mode)
- Metadata (author, time, remark, etc.)
Create Branch
Branches allow creating independent edit lines from a specific version.
Branch Concepts
main: Main branch, default branchfeature-xxx: Feature branchfix-xxx: Fix branch- Branches evolve independently without affecting each other
Branch Creation Scenarios
- Develop new features without affecting the main line
- Multi-person collaboration with independent editing
- Experimental changes that can be discarded anytime
API Method
import { DrawingManagerService, BranchCreateDialog } from 'vjcad';
const drawingManager = new DrawingManagerService();
// Method 1: Create via dialog
const dialog = new BranchCreateDialog();
const result = await dialog.showDialog({
type: 'imports',
mapid: 'your-map-id',
version: 'v1',
fromBranchName: 'main',
fromPatchId: 'base'
});
if (result && result.action === 'create') {
const createResult = await drawingManager.createBranch({
type: result.type,
mapid: result.mapid,
version: result.version,
sourceBranch: result.fromBranchName, // Source branch
sourcePatchId: result.fromPatchId, // Source version
branchName: result.newBranchName // New branch name
});
if (createResult.status) {
console.log(`Branch "${result.newBranchName}" created successfully!`);
} else {
console.error(`Creation failed: ${createResult.error}`);
}
}
// Method 2: Create directly via API
const result = await drawingManager.createBranch({
type: 'imports',
mapid: 'your-map-id',
version: 'v1',
sourceBranch: 'main',
sourcePatchId: 'base',
branchName: 'feature-new-layer'
});Command Method
// Execute OPENFROMSERVER to open drawing browser
// Select the drawing and version to create branch from
// Right-click the version, select "Create Branch"
// Enter new branch name
await Engine.editor.executerWithOp('OPENFROMSERVER');Save Version
Each save creates a new Patch version.
Save Workflow
- Open drawing from server (get original data)
- Edit drawing (add/delete/modify entities)
- Execute save
- System calculates diff, generates patch
- Upload patch to server
API Method
import { Engine, DrawingManagerService, LineEnt, CircleEnt } from 'vjcad';
const drawingManager = new DrawingManagerService();
// Step 1: Open drawing from server (get original data for diff calculation)
const openResult = await drawingManager.openDrawing({
type: 'imports',
mapid: 'your-map-id',
version: 'v1',
branch: 'main',
patchId: 'base'
});
if (!openResult.success) {
throw new Error(`Failed to open drawing: ${openResult.error}`);
}
// Load drawing data into editor
const webcadData = openResult.webcadData;
const jsonString = openResult.webcadJson;
const docName = 'your-map-id_v1_main';
const virtualFile = new File([jsonString], docName, { type: 'application/json' });
await Engine.view.openDbDoc(virtualFile, webcadData);
// Save original data (for subsequent diff calculation)
const originalJson = openResult.webcadJson;
await Engine.currentDoc.setOriginalJson(originalJson);
// Step 2: Edit drawing (add new graphics)
const initBounds = Engine.currentDoc.currentSpace.initBounds;
const centerX = (initBounds[0] + initBounds[2]) / 2;
const centerY = (initBounds[1] + initBounds[3]) / 2;
const size = Math.min(initBounds[2] - initBounds[0], initBounds[3] - initBounds[1]) * 0.1;
const circle = new CircleEnt([centerX, centerY], size);
circle.setDefaults();
circle.color = 1;
Engine.addEntities(circle);
const line = new LineEnt([centerX - size, centerY - size], [centerX + size, centerY + size]);
line.setDefaults();
line.color = 3;
Engine.addEntities(line);
// Step 3: Save (calculate diff and generate patch)
const currentJson = JSON.stringify(Engine.currentDoc.toDb());
const saveResult = await drawingManager.saveDrawing({
type: 'imports',
mapid: 'your-map-id',
version: 'v1',
branchName: 'main',
originalJson: originalJson,
currentJson: currentJson,
parentId: openResult.latestPatchId || 'base', // Use opened version as parent
drawingName: 'Save Version Example',
author: 'Example User',
remark: 'Added circle and line'
});
if (saveResult.status) {
if (saveResult.patchId === 'no_change') {
console.log('No changes to save');
} else {
console.log(`Save successful! Patch ID: ${saveResult.patchId}`);
// Update local original data
await Engine.currentDoc.setOriginalJson(currentJson);
}
} else if (saveResult.conflict && saveResult.conflict.hasConflict) {
console.warn('Conflict detected with other users\' changes');
} else {
console.error(`Save failed: ${saveResult.error}`);
}Command Method
// Execute SAVESERVER to save to server
await Engine.editor.executerWithOp('SAVESERVER');Delete Patch
const deleteResult = await drawingManager.deletePatch({
type: 'imports',
mapid: 'your-map-id',
version: 'v1',
branch: 'main',
patchId: 'patch-001'
});
if (deleteResult.status) {
console.log('Patch deleted');
}Merge Branch
Complete Branch Workflow
import { Engine, DrawingManagerService, LineEnt, CircleEnt } from 'vjcad';
const drawingManager = new DrawingManagerService();
const timestamp = Date.now();
const branchA = `test-branch-A-${timestamp}`;
const branchB = `test-branch-B-${timestamp}`;
// Step 1: Create branch A
const createResultA = await drawingManager.createBranch({
type: 'imports',
mapid: 'your-map-id',
version: 'v1',
sourceBranch: 'main',
sourcePatchId: 'base',
branchName: branchA
});
if (!createResultA.status) {
throw new Error(`Failed to create branch A: ${createResultA.error}`);
}
// Step 2: Create branch B
const createResultB = await drawingManager.createBranch({
type: 'imports',
mapid: 'your-map-id',
version: 'v1',
sourceBranch: 'main',
sourcePatchId: 'base',
branchName: branchB
});
// Step 3: Modify on branch A
const openResultA = await drawingManager.openDrawing({
type: 'imports',
mapid: 'your-map-id',
version: 'v1',
branch: branchA,
patchId: 'base'
});
// Load drawing and add graphics...
const circleA = new CircleEnt([centerX - size, centerY], size * 0.5);
circleA.setDefaults();
circleA.color = 1; // Red
Engine.addEntities(circleA);
// Save branch A
const saveResultA = await drawingManager.saveDrawing({
type: 'imports',
mapid: 'your-map-id',
version: 'v1',
branchName: branchA,
originalJson: jsonStringA,
currentJson: currentJsonA,
parentId: openResultA.latestPatchId || 'base',
remark: 'Added red circle on branch A'
});
// Step 4: Modify on branch B (similar operations)
// ...
// Step 5: Merge branch A to main
const mergeResultA = await drawingManager.mergeBranch({
type: 'imports',
mapid: 'your-map-id',
version: 'v1',
sourceBranch: branchA,
targetBranch: 'main',
remark: 'Merge red circle from branch A'
});
if (mergeResultA.status) {
console.log(`Branch A merged to main successfully! Patch ID: ${mergeResultA.patchId}`);
} else if (mergeResultA.conflict && mergeResultA.conflict.hasConflict) {
console.warn("Conflict detected");
} else {
console.error(`Merge failed: ${mergeResultA.error}`);
}
// Step 6: Merge branch B to main
const mergeResultB = await drawingManager.mergeBranch({
type: 'imports',
mapid: 'your-map-id',
version: 'v1',
sourceBranch: branchB,
targetBranch: 'main',
remark: 'Merge green line from branch B'
});Clean Up Branches
// Delete branch
const deleteResult = await drawingManager.deleteBranch({
type: 'imports',
mapid: 'your-map-id',
version: 'v1',
branchName: 'feature-xxx'
});
if (deleteResult.status) {
console.log('Branch deleted');
}Version History
Version Info Contains
- Patch ID: Unique identifier
- Parent Patch ID: Version chain relationship
- Author: Submitter
- Time: Submit time
- Remark: Change description
- Change statistics: Add/delete/modify counts
Get Branch and Version List
import { DrawingManagerService } from 'vjcad';
const drawingManager = new DrawingManagerService();
// Get branch list (includes patch info for each branch)
const branches = await drawingManager.listBranches({
type: 'imports',
mapid: 'your-map-id',
version: 'v1'
});
console.log(`Branch list: ${JSON.stringify(branches.map(b => b.name))}`);
// Get Patch list from branch info
const mainBranch = branches.find(b => b.name === 'main');
const patches = mainBranch ? mainBranch.patches : [];
console.log(`Patch list: ${patches.length} versions`);
// Display version details
for (const patch of patches) {
console.log(`- ${patch.id}: ${patch.remark || '(no remark)'} by ${patch.author || 'unknown'}`);
}Open Specific Version
// Open specific patch version
const result = await drawingManager.openDrawing({
type: 'imports',
mapid: 'your-map-id',
version: 'v1',
branch: 'main',
patchId: 'patch-001' // Specify version
});Command Method
// In drawing browser:
// - View history: Expand version tree
// - Open specific version: Double-click version node
// - Create branch: Create from any version
// - Delete version: Right-click to delete
await Engine.editor.executerWithOp('OPENFROMSERVER');Conflict Resolution
When two branches modify the same entity, merge will produce conflicts.
Conflict Detection Flow
Conflict Resolution Example
import { DrawingManagerService, ConflictResolutionDialog } from 'vjcad';
const drawingManager = new DrawingManagerService();
// Assume two branches both modified the same entity
// Merge branch A first (User A submitted first)
const mergeResultA = await drawingManager.mergeBranch({
type: 'imports',
mapid: 'your-map-id',
version: 'v1',
sourceBranch: branchA,
targetBranch: 'main',
remark: 'Merge User A\'s changes'
});
// Try to merge branch B (may produce conflict)
const mergeResultB = await drawingManager.mergeBranch({
type: 'imports',
mapid: 'your-map-id',
version: 'v1',
sourceBranch: branchB,
targetBranch: 'main',
remark: 'Merge User B\'s changes'
});
if (mergeResultB.conflict && mergeResultB.conflict.hasConflict) {
console.error("Conflict detected! Two users modified the same area");
const conflictingEntities = mergeResultB.conflict.conflictingEntities || [];
const conflictingLayers = mergeResultB.conflict.conflictingLayers || [];
console.log(`Conflicting entities: ${conflictingEntities.length}`);
console.log(`Conflicting layers: ${conflictingLayers.length}`);
// Show conflict resolution dialog
const dialog = new ConflictResolutionDialog();
const resolution = await dialog.showDialog({
conflictingEntities: mergeResultB.conflict.conflictingEntities,
conflictingLayers: mergeResultB.conflict.conflictingLayers,
latestPatchId: mergeResultB.conflict.latestPatchId
});
if (resolution && resolution.action === 'resolve') {
// Re-merge with conflict resolution
const retryResult = await drawingManager.mergeBranch({
type: 'imports',
mapid: 'your-map-id',
version: 'v1',
sourceBranch: branchB,
targetBranch: 'main',
remark: 'Merge after conflict resolution',
conflictResolution: resolution.resolution
});
if (retryResult.status) {
console.log(`Conflict resolved, merge successful! Patch ID: ${retryResult.patchId}`);
}
}
}Conflict Resolution Strategy
// conflictResolution format
const conflictResolution = {
'entity-id-1': { choice: 'server' }, // Use server version
'entity-id-2': { choice: 'client' }, // Use local version
'entity-id-3': {
choice: 'client',
entityData: { /* custom merge data */ }
}
};Incremental Change Tracking
Collect Modified Entities
const doc = Engine.currentDoc;
// Collect modified entity IDs (for incremental save)
const modifiedIds = doc.collectModifiedEntityIds();
console.log('Modified entities:', modifiedIds);
// Mark entity as modified (for diff tracking)
entity._isModifiedForDiff = true;
// Clear modified marks after save
doc.clearModified();
// Restore modified marks (for local cache recovery)
doc.restoreModifiedEntityIds(savedModifiedIds);serverSource Properties
| Property | Type | Description |
|---|---|---|
mapid | string | Map/document ID |
version | string | Version number |
branchName | string | Branch name |
lastPatchId | string | Last patch ID |
editAreas | BoundingBox[] | Editable areas |
editLayers | string[] | Editable layers |
loadedEntityIds | Set<number> | Loaded entity IDs |
Data Compression
WebCAD uses WASM for data compression to improve transmission efficiency.
import { WebCadCoreService } from 'vjcad';
const wasmService = await WebCadCoreService.getInstance();
// Compress data
const webcadJson = doc.toDb();
const originalJson = JSON.stringify(webcadJson);
const compressedData = await wasmService.compressWebcadToVcad(originalJson);
console.log('Compressed size:', compressedData.byteLength);
// Decompress data
const decompressed = await wasmService.decompressVcadToWebcad(compressedData);
const restoredDoc = JSON.parse(decompressed);Next Steps
- File Operations - File open, save, import, export
- Tile Mode - Tile editing combined with version control
- Document Model - CadDocument details
- API Reference - Complete API