原生鸿蒙像素画板实战 17:项目合并 把两个项目合成一个不能理解成“把两张图贴在一起”。像素画项目里还有画布尺寸、图层名称、透明度、背景层、帧、工具配置和项目 ID。若合并直接改写任意一个原项目用户很容易失去原始版本若只复制可见合成图又会丢失之后继续编辑需要的图层结构。bitArt 的合并策略是读取多个项目创建一个新的持久化项目把每个项目的图层放进统一画布并保留原项目文件。合并的产物必须是一个新项目项目之间的画布尺寸可能不同。简单使用第一个项目的尺寸会裁掉其他项目内容直接拉伸又会破坏像素比例。图层名称也可能重复例如每个项目都有“线稿”或“背景”。如果新项目里保留同名图层用户后续编辑时很难辨认来源。更重要的是合并项目不是一次普通保存它必须生成新 projectId不能把原项目覆盖成混合版本。合并流程中页面与存储层各做什么合并项目比切换项目更需要保守新结果必须有自己的身份原始项目必须完整保留。只有这样用户才敢把草稿、线稿和配色稿放进同一次合并。先选统一画布再放置每一层像素当前实现先读取选中的所有项目少于两个有效项目则直接返回 null。随后计算合并画布宽高统计所有图层名称的总数并在写入时为重复名称生成带编号的结果。每个源图层得到新的 layer-id像素通过 placePixelsOnMergedCanvas 放到统一尺寸的透明数组里合并后再补一层锁定背景。合并项目沿用第一个项目的一部分编辑配置例如活动工具、当前色、调色板和自动保存间隔但图层、像素和帧由新的结果生成。帧不会把多个项目的原动画时间轴硬拼在一起而是创建一条新的初始帧。这是一种保守但可解释的选择先保证静态图层结构可编辑再把跨项目动画作为独立需求处理。合并从读取多个独立项目开始任何有效数量不足的情况都不会产出半成品。static async mergeProjects(context: common.UIAbilityContext, projectIds: string[]) { const projects: EditorPersistentProject[] []; for (let index 0; index projectIds.length; index) { const project await EditorPersistenceService.readProjectFile(context, projectIds[index]); if (project ! null) projects.push(project); } if (projects.length 2) return null; const canvasWidth EditorPersistenceService.resolveMergedCanvasWidth(projects); const canvasHeight EditorPersistenceService.resolveMergedCanvasHeight(projects); // 后续为每个源图层分配新 id、名称和目标像素数组 }重名图层和动画状态要有明确取舍画布合并的关键是“放置”而非缩放。目标宽高取所有有效项目对应维度的最大值源项目的每层像素按自己的 width 和 height 读取再居中写入统一尺寸没有被源图层占用的位置保持透明。放置函数仍保留边界检查但正常合并不会因为画布较小而裁掉一个合法源项目。图层序列号从 1 重新分配背景层固定加入最后使新项目的内部 ID 不会和任何原项目冲突。for (let projectIndex 0; projectIndex projects.length; projectIndex) { const project projects[projectIndex]; const sourceWidth EditorPersistenceService.resolveProjectCanvasWidth(project); const sourceHeight EditorPersistenceService.resolveProjectCanvasHeight(project); for (let layerIndex 0; layerIndex project.layers.length; layerIndex) { mergedLayers.push({ id: layer- serial.toString(), name: EditorPersistenceService.resolveMergedLayerName(project.layers[layerIndex].name, layerNameTotals, layerNameIndexes), visible: project.layers[layerIndex].visible, locked: project.layers[layerIndex].locked, opacity: project.layers[layerIndex].opacity }); mergedLayersPixels.push(EditorPersistenceService.placePixelsOnMergedCanvas(project.layersPixels[layerIndex], sourceWidth, sourceHeight, canvasWidth, canvasHeight)); serial; } }合并完成后才切换到新的项目身份工作流要求至少选择两个项目并先结束当前笔触、保存当前项目再调用持久化服务。合并成功时应用新项目、退出管理模式、清空 selectedProjectIds、打开图层面板并重新安排自动保存。原项目仍留在项目列表这使“合并”成为可回退的创作动作而不是破坏性批处理。合并成功时才应用新项目并清空多选状态失败则保留原项目和管理上下文供用户继续选择。if (target.selectedProjectIds.length 2) { hooks.showToast(至少选择两个项目); return; } hooks.endStroke(); await hooks.saveNow(true); const mergedProject await EditorPersistenceService.mergeProjects(context, target.selectedProjectIds); if (mergedProject ! null) { hooks.applyPersistentProject(mergedProject); target.selectedProjectIds []; hooks.scheduleAutoSave(); }多项目合成时不能模糊的取舍不要把合并结果保存回第一个项目必须创建新的 projectId 和新的项目名称。不同画布尺寸应该按像素位置放置不应在合并时做平滑缩放。不要假装多项目动画可以无损拼接当前实现明确新建初始帧避免时间轴语义混乱。图层名称重名处理看似只是 UI 问题实际影响后续自动化和用户判断。合并前先统计总数再在写入时累加索引能确保只对真正重复的名称追加后缀。像素数据和图层元数据都复制到新数组避免合并后引用仍指向某个原项目的内存对象。合并前先说清数据如何保留合并功能最好先用可辨认的源项目做手工样本。例如项目 A 放红色对角线和名为“线稿”的图层项目 B 放蓝色方块和同名图层同时设置不同尺寸。合并后应能从图层面板看出两份内容都存在名称也能区分再分别关闭、移动和修改它们确认它们确实是独立图层而非一张扁平合成图。合并后的项目还要和原项目并排检查。修改新项目、保存、重开再返回 A 与 B原画布不应变化。因为合并保留的是新的项目身份最重要的验收不是“画面合起来了”而是“原始草稿仍是原始草稿”。对于创作工具这条可逆性比自动做出复杂混合效果更值得优先保证。怎样确认合并没有覆盖原稿合并检查必须回到原项目确认数据未被覆盖而不只检查新项目是否出现。选两个不同尺寸、都含“线稿”图层的项目确认合并后出现可区分的图层名称。合并后修改新项目的某一层返回原项目确认原像素没有变化。选择不足两个项目时确认系统给出明确提示且不创建空项目。合并含多层和透明像素的项目确认图层顺序、透明区域和锁定背景符合预期。项目合并检查记录 少于两个项目不创建结果 输出项目拥有新 ID 和新名称 重复图层名可区分 原项目保持不变合并结果可独立编辑项目合并暂不承诺的内容当前合并处理的是本地项目的图层集合不提供对齐辅助线、混合模式选择、跨项目帧拼接或冲突可视化。它适合把草稿、线稿和配色稿聚到一个可继续编辑的项目中而不是替代完整的版本控制系统。这一篇的重点是在保留原稿的前提下创建一个新的可编辑合并项目。项目结构稳定之后可以把一些常用图案做成素材入口下一篇先处理素材库如何把数据变成可读的像素预览。