11|HarmonyOS图纸批注锚点与图层独立保存【共创稿事节】
机电图纸批注工作台已经解决两层坐标问题:第一篇把点位存成原始图纸坐标,第二篇让旋转、缩放和平移后的屏幕触点能够通过逆矩阵回到同一坐标。接下来要处理的是数据归属。给水阀门、强电线路和文字说明都可能覆盖在一张图纸上;当用户关闭强电层时,其他批注不能跟着消失,更不能把“隐藏”误当成删除后从存储中移除。
继续沿用首层机电平面图,页逻辑尺寸4200×2970,视口960×640。给水阀门的锚点仍为(2600,1850),页面是否旋转十五度、缩放到1.5倍,都不应改变这一份数据。新的设计把图纸、图层、批注三个实体分开,图层控制可见性与绘制层级,批注持有长期业务锚点,视口变换仍由前一篇的矩阵模块负责。

一、图层不是批注数组上的一个临时过滤词
我把图纸实体记为pageId、原始页宽高和图纸版本;图层实体记录layerId、pageId、名称、类型、zIndex以及可编辑策略;批注记录包含annotationId、pageId、layerId、anchorX、anchorY、内容、形状类型和revision。三者通过稳定标识关联,而不是依靠界面列表索引。一个批注只有一个归属图层,改变显示顺序并不会修改它的业务位置。
图层的显示状态属于当前阅读视图,应与永久存储的批注数据分开。比如电气工程师暂时关闭给排水层,是为了减少画面遮挡;其他人再次打开同一张图纸,不能因为这位工程师的临时操作而丢掉管线标记。可以按用户偏好记忆图层开关,但这种偏好与批注内容是不同数据,保存时也不能共用删除逻辑。
还需要明确跨页边界。同样命名为“给排水层”的图层可以出现在不同图纸页中,却不能直接共用同一个业务标识。存储层约束(pageId, layerId)的归属关系,添加批注时必须校验两者对应同一张图纸。否则筛选第一页批注时,可能意外取出第二页相同名称的标记,图形位置看似合理,实际上指向错误设备。
二、写入批注前先校验页面、图层和锚点
输入点位必须落在原始页的有效范围内,且应是有限数字。极大值、负数、NaN或无穷大一旦进入坐标变换,会污染后续的绘制边界、命中测试和导出。对于来自逆矩阵的浮点值,可以依据业务精度记录小数坐标,但不能因为屏幕像素是整数就把页坐标一概四舍五入。
下面用应用层代码实现批注的写入规则。它返回新的存储快照,不直接修改传入数组;写入时查找稳定主键、校验图层与页的关系,新增和更新分别有不同版本检查。持久化实现应把查重、校验和写入放到同一事务,避免两个编辑入口同时覆盖同一条记录。
function saveAnnotation(state, input, expectedRevision = null) {
const page = state.pages[input.pageId];
const layer = state.layers[input.layerId];
if (!page || !layer || layer.pageId !== input.pageId) throw new Error('BAD_LAYER');
for (const v of [input.anchorX, input.anchorY]) {
if (typeof v !== 'number' || !Number.isFinite(v)) throw new Error('BAD_POINT');
}
if (input.anchorX < 0 || input.anchorX > page.width ||
input.anchorY < 0 || input.anchorY > page.height) throw new Error('OUT_OF_PAGE');
const previous = state.annotations[input.annotationId];
if (previous && previous.revision !== expectedRevision) throw new Error('VERSION_CONFLICT');
if (!previous && expectedRevision !== null) throw new Error('MISSING_ANNOTATION');
const record = { ...input, revision: (previous?.revision ?? 0) + 1 };
return { ...state, annotations: { ...state.annotations, [input.annotationId]: record } };
}
保存前还有一条约束:编辑页面仅传入声明过的字段,不能用任意表单对象覆盖旧记录。批注的创建时间、所属图纸以及创建者信息应有明确的修改策略;在修改说明文字时保留原锚点,移动锚点时保留原内容。若每次保存都重新生成annotationId,会让图层统计、撤销记录和后续同步全部失去关联基础。
三、绘制层级和可见性如何与数据隔离
实际绘制时需要两步筛选:先选择当前pageId下可见的图层,再提取归属这些图层的批注并按图层zIndex排序。这里的“不可见”只影响本次绘制列表,不影响持久化记录、引用关系或审计信息。对于锁定的图层,还要阻止误编辑;不可编辑和不可见是两个不同条件,不能共用一个布尔变量。
下面的函数只计算本次图纸视图的绘制集合。它显式接收用户当前的图层可见性,默认值来自图层配置,并用稳定主键消除排序同值时的随机闪动。实际绘制阶段仍使用第二篇的相机矩阵将anchorX和anchorY变换到视口像素。
function selectVisibleAnnotations(state, pageId, visibleOverride = {}) {
const visible = Object.values(state.layers)
.filter(l => l.pageId === pageId && (visibleOverride[l.layerId] ?? l.visible))
.sort((a, b) => a.zIndex - b.zIndex || a.layerId.localeCompare(b.layerId));
const rank = new Map(visible.map((l, index) => [l.layerId, index]));
return Object.values(state.annotations)
.filter(a => a.pageId === pageId && rank.has(a.layerId))
.sort((a, b) => rank.get(a.layerId) - rank.get(b.layerId) ||
a.annotationId.localeCompare(b.annotationId));
}
const overrides = { electric: false };
const tinyState = { layers: { plumbing: { layerId:'plumbing',pageId:'first-floor',zIndex:1,visible:true },
electric: { layerId:'electric',pageId:'first-floor',zIndex:2,visible:true } },
annotations: { valve: { annotationId:'valve',pageId:'first-floor',layerId:'plumbing',anchorX:2600,anchorY:1850 } } };
console.log('绘制数量', selectVisibleAnnotations(tinyState, 'first-floor', overrides).length);

