16|HarmonyOS图纸导出坐标一致性与资源回收【共创稿事节】
首层机电平面图的批注工作台已经把几条最容易互相干扰的数据关系分开:图纸页逻辑尺寸4200×2970,视口基准960×640,给水阀门的原始页锚点是(2600,1850);缩放、旋转和拖动只影响从页到屏幕的变换。给排水层、强电层和文字批注层独立保存,第六篇采用512逻辑单位的可见块调度,第七篇又用追加事件与修订号处理撤销。此时工程看起来可以结束,但把图纸输出给施工和复核人员时,很容易出现新的偏移。
问题通常出现在直接截取当前屏幕。屏幕上只加载了可见区域的几个图块,文字标签也已为避让调整显示位置。如果把这一帧直接保存,导出文件会缺掉视口外的管线;如果把经过旋转的屏幕坐标当作页坐标,给水阀门甚至会漂到另一个房间。这一篇把“供用户浏览的视口”与“用于生成文件的输出画布”彻底隔离,再补齐临时绘制对象、图块缓存和文件句柄的释放过程。

一、导出坐标只由页尺寸与目标尺寸决定
先约定一个明确的输出规格:从4200×2970的页空间生成2100×1485的矢量图,宽高都缩小到一半。固定页锚点(2600,1850)的输出位置应该是(1300,925)。这个结果与用户此刻在手机上缩放到150%、旋转15°、滚动到哪块图无关。导出比例按输出宽高分别计算,如果两者不一致且业务要求等比例,就应停止导出并提示尺寸设置错误,不能悄悄把图形压扁。
屏幕矩阵负责page → viewport,输出矩阵负责page → export,两条路径共享页坐标源,却不串接。先把页坐标变到屏幕、再根据屏幕像素反推输出文件,会让缩放误差、设备像素密度和浮点圆整连续叠加。特别是给水阀门旁的文字标签,它的显示框偏移在第五篇已被保存为相对于页锚点的逻辑量;导出时应把文字框和连接线的端点也按目标矩阵转换,而不是仅转换红色锚点。
要解决的问题是给这条变换增加输入范围与比例检查,并固定一组可复查的算例。
const assert = require('node:assert/strict');
const page = Object.freeze({width:4200,height:2970});
const output = Object.freeze({width:2100,height:1485});
function pageToOutput(point, source, target) {
if (![point.x,point.y,source.width,source.height,
target.width,target.height].every(Number.isFinite))
throw new Error('坐标与尺寸必须为有限数值');
if (source.width <= 0 || source.height <= 0 ||
target.width <= 0 || target.height <= 0)
throw new Error('尺寸必须大于零');
if (point.x < 0 || point.x > source.width ||
point.y < 0 || point.y > source.height)
throw new Error('锚点超出原始图纸');
const sx=target.width/source.width;
const sy=target.height/source.height;
if (Math.abs(sx-sy)>1e-9) throw new Error('输出比例不一致');
return {x:point.x*sx,y:point.y*sy};
}
const valve=pageToOutput({x:2600,y:1850},page,output);
assert.deepEqual(valve,{x:1300,y:925});
assert.deepEqual(pageToOutput({x:0,y:0},page,output),{x:0,y:0});
assert.deepEqual(pageToOutput({x:4200,y:2970},page,output),{x:2100,y:1485});
这一段保留了转换后的浮点数,只有输出到位图像素时才按统一规则取整。如果同一份图纸同时导出SVG和PNG,可让二者先共享一个浮点几何快照,再由各自编码器处理像素网格。不要在业务记录里提前覆盖anchorX、anchorY,因为固定页锚点仍是后续编辑与历史回放的唯一参考。
二、导出前冻结一份明确的修订快照
第七篇建立了批注事件日志,撤销会添加补偿事件,而不是删除旧历史。因此导出不是遍历全部事件逐条绘图,而是根据最终修订重建当前批注实体,再固定为一个只读快照。快照包含drawingId、drawingRevision、exportWidth、exportHeight、可见图层集合、annotationId、layerId、anchorX、anchorY、text、labelOffset和revision;不包含当前窗口宽度、触点临时缓冲、缩放滑块值或分块缓存引用。
假设本次选择“导出当前可见图层”,那么隐藏的强电层在文件里应保持隐藏,给排水层和文字批注层继续参与绘制。这并不意味着强电层的数据被删除:导出选择仅影响本次输出范围。如果业务需要同时导出全量图层,提供另一个明确选项,避免用户以为“没有显示”就是“没有保存”。同一图层里的元素应根据稳定业务排序键绘制,以免不同设备拿到不同迭代顺序而影响遮盖关系。
要解决的问题是让正在执行的导出使用同一份修订,而不是一边绘制一边接收新编辑。
const assert = require('node:assert/strict');
function exportSnapshot(drawing, annotations, layers) {
const visible = new Set(layers.filter(l => l.visible).map(l => l.id));
const records = annotations
.filter(a => a.pageId===drawing.pageId && !a.deleted && visible.has(a.layerId))
.map(a => Object.freeze({id:a.id,layerId:a.layerId,
x:a.anchorX,y:a.anchorY,text:a.text,revision:a.revision}))
.sort((a,b) => a.layerId.localeCompare(b.layerId) || a.id.localeCompare(b.id));
return Object.freeze({drawingId:drawing.id,pageId:drawing.pageId,
revision:drawing.revision,source:Object.freeze({width:4200,height:2970}),
target:Object.freeze({width:2100,height:1485}),
annotations:Object.freeze(records)});
}
const drawing={id:'first-floor',pageId:'first',revision:7};
const annotations=[
{id:'valve',pageId:'first',layerId:'water',anchorX:2600,
anchorY:1850,text:'给水阀门',revision:7,deleted:false},
{id:'cable',pageId:'first',layerId:'power',anchorX:900,
anchorY:700,text:'电缆桥架',revision:5,deleted:false}
];
const snap=exportSnapshot(drawing,annotations,[
{id:'water',visible:true},{id:'power',visible:false}]);
assert.equal(snap.annotations.length,1);
assert.equal(snap.annotations[0].id,'valve');
assert.equal(snap.revision,7);
在实际持久层读取时,要么使用一致性读事务取得图纸版本与对应的批注集合,要么先读取版本、完成快照后再复查版本;发生变化时重新取数。只把页面列表复制成数组并不足以解决数据库写入竞态。导出任务可以记录采用的修订号与图层选择摘要,便于用户之后核对同一份文件的来源,但这条导出记录不能修改已有批注的历史版本。

