版本控制
大约 7 分钟
版本控制
WebCAD 提供完整的版本控制系统,支持分支管理、版本追踪、协作编辑和冲突解决。
核心概念
版本结构
图纸 (mapid/version)
└── main 分支
├── base (初始版本)
├── patch-001
├── patch-002
└── patch-003 (当前)
└── feature-xxx 分支
├── base (从main分叉)
└── patch-001Patch 版本链
每次保存都会创建一个新的 Patch 版本:
base → patch-001 → patch-002 → ...每个 patch 记录相对于父版本的增量变更。
Patch 内容包含
- 新增的实体
- 修改的实体
- 删除的实体ID
- 图层变更
- 编辑区域信息(瓦片模式)
- 元数据(作者、时间、备注等)
创建分支
分支允许从某个版本创建独立的编辑线。
分支概念
main: 主分支,默认分支feature-xxx: 功能分支fix-xxx: 修复分支- 分支间独立演进,互不影响
创建分支的场景
- 开发新功能,不影响主线
- 多人协作,各自独立编辑
- 尝试性修改,可随时放弃
API 方式
import { DrawingManagerService, BranchCreateDialog } from 'vjcad';
const drawingManager = new DrawingManagerService();
// 方式1: 通过对话框创建
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, // 源分支
sourcePatchId: result.fromPatchId, // 源版本
branchName: result.newBranchName // 新分支名称
});
if (createResult.status) {
console.log(`分支 "${result.newBranchName}" 创建成功!`);
} else {
console.error(`创建失败: ${createResult.error}`);
}
}
// 方式2: 直接调用API创建
const result = await drawingManager.createBranch({
type: 'imports',
mapid: 'your-map-id',
version: 'v1',
sourceBranch: 'main',
sourcePatchId: 'base',
branchName: 'feature-new-layer'
});命令方式
// 执行 OPENFROMSERVER 打开图纸浏览器
// 选择要创建分支的图纸和版本
// 右键点击版本,选择「创建分支」
// 输入新分支名称
await Engine.editor.executerWithOp('OPENFROMSERVER');保存版本
每次保存会创建一个新的 Patch 版本。
保存工作流程
- 从服务端打开图纸(获取原始数据)
- 编辑图纸(增删改实体)
- 执行保存
- 系统计算 diff,生成 patch
- 上传 patch 到服务器
API 方式
import { Engine, DrawingManagerService, LineEnt, CircleEnt } from 'vjcad';
const drawingManager = new DrawingManagerService();
// 第一步:从服务器打开图纸(获取原始数据用于diff计算)
const openResult = await drawingManager.openDrawing({
type: 'imports',
mapid: 'your-map-id',
version: 'v1',
branch: 'main',
patchId: 'base'
});
if (!openResult.success) {
throw new Error(`打开图纸失败: ${openResult.error}`);
}
// 加载图纸数据到编辑器
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);
// 保存原始数据(用于后续diff计算)
const originalJson = openResult.webcadJson;
await Engine.currentDoc.setOriginalJson(originalJson);
// 第二步:编辑图纸(添加新图形)
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);
// 第三步:保存(计算diff生成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', // 使用打开时的版本作为父版本
drawingName: '保存版本示例',
author: '示例用户',
remark: '添加了圆形和直线'
});
if (saveResult.status) {
if (saveResult.patchId === 'no_change') {
console.log('没有需要保存的修改');
} else {
console.log(`保存成功! Patch ID: ${saveResult.patchId}`);
// 更新本地原始数据
await Engine.currentDoc.setOriginalJson(currentJson);
}
} else if (saveResult.conflict && saveResult.conflict.hasConflict) {
console.warn('检测到与其他用户的修改冲突');
} else {
console.error(`保存失败: ${saveResult.error}`);
}命令方式
// 执行 SAVESERVER 保存到服务器
await Engine.editor.executerWithOp('SAVESERVER');删除 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 已删除');
}合并分支
完整的分支工作流
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}`;
// 第1步:创建分支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(`创建分支A失败: ${createResultA.error}`);
}
// 第2步:创建分支B
const createResultB = await drawingManager.createBranch({
type: 'imports',
mapid: 'your-map-id',
version: 'v1',
sourceBranch: 'main',
sourcePatchId: 'base',
branchName: branchB
});
// 第3步:在分支A上修改
const openResultA = await drawingManager.openDrawing({
type: 'imports',
mapid: 'your-map-id',
version: 'v1',
branch: branchA,
patchId: 'base'
});
// 加载图纸并添加图形...
const circleA = new CircleEnt([centerX - size, centerY], size * 0.5);
circleA.setDefaults();
circleA.color = 1; // 红色
Engine.addEntities(circleA);
// 保存分支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: '在分支A添加红色圆形'
});
// 第4步:在分支B上修改(类似操作)
// ...
// 第5步:合并分支A到main
const mergeResultA = await drawingManager.mergeBranch({
type: 'imports',
mapid: 'your-map-id',
version: 'v1',
sourceBranch: branchA,
targetBranch: 'main',
remark: '合并分支A的红色圆形'
});
if (mergeResultA.status) {
console.log(`分支A合并到main成功!Patch ID: ${mergeResultA.patchId}`);
} else if (mergeResultA.conflict && mergeResultA.conflict.hasConflict) {
console.warn("检测到冲突");
} else {
console.error(`合并失败: ${mergeResultA.error}`);
}
// 第6步:合并分支B到main
const mergeResultB = await drawingManager.mergeBranch({
type: 'imports',
mapid: 'your-map-id',
version: 'v1',
sourceBranch: branchB,
targetBranch: 'main',
remark: '合并分支B的绿色直线'
});清理分支
// 删除分支
const deleteResult = await drawingManager.deleteBranch({
type: 'imports',
mapid: 'your-map-id',
version: 'v1',
branchName: 'feature-xxx'
});
if (deleteResult.status) {
console.log('分支已删除');
}版本历史
版本信息包含
- Patch ID: 唯一标识
- 父版本ID: 版本链关系
- 作者: 提交人
- 时间: 提交时间
- 备注: 修改说明
- 变更统计: 增/删/改数量
获取分支和版本列表
import { DrawingManagerService } from 'vjcad';
const drawingManager = new DrawingManagerService();
// 获取分支列表(包含各分支的patch信息)
const branches = await drawingManager.listBranches({
type: 'imports',
mapid: 'your-map-id',
version: 'v1'
});
console.log(`分支列表: ${JSON.stringify(branches.map(b => b.name))}`);
// 从分支信息中获取Patch列表
const mainBranch = branches.find(b => b.name === 'main');
const patches = mainBranch ? mainBranch.patches : [];
console.log(`Patch列表: ${patches.length} 个版本`);
// 显示版本详情
for (const patch of patches) {
console.log(`- ${patch.id}: ${patch.remark || '(无备注)'} by ${patch.author || 'unknown'}`);
}打开特定版本
// 打开特定 patch 版本
const result = await drawingManager.openDrawing({
type: 'imports',
mapid: 'your-map-id',
version: 'v1',
branch: 'main',
patchId: 'patch-001' // 指定版本
});命令方式
// 在图纸浏览器中:
// - 查看历史: 展开版本树
// - 打开特定版本: 双击版本节点
// - 创建分支: 从任意版本创建
// - 删除版本: 右键删除
await Engine.editor.executerWithOp('OPENFROMSERVER');冲突解决
当两个分支修改了同一个实体时,合并会产生冲突。
冲突检测流程
冲突解决示例
import { DrawingManagerService, ConflictResolutionDialog } from 'vjcad';
const drawingManager = new DrawingManagerService();
// 假设两个分支都修改了同一个实体
// 先合并分支A(用户A先提交)
const mergeResultA = await drawingManager.mergeBranch({
type: 'imports',
mapid: 'your-map-id',
version: 'v1',
sourceBranch: branchA,
targetBranch: 'main',
remark: '合并用户A的修改'
});
// 尝试合并分支B(可能产生冲突)
const mergeResultB = await drawingManager.mergeBranch({
type: 'imports',
mapid: 'your-map-id',
version: 'v1',
sourceBranch: branchB,
targetBranch: 'main',
remark: '合并用户B的修改'
});
if (mergeResultB.conflict && mergeResultB.conflict.hasConflict) {
console.error("检测到冲突!两个用户修改了相同区域");
const conflictingEntities = mergeResultB.conflict.conflictingEntities || [];
const conflictingLayers = mergeResultB.conflict.conflictingLayers || [];
console.log(`冲突实体数: ${conflictingEntities.length}`);
console.log(`冲突图层数: ${conflictingLayers.length}`);
// 显示冲突解决对话框
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') {
// 使用冲突解决方案重新合并
const retryResult = await drawingManager.mergeBranch({
type: 'imports',
mapid: 'your-map-id',
version: 'v1',
sourceBranch: branchB,
targetBranch: 'main',
remark: '解决冲突后合并',
conflictResolution: resolution.resolution
});
if (retryResult.status) {
console.log(`冲突解决,合并成功!Patch ID: ${retryResult.patchId}`);
}
}
}冲突解决策略
// conflictResolution 格式
const conflictResolution = {
'entity-id-1': { choice: 'server' }, // 使用服务端版本
'entity-id-2': { choice: 'client' }, // 使用本地版本
'entity-id-3': {
choice: 'client',
entityData: { /* 自定义合并数据 */ }
}
};增量修改追踪
收集修改的实体
const doc = Engine.currentDoc;
// 收集已修改的实体ID(用于增量保存)
const modifiedIds = doc.collectModifiedEntityIds();
console.log('修改的实体:', modifiedIds);
// 标记实体为已修改(用于diff追踪)
entity._isModifiedForDiff = true;
// 保存后清除修改标记
doc.clearModified();
// 恢复修改标记(用于本地缓存恢复)
doc.restoreModifiedEntityIds(savedModifiedIds);serverSource 属性
| 属性 | 类型 | 说明 |
|---|---|---|
mapid | string | 地图/文档ID |
version | string | 版本号 |
branchName | string | 分支名 |
lastPatchId | string | 最后补丁ID |
editAreas | BoundingBox[] | 可编辑区域 |
editLayers | string[] | 可编辑图层 |
loadedEntityIds | Set<number> | 已加载实体ID |
数据压缩
WebCAD 使用 WASM 进行数据压缩,提高传输效率。
import { WebCadCoreService } from 'vjcad';
const wasmService = await WebCadCoreService.getInstance();
// 压缩数据
const webcadJson = doc.toDb();
const originalJson = JSON.stringify(webcadJson);
const compressedData = await wasmService.compressWebcadToVcad(originalJson);
console.log('压缩后大小:', compressedData.byteLength);
// 解压数据
const decompressed = await wasmService.decompressVcadToWebcad(compressedData);
const restoredDoc = JSON.parse(decompressed);