当给排水层开启、强电层关闭时,给水阀门的坐标仍是(2600,1850),只需选择对应绘制集合并更新可见区域。画面上隐藏某一层后,批注列表可以按当前可见层筛选,也可以允许“查看全部”,但必须明确告诉用户过滤状态,避免误以为数据丢失。用户切换图层后不必重新写入每个批注的业务记录,这能减少多余数据库更新。
四、图层顺序、删除和迁移必须有独立策略
图层zIndex决定覆盖顺序,不能取代图层主键。需要调整顺序时,仅更新排序字段,并保证相同排序值下仍有确定的次级排序规则。图层删除则复杂得多:它可能还被几百条批注引用。直接执行级联删除容易丢失重要现场记录。更稳妥的做法是明确“删除空图层”“迁移批注后删除”和“归档图层”三个业务操作,并在界面中区分。
给水阀门原本位于给排水层,若因专业调整需要移动到其他图层,应检查目标图层是否属于同一页、类型是否允许、编辑者是否具备操作资格,然后在同一事务中修改归属并增加修订版本。位置坐标保持不变。单纯勾选显示开关则既不增加批注版本,也不改写修订时间,这样才能准确判断哪些动作真正修改了工程资料。
批注长期保存的另一个问题是恢复:页面缩放状态通常是短期交互数据,不必和图纸批注放在同一张表;annotationId、页锚点、图层引用和版本号则必须一并读取。重新打开应用时先加载图纸及图层,再装载批注,最后根据当前设备视口计算显示矩阵。顺序颠倒会让尚未取得页尺寸的批注提前参与绘制,出现异常坐标或短暂错位。
五、用不变量核查持久化前后的记录
我更关注两项回归条件:第一,关闭某图层不会让批注总量变化;第二,保存和重新加载后,锚点与图层归属保持一致。图层开关变化是视图状态改变,应该只改变selectVisibleAnnotations()结果的数量,不应该影响annotations中的原始数据。
第三段代码将批注导出为可比较的稳定数据,并对页引用与图层引用做完整性检查。它不依赖当前缩放、旋转角度和是否显示某个图层,因此可以用于本地持久化后的回读比对。字段输出顺序固定,后续增加新字段时应同步更新版本规则,而不是悄悄改变既有序列。
function auditAnnotations(state, pageId) {
const page = state.pages[pageId];
if (!page) throw new Error('PAGE_NOT_FOUND');
const records = Object.values(state.annotations).filter(a => a.pageId === pageId)
.sort((a, b) => a.annotationId.localeCompare(b.annotationId));
for (const a of records) {
if (!state.layers[a.layerId] || state.layers[a.layerId].pageId !== pageId) {
throw new Error('ORPHAN_LAYER');
}
if (![a.anchorX, a.anchorY].every(Number.isFinite)) throw new Error('INVALID_ANCHOR');
}
return records.map(a => ({ id: a.annotationId, layerId: a.layerId,
x: a.anchorX, y: a.anchorY, revision: a.revision, content: a.content }));
}

排查时可以从批注主键一路找到图层与页面:查annotationId是否稳定、layerId是否有效、页逻辑范围是否满足,再看当前视图的可见性覆盖值,最后核对相机矩阵。如果数据库里的锚点正确但屏幕位置不对,应优先检查屏幕到页坐标的转换与视口尺寸;如果屏幕绘制正确而重新打开后批注缺失,则应核对事务提交、加载顺序和图层过滤条件。
画布绘制涉及的线条、点位、图元设置可以参照华为ArkGraphics 2D的Canvas与Brush/Pen文档;涉及长期存储时,应使用已经确认的关系型数据库接口来管理记录。离开图纸页面时释放多余的绘制订阅和临时缓存,保存动作则在事务完成后结束数据库操作,避免把长期业务状态绑定到某个已经销毁的页面对象。
这一章建立的核心约定很具体:页坐标是批注的真实位置,图层是独立的数据归属和显示顺序,显隐只是视图选择。下篇加入多点触控和笔输入时,不同交互手势最终仍要通过同一锚点与图层写入入口,避免每种输入方式都发展出一套自己的保存规则。
相关华为开发者文档:
更多推荐



所有评论(0)