三、完整导出不能受可见块缓存限制
第六篇为显示性能把图纸分成512逻辑单位的块,视口只保留当前附近的数据。导出整页则要建立新的绘制上下文,顺序取用全部需要的图纸图元。可以继续复用相同的几何解析与文字排布代码,却不能把visibleTiles当成导出数据的全集。底图应按原始页坐标重建,批注从上面的固定快照取得,两者最后经同一导出矩阵投到输出空间。
如果用户导出2100×1485的SVG矢量文件,可以先由应用业务层生成SVG几何文本,再借助实际工程的文件写入能力保存。SVG只是一个便于核对坐标的文件格式选择;需要PNG时应采用与目标SDK匹配的ArkGraphics绘制和Image Kit编码流程,并正确处理PixelMap和编码器的生命周期。无论哪种格式,失败后都不能残留一个被误当成完整结果的半写入文件。
下面使用Node.js构造最小矢量输出片段,并让临时目录在成功或异常时均被清理。这里把所有图元写入内存文本只是小图纸的实现方式;大图纸需按块输出或按合适预算分段绘制,以免整页峰值内存过高。
const assert = require('node:assert/strict');
const fs = require('node:fs/promises');
const os = require('node:os');
const path = require('node:path');
function escapeXml(text) {
return String(text).replace(/[&<>"']/g,c => ({
'&':'&','<':'<','>':'>', '"':'"', "'":'''
})[c]);
}
function svgForValve() {
const x=2600*.5, y=1850*.5;
return `<svg xmlns="http://www.w3.org/2000/svg" `+
`width="2100" height="1485" viewBox="0 0 2100 1485">`+
`<path d="M 100 900 L 1500 900" stroke="#1780cf" fill="none"/>`+
`<circle cx="${x}" cy="${y}" r="10" fill="#ea425b"/>`+
`<text x="${x+20}" y="${y-15}" font-size="24">`+
`${escapeXml('给水阀门')}</text></svg>`;
}
async function stageSvg() {
const folder=await fs.mkdtemp(path.join(os.tmpdir(),'drawing-stage-'));
try {
const tempFile=path.join(folder,'output.svg');
await fs.writeFile(tempFile,svgForValve(),'utf8');
const content=await fs.readFile(tempFile,'utf8');
if (!content.includes('cx="1300" cy="925"'))
throw new Error('导出锚点不一致');
return {bytes:Buffer.byteLength(content),content};
} finally {
await fs.rm(folder,{recursive:true,force:true});
}
}
stageSvg().then(result => {
assert(result.bytes>200);
assert(result.content.includes('给水阀门'));
}).catch(error => { console.error(error); process.exitCode=1; });
正式文件交付还需要将已完成的临时文件经业务校验后保存到用户选择的目标位置,并避免跨文件系统移动失败造成覆盖。导出过程中被借用的图块资源要标记引用计数;正在绘制的块不能被视口滚动同时释放。对于图片编码,华为Image Kit文档明确提示异步操作完成后再释放PixelMap和ImagePacker,因此清理动作必须放在等待编码完成的finally路径,而不是调用编码后立刻执行。
四、异常处理从三组坐标开始排查
如果导出图上给水阀门偏移,先记录页锚点、目标画布和输出矩阵,检验(2600,1850)是否得到(1300,925)。若数值正确但看起来仍错位,再检查底图与批注是否使用相同的原点、裁剪区域与缩放顺序。第二篇的屏幕逆矩阵只应用于交互触点,不能混入导出逻辑。若只有文字框发生漂移,检查第五篇的标签避让结果是否以页空间保存,以及字体替换后是否采用了不同的测量尺寸。
另一个典型问题是“手机浏览正常,导出的右半张图为空白”。此时应检查图纸源数据是否覆盖所有块、导出时是否绕过了视口可见区裁剪、读取任务是否提前终止。若导出任务中途取消,应终止未开始的图块读取,等待已占用资源的绘制结束,再关闭临时文件与图像编码器。正在使用的共享图纸数据服务不应因为一个导出页面被关闭而中断其他窗口。导出完成后,保留版本信息与文件元数据,清理临时画布、当前导出专属缓存及订阅。

至此,这条连载从页坐标、逆矩阵、独立图层、笔触仲裁、文字避让和分块调度,走到可撤销版本与完整导出。保持稳定的不是某一张屏幕截图,而是原始页空间、批注修订及每一步清楚的转换关系。把导出视作固定快照上的单独工作,又把临时资源回收放在完成或失败后的明确阶段,图纸才能在不同尺寸与不同设备间保持内容位置一致。
参考华为开发者资料:ArkGraphics 2D离屏画布与绘制结果、Canvas矩阵及保存恢复、Image Kit图片编码常见问题及图片绘制与PixelMap。上面的坐标映射、修订快照和SVG文件组装属于应用业务算法;导出存储与编码需结合目标项目的SDK版本采用对应实现。
更多推荐



所有评论(0)