ColorYourModel logoColorYourModel Docs
GitHub ↗

ColorYourModel — 通往「可用 v0.1」开发路线图(正式版 · REVISE-1)

项 值
文档日期 2026-08-10
状态 ACTIVE — 取代 docs/05 的候选路线图(已证伪)
版本 REVISE-1 — 已吸收独立 REFUTE 的 3 blocker + 6 major
分支 feat_theme_persist_paint_ux(持续 commit,不 push)
v0.1 定义 达到可用状态才认可为 v0.1 —— 以 PRD §6 验收门槛为准,不以「代码是否写完」为准
方法 Adversarial Development Loop:PLAN → REFUTE → REVISE → IMPLEMENT → TEST → RETROSPECT
实测基线 npx tsc --noEmit exit 0;cargo test --lib 103 passed(REVISE-1 基线 13 → 2026-08-09 现状 67 → 2026-08-10 现状 103;覆盖已扩至 export 预设库 + mesh/kdtree + segment 全算法 + split/merge);git tag -l 空

如何使用本文档:从 §3 Phase 0 开始逐项勾选。每个 Gate 必须产出二值结论 + 失败分支,未达 EXIT 不得进入下一 Phase。每完成一个 Phase 追加一条 loop-journal.md 条目。


0. 当前看板(滚动更新 · 2026-08-10)

泳道 条目 状态 凭据 / 阻塞点
Phase -1 裁决 D1–D8 ✅ 全部裁决 §1
Phase 0 前置 P1-8 render 期副作用(3 处) ✅ 完成 550289e + 4b8da67
Gate 0a HUD 仪器校准 ✅ 完成 8ea2217
Gate 0b 3MF 格式前置验证 🟢 核心完成(预设库 2e4e070) 清单 v2 第 1–5 项 ✅;cargo test --lib 66 passed;sample 解包验证 printer_settings_id 逐字相等、machine 78 keys 含 gcode 宏、inherits_group 8 元素。剩余:D6-a 非静默预览 UI(下轮)+ spool 三处复验(待需求方)
├ paint_color 编码器 位流编码,6 黄金值锁定 ✅ 完成 export/paint_color.rs,C1/C2
├ median-cut 量化 ≤16 槽,确定性 ✅ 完成 export/quantize.rs,C1/C5
├ project_settings.config 单一扁平 DynamicPrintConfig writer ✅ 预设库重构完成 export/project_config.rs:槽位数=filament_colour.size();混合耗材写 N 长数组(set_at 散射);inherits_group=N+2(process/filaments/printer 父);selected 路径嵌入 machine+process 完整配置(REFUTE-6)
├ 3dmodel.model 改写 删 basematerials,ns 改 core/2015/02 ✅ 完成 export/threemf.rs,C3
├ D6-a 非静默量化预览 量化改色须告知用户 ⬜ 未开工 当前静默改色,违 WYSIWYG。backlog:待需求方确认导出交互后迭代
└ spool 卡三处复验(箭头所指) Orca 重开 paint_color_sample.3mf 🟡 待需求方复验 新 sample(2e4e070 selected 路径):下拉框应显示 Snapmaker U1 (0.4 nozzle)、喷嘴 0.4、工艺 0.20 Standard、spool 卡完整。首次弹 "Customized Preset" 确认框属正常(勾选不再提醒)
Gate 0b′ 撤销架构归一(R4) ✅ 已裁决 + 实现完成 D7 + D8
├ S1 后端统一时间线 mesh.history 单栈 + stroke_id ✅ 完成 5c85e80、7522659、f43cb44
├ S2 前端切换 删前端双栈,改旗标 + bridge ✅ 完成 7eaef39,tsc --noEmit exit 0
├ S2 运行期验证 v2 Ctrl+Z/Y、笔画合并、分区撤销可见性 ✅ 通过 命名 39736dd / 合并 f7d2f83 / 拆分 72455f3 全部接入后端时间线且可撤销,需求方多轮实测无回归反馈
└ 重出含 embedded preset 的构建 含 REFUTE-4 后端改动(machine/process/filament 三份 preset) ✅ 完成 多轮 tauri build 成功(B3 例行化)
Gate 0c 150 万面撤销性能(≤200ms/op、≤256MB@50 深) ⬜ 未开工 D8 后由「决策门」降级为「验收实测」;backlog:待需求方确认撤销交互后迭代
Gate 0d 高亮性能方案(方案 α 着色器) ✅ 已完成(eea2fd1 着色器高亮 + 485aa7d 性能优化 + f192f98 工具门控) 删 SegmentHighlight/FillFaceHighlight/giantSegmentIds/WHOLE_SEGMENT_MAX_SHARE;条件性 demand + 删 preserveDrawingBuffer + flat varying;高亮仅 fill/picker/segment 开启
Gate 0e 切片器环境 ✅ 完成 573505f;基线归属已更正(Snapmaker fork / stock 双路径)
导入健壮性 STL 导入 kdtree 崩溃(球面等重合轴点) ✅ 已修复(69a17ea) kiddo KdTree 重合轴点 >32 panic → 按 index ~1e-4 确定性扰动;回归测试 kdtree_coincident_axis_no_panic
Phase 1 删除式重构 1a/1b/1c 1a ✅ / 1b ✅ / 1c 由 0d 方案吸收 1a 7b74707 解粘滞;1b eea2fd1 删闸门/giantSegmentIds/WHOLE_SEGMENT_MAX_SHARE;1c 的集合语义已被着色器方案(hover/选中都走 uniform,非双 geometry 面集)天然化解,无需再做
智能分区 可配置算法 + 面板 + 持久化 ✅ 完成 b087c3c 特征感知分区 + golden harness、47004d9 IntelligentSegmentPanel + 参数持久化、0d5f162 文档。三算法:二面角 / 形态直径 SDF / 曲率 K-Means(可融合 SDF 特征)
├ 分区命名 双击重命名 ✅ 完成 39736dd
├ 分区合并 选中 ≥2 合并为一,可撤销 ✅ 完成(A10) f7d2f83 + SegmentsPanel Ctrl+点击多选 + 「合并」按钮;label-only 历史,增量 diff 撤销
├ 分区拆分 沿内部棱角拆分,可撤销 ✅ 完成 72455f3 + SegmentsPanel「拆分」按钮 + 阈值滑块;拆分量独立成区,最大片留原 label
├ 手动区跨自动重跑保留 preserve_manual ✅ 完成 364a2be;手动 label 永不回收 b7d64c4
├ §2.4 配色不同源修复 面板/3D 按 label 统一取色 ✅ 完成 788f7b9(调色板单一来源,按 label 索引)
└ segment 元数据单一构建点 后端一处构建 ✅ 完成 ec9af61
性能(智能分区) 点击「智能分区」主线程卡死 ✅ 已修复(06303ee) auto_segment_v2 改 async(Tauri tokio worker 线程)+ take() 出 Mutex + 空 face_colors(省多兆 IPC);REFUTE 证实 sync 命令跑主线程
├ 阶段进度条 + 转圈 抽象阶段,动画反馈 ✅ 完成(b1d523c) 后端 stage 改 canonical key;前端 segmentStages.ts 阶段计划 + resolveSegmentStage;CSS 旋转环 + indeterminate 条纹
├ 默认算法对齐 import 默认 curvatureKMeans + useSdf ✅ 完成(eac4a64) 面板/导入默认一致;平滑模型(装甲等)不再被 dihedral 归成整块
└ crease 阈值默认 45° 过分割修复(89 区 → ~7–10 区) ✅ 完成(82b7366) DEFAULT_ALGORITHM_PARAMS.curvatureKMeans.creaseThresholdDeg 20°→45°;滑块仍 1–90° 可调
Lasso 第 3 点在薄边被误判闭合吞点 ✅ 已修复(156f673 + 71e8d34) 根因 1:closing 检测读 stale lassoSnapRef(先算 snap 后判距);根因 2:闭合只判空间距离,细电缆空间近 ≠ 拓扑近 → 加顶点 1-ring 拓扑守卫 + closeThreshold 0.012→0.004
知识沉淀 论文/计划入项目资产 ✅ 完成(e1d374e) docs/08-智能分区-算法参考与论文.md(三算法论文引用 + 复杂度 + pipeline + 性能复盘);配对 docs/07 设计/REFUTE 文档
Phase 2 复验 A4 / A5 ⬜ 未开工 —
Phase 3 分区合并(D4)→ 已并入智能分区泳道 ✅ 完成 合并 f7d2f83 / 拆分 72455f3 / 命名 39736dd / 撤销接入 / 配色修复 788f7b9。⚠️ PRD FR-SEG-03 语义未同步(当前仍为「元数据双写」旧条目)
Phase 4 3MF 收口(A9) 🟡 阻塞 依赖 spool 复验 + 需求方确认导出交互后迭代 D6-a + 双基线 E2E
Phase 5 发版收口 ⬜ 未开工 —

当前等待需求方复验(多类,均需真实 Tauri runtime / 切片器,headless 不可覆盖):

  1. 智能分区效果 — 装甲人偶经 eac4a64(默认 curvatureKMeans)+ 82b7366(crease 45°)应拆成 ~7–10 个分区(头/躯干/臂/腿),不再是 89 区 / 1 整块。
  2. 阶段进度条 + 转圈(b1d523c)— 点「智能分区」应看到「阶段 X/Y · 名称」+ 旋转环,不再出现"卡死/无反馈"。
  3. Lasso 第 3 点(71e8d34)— 胸前细电缆上连续点 3 点:第 3 点应正常放置、不误报黄色"ready to close";只有点回起点顶点附近才变黄闭合。
  4. Sphere.stl 导入不再崩(69a17ea)— 安装最新 NSIS,打开 samples/_src/Sphere.stl 确认不闪退。
  5. Gate 0b 预设库复验 — 在切片器打开 examples/paint_color_sample.3mf:下拉框 Snapmaker U1 (0.4 nozzle)、喷嘴 0.4、工艺 0.20 Standard、spool 卡完整;首次弹 "Customized Preset" 确认框属正常。
  6. (不阻塞) D6-a 非静默预览 UI — 可下轮排。

1. 产品裁决记录(Phase -1,已完成 ✅)

2026-08-06 由用户裁决。这些是后续所有技术方案的前提,不得单方面推翻。

ID 裁决内容 结论 影响
D1 Fill 目标的权威源 click 命中权威 — hover 降级为纯视觉提示 删除 hover 参与 fill 决策的全部通路;P0-3 无需 TTL 安全阀。需改 PRD FR-PNT-03
D2 大分区(>80% 占比)Fill 语义 整段填充,不降级 删除两个 share 阈值及其残留 gate(见 §2.1)。需改 PRD FR-PNT-02。⚠️ 本项条件性依赖 Gate 0c,见 §2.5
D3 A9 验收基线切片软件 OrcaSlicer + SnapmakerOrca 双基线 ✅ Gate 0e 已确认二者均安装于开发机,双基线维持
D4 分区合并/拆分是否纳入 v0.1 纳入 经取证「拆分」已实现(§2.3),真实缺口仅「合并」
D5 手动区豁免口径 自动消解 D2 判定后阈值全删,「豁免」概念不复存在
D6 导出色彩域量化策略(§2.8) β 自动量化 — N 色聚到 ≤16 槽位 永远可导出。⚠️ 附带强制约束 D6-a(见下),因 β 原生缺陷是静默改色
D7 撤销栈归一(Gate 0b′) 统一到 Rust 后端 前端 pushUndo 全量快照栈下线;四类操作(涂色/手动分区/合并/拆分)全部在后端记历史
D8 撤销快照粒度 增量 diff 只存变更面索引 + 旧色;Gate 0c 由「决策门」降为「验收实测」

D6-a(附加约束,非新增需求,而是 β 失败模式的必要补丁):量化不得静默。导出前必须展示映射结果——原始色数、目标槽位数、每个槽位由哪些原色合并而来、以及一张合并前后对照预览。用户确认后才写文件。理由:工具的核心承诺是「所见即所得」,β 在切片器里改色而用户不知情会直接摧毁这个承诺;把改动摊开则 β 的代价从「欺骗」降为「知情妥协」。


2. 技术前提(已经 REFUTE 校验,含被推翻的原始判断)

2.1 三个 P0 由同一次「删除式重构」解决 —— 但删除清单需扩到五处

原判断(「删两个阈值 + fill 改 click 即可」)被 REFUTE 推翻。穷举「决定填哪些面 / 高亮哪些面」的全部代码路径后,实际需要删除五处,缺一则 Phase 2 复验必不通过:

# 位置 现状 为何必须删
1 Viewport.tsx:1475-1496 let nextHover = prevHover(:1477)+ manual 钉死条件(:1484) hoveredSegment 是历史粘滞值,光标移出模型 / 移到 giant 段 / 移到 auto 碎屑时都不跟随。高亮渲染读的正是它(:1786-1789)→ 用户看见 A 段高亮却填掉 B 段
2 usePaintTool.ts:106-107 const label = (hovered != null) ? hovered : segmentLabels[faceId] D1 要求 click 权威,hover 不得参与决策
3 usePaintTool.ts:125-130 !!seg && (isManual ? share<=0.95 : (realSegs.length>1 && share<=0.8)) 仅删两个 share 阈值不够:残留的 !!seg 与 realSegs.length > 1 会让「单一真实分区」的模型 fill 仍静默降级为 fill_paint 局部填充(:146-150),而 hover 照样整段高亮 → 面集又不等。改为 !opts?.wholeRegion && label !== undefined
4 Viewport.tsx:1028-1040 giantSegmentIds + autoSegs.length <= 1 早退 D2 取消降级后静音无依据。⚠️ 但它是目前唯一挡住 5400 万次 push 的东西,删除时机受 Gate 0d 约束,见 §2.2
5 Viewport.tsx:1236-1237 hoveredSegment: snap ?? lastValidHoveredSegmentRef.current store 空时主动复活历史值(iter29 v5 引入)

后端口径已验证一致,无需改动:fillSegment 传 segmentId = overrideSegmentId ?? segmentLabels[faceId](usePaintTool.ts:32-69)→ fill_segment_paint(commands/paint.rs:116-132)→ fill_segment 按 filter(|&label| label == segment_id) 收全段(fill.rs:14-26)。前端 facesBySeg(Viewport.tsx:466-478)同样纯按 segmentLabels 分组。两端唯一真值都是 segment_labels。

这满足 backlog M5 的「必须换角度」要求:过去 7 轮都在修补 hover 传递链路,本轮是移除 hover 的决策权。删除比统一更彻底 —— 没有第二条路径,就不会再次分叉。

B1(HUD 与屏幕高亮不同源)仍须先修:即便一致性成为结构性保证,回归验证仍需可信读数。Gate 0a 保留。

2.2 D2 的代价:高亮渲染性能 —— 原方案 β 已被推翻

SegmentHighlight(Viewport.tsx:591-610)在 useMemo 中同步重建 geometry:150 万面 → 5400 万次 Array.push + 216MB Float32Array 分配 + 全量 computeVertexNormals(),全在主线程。这才是 giantSegmentIds 静音存在的真实原因。

REFUTE 修正两点:

  • ✅ SegmentOutline(:528-574)成本更低不是更高:它遍历面找「邻面 label 不同」的边界边,单段覆盖全模型时不存在边界边,直接返回 null。§2.2 只算 Highlight 的数量级是对的。
  • ❌ 方案 β(indexed BufferGeometry)不可行:主 geometry 是非索引展开式(Viewport.tsx:13-23 构建注释、:1064-1068 明确保持 geometry.index === null),每三角形独占 3 顶点。在非索引 buffer 上建 index 等价于 [0,1,2,...],省不下任何东西。要真省必须先做顶点去重映射,而这会破坏 faceIndex === 后端 face 序号 这个前提,使 indirect:true 的 BVH raycast 优化(:942-963)失效。

修订后的候选方案(Gate 0d 决策):

方案 做法 收益 风险
α(首选) 着色器高亮,零 geometry 分配:给主 mesh 加一个 per-vertex 的 aSegLabel float attribute(同三角形三顶点同值,分区变化时才更新,O(F) 一次);hover 变化只改 uniform uHighlightLabel,shader 内比较着色 hover 切换成本 O(F)→O(1),无新 buffer、无 GC、不碰 raycast 前提 需 onBeforeCompile 注入或自定义 ShaderMaterial,与主题色/vertexColors 系统耦合
γ 大分区高亮异步构建 + 过渡态 不阻塞主线程 高亮延迟出现,A4 取证变复杂
δ 保留静音,但阈值提高到接近 1.0 改动最小 与 D2 语义冲突(用户仍会遇到「点了没高亮」)
β indexed BufferGeometry 12× 已否决:非索引几何 + 破坏 indirect raycast

2.3 D4 取证结论:「拆分」已实现,缺口仅「合并」

H1 证实 — finalize_manual_region(src-tauri/src/segment/manual.rs:578-582)无条件覆写 mesh.segment_labels[f] = label,源码注释明写 "faces may belong to an earlier region",并以 ManualRegionSnapshot 记录原 label 供撤销。paint_segment_face(commands/segment.rs:122-123)同理。

→ 用户用套索/分区笔在任意已有分区内圈一块,该块 label 被覆写,原分区自动少掉这部分。功能上就是拆分。

H2 证实 — src-tauri/src/commands/ 下无任何 merge / split / relabel 命令,合并需从零实现。有利条件:SegmentsPanel.tsx 已存在,store 已有 selectedSegment 单选态(:13/:33),Viewport 已消费其高亮(:1797-1801)。

REFUTE 澄清(原判断为误解):rebuild_segments(mesh/model.rs:332-367)只用 segment_labels 重建 segments,全程不触碰 face_colors → 合并不会丢失用户涂色。且后端 Segment.color(:351-360)是死字段,全前端无一处读取。原文档「保留目标分区的 label 与颜色」的表述应删除。

2.4 新发现缺陷:面板配色与 3D 配色不同源(docs/04 未收录)

侧 取色方式 位置
3D 视图 按 label id:ci = segId % SEGMENT_COLORS_RGB.length useMesh.ts:96
面板色点 按 数组下标:i % SEGMENT_COLORS.length SegmentsPanel.tsx:43(:29 segments.map((seg, i))

后端 segments.sort_by_key(|s| s.id)(model.rs:365)。auto 段 id 连续为 0..n-1 时 i == id 属巧合一致;一旦出现手动区(id ≥ 100000,当前已可复现)或 Phase 3 合并造成 id 空洞,i ≠ id → 面板色点与 3D 分区颜色错位。

处置:统一为 segId % len,纳入 Phase 3,并补录进 docs/04。

2.5 撤销架构:存在两套互不同步的栈(R4 级架构决策,必须前置)

栈 记录内容 入口 栈深
后端 ManualRegionSnapshot per-face (label, color) LIFO finalize_manual_region(manual.rs:539-596)/ undo_last_manual_region(:604-612)/ 命令 manual_region_undo(commands/segment.rs:245-262) 无限
前端 全量 face_colors 快照 pushUndo(appStore.ts:341-345)/ undoPaint → restoreFaceColors(useTauriCommand.ts:206-227)→ restore_face_colors(commands/segment.rs:272-312) slice(-19) = 19 层

两者不互斥但不同步:lasso 落笔时 pointerdown 已 pushUndo 一份前端快照,finalize 又 push 一份后端 snapshot —— 同一次操作在两个栈里各留一条记录,谁先弹出取决于用户按了哪个入口,栈深还不一致。

原文档把「合并的撤销走哪套」写成「二选一(依 0c 结论)」并留在 Phase 3,违反 R4:这是前后端历史语义的架构决策,不是性能实测的下游产物。0c 只能回答「全量快照能否撑 150 万面」,回答不了「两套栈如何归一」。→ 前移为 Gate 0b'。

2.6 D2 条件性依赖 Gate 0c(原文档遗漏的依赖)

D2「整段填充不降级」是最大 undo 快照的唯一来源:一次填充可改动全模型面数。pushUndo 每次存全量 face_colors(appStore.ts:341-345),150 万面 ≈ 18MB/次 × 19 层 ≈ 340MB 常驻;撤销时 restoreFaceColors(useTauriCommand.ts:184-200)用 Array.from() 把整个数组转 JSON 数字过 IPC → 单次承载 450 万个十进制数。

D2 的论证前提是「有撤销兜底」。若 0c 实测不通过,该前提当场崩塌。因此 0c 必须写明失败分支,见 §3 Gate 0c。

2.7 Phase 1 与 Phase 3 的渲染契约冲突(原文档遗漏)

selectedSegment(appStore.ts:31,单值 number | null)与 hoveredSegment(:34)共用同一个 SegmentHighlight 组件,靠视图模式分流:paint 视图渲染 hovered(Viewport.tsx:1786-1789),segment 视图渲染 selected 并在 hovered ≠ selected 时额外画一条 hovered 的 Outline(:1795-1805)。

Phase 1 的全部工作是把决定权收敛到一个面集,而 Phase 3 要把 selectedSegment 扩成多选集合 —— segment 视图里会立刻重新出现「选中集 N 段」与「hover 集 1 段」两个面集,且 :1803 的 hoveredSegment !== selectedSegment 判等在集合语义下恒真(number !== Set)。

若放任到 Phase 3 再处理,等于亲手重建刚拆掉的「两个面集」结构。→ 渲染契约改造前移为 Phase 1c,Phase 3 只填数据。

2.8 色彩域基数差:本工具 RGBA 任意 vs 切片器 ≤16 槽位(Gate 0b 新发现,R4 级)

这是 A9 的真正拦路石,此前所有文档均未记录。

ColorYourModel OrcaSlicer / SnapmakerOrca
色彩表达 逐面任意 RGBA 逐面挤出机槽位序号 1..16
颜色来源 面上直接存 由 filament_colour 预设间接解析
基数上限 无(用户想涂几种涂几种) 16

导出不是「格式转换」,而是有损映射:必须先把 N 种面色量化到 ≤16 个槽位,再把这 16 个颜色写进 Metadata/project_settings.config 的 filament_colour。

三条候选策略,代价与语义差别很大:

策略 做法 优点 代价
α 精确 + 硬上限 统计去重色,>16 直接报错拒绝导出 零失真,语义诚实 用户可能已涂 20 色,被卡住且无补救
β 自动量化 k-means / median-cut 聚到 16 色 永远能导出 静默改色,与「所见即所得」冲突
γ 调色板前置 涂色阶段就限定调色板 ≤16 槽 全程 WYSIWYG,导出无损 需改涂色 UI,影响 Phase 1/2

倾向 α(v0.1 范围最小、语义最诚实,且失败信息可直接指导用户),但 R4 要求用户确认后再动代码。


3. Phase 执行计划

Phase -1 — 裁决门 ✅ 已完成

  • D1–D5 全部裁决(§1)
  • README 四处失真校正(commit 9bbc9ff)
  • 路线图 PLAN 版落盘(commit a5c4e51)→ REFUTE → 本 REVISE-1

Phase 0 — Gate 阵列(六项,全部产出二值结论 + 失败分支)

前置修复(必须最先做):P1-8 render 期副作用(2 处)。✅ 已完成(550289e)

⚠️ 本条原立论已被证伪,修正如下:原文写「会让 0c/0d 实测不可复现」。查 React 官方文档后确认,StrictMode 在 DEV 下对 useMemo 工厂函数是确定性二次调用——测量值是可复现的,但被稳定放大约 2×。危害性质从「不可复现」变为「系统性偏高」,是准确性风险而非复现性风险。两者应对手段不同:前者靠稳定化,后者靠剥离观测干扰。

  • 移除 render 期 ref 写入 2 处(useMesh.ts baseGeometry / paintColorArray),后者改用 useLayoutEffect 在 commit 后发布
  • 新增测量逃生舱:VITE_NO_STRICT=1 时关闭 StrictMode,令 0c/0d 的内存与耗时反映生产行为
  • DoD:npx tsc --noEmit exit 0;vite build 通过;cargo test --lib 13 passed

Gate 0a — 校准 HUD 仪器(B1)✅ 已完成(8ea2217)

三重分叉:HUD highlighted 取 facesBySeg.get(targetLbl).length(Viewport.tsx:1183,含 click 回退与复活值),屏幕高亮取 store hoveredSegment 且过 giantSegmentIds(:1786-1788)→ iter25–30 六条「决定性证据」不可采信。

采用解法:引入唯一真源 memo renderedHighlightLabel,覆盖 segmentView / hover / 巨块过滤三条分支;overlay JSX 与 HUD 同读该 memo,使「屏幕画的」与「HUD 报的」在结构上无法分叉。

  • overlay JSX(paint view :1786、segment view :1797/:1803)改读 renderedHighlightLabel
  • HUD 增打 renderedHover=(屏幕真正高亮的 label,null 显式打出)与 fillTarget=
  • HUD 仅在 fillRouting === "segment" 时断言 MISMATCH——local 笔刷与 whole-region 洪泛设计上就与高亮不等,此前对它们报 MISMATCH 属误报
  • enqueuePaint 快照 renderedHover,与 hoveredSegment 分开存,保留「所见高亮 vs 实际填充目标」的差值可见性
  • DoD:npx tsc --noEmit exit 0;vite build 通过

Gate 0b — 3MF 格式前置验证(从 Phase 4 前移)🔴 结论:A9 按现设计不可达

调查已完成,结论比原预期严重。证据分两层:规范层(3MF Consortium 官方规范)与实现层(本机 OrcaSlicer 源码 C:\workDir\programing\OrcaHunSe\OrcaSlicer,逐条 grep 亲验)。

B1|规范层缺陷(4 条,均已确认)

位置 问题 正确做法
threemf.rs 默认 xmlns 写成 http://schemas.microsoft.com/3dmanufacturing/2013/01。该串是 OPC 关系类型,不是 XML 命名空间 http://schemas.microsoft.com/3dmanufacturing/core/2015/02(OrcaSlicer 3mf.cpp:2542 亦写此串)
:84 m:basematerials、:95 m:base 二者属核心规范默认命名空间,不属 Materials 扩展 去掉 m: 前缀
:112 ("m:basematerials","1") 规范中无此属性 pid="1" pindex="0"
:149-150 pid="1" p1="..." 因上条未注册成功 → 悬空引用 随上条修复而解

B2|实现层:basematerials 在 OrcaSlicer 中零读者

grep -ri "basematerial|displaycolor" OrcaSlicer/src/libslic3r/Format/   →  No matches found

覆盖 3mf.cpp / bbs_3mf.cpp / AMF / STL / OBJ / STEP 全部导入导出器。即使把 B1 四条全部改对,OrcaSlicer 仍然一点颜色都读不到——规范正确性与目标切片器可用性在此处不是同一件事。

B3|语义层(决定性):OrcaSlicer 的颜色模型是「挤出机槽位」,不是 RGB

两条私有通道,均为逐三角面属性,不是材质引用:

切片器族 属性名 命名空间 源码位置
PrusaSlicer 兼容 slic3rpe:mmu_segmentation xmlns:slic3rpe="http://schemas.slic3r.org/3mf/2017/06" 3mf.cpp:114
Bambu / Orca paint_color 无前缀(BBS 自有格式) bbs_3mf.cpp:280

编码规则(Model.cpp:3478 get_triangle_as_string + TriangleSelector.cpp:1648 serialize,源码注释明写 "changing it may break backwards compatibility"):十六进制 nibble 串,未细分叶三角面状态 n 编码为 [0,0] + (n<3 时 2 bit / n>=3 时 [1,1] + base-15 的 4-bit 块),nibble 内 bit 序小端,字符串前插。实测推导:

槽位 串 槽位 串
挤出机 1 4 挤出机 4 1C
挤出机 2 8 挤出机 5 2C
挤出机 3 0C 挤出机 16 DC

EnforcerBlockerType(TriangleSelector.hpp:13)枚举为 Extruder1..Extruder16——存的是槽位序号,不是颜色。真实颜色由 filament_colour 决定,落在 Metadata/project_settings.config(Orca/BBS,bbs_3mf.cpp:580)或 Metadata/Slic3r_PE.config(Prusa 路线,3mf.cpp:73)。

⛔ 由此产生一条 R4 级产品约束(阻塞,需用户裁决)

ColorYourModel 允许用户涂任意多种 RGBA;OrcaSlicer 只能表达 ≤16 个槽位。这不是 bug,是两个色彩域的基数差。要让颜色真正显示,必须补一个量化步骤(N 种面色 → ≤16 槽位)+ 写调色板进 config。量化策略属产品决策,见 §2.8。

  • 规范逐条比对(B1 四条)
  • 目标切片器实现层亲验(B2、B3)
  • §2.8 已裁决(D6 = β 自动量化) → threemf.rs 解冻,改造清单如下

改造清单 v1(7 项,D6 驱动)— 已被 REFUTE-3 部分推翻,见下

# 项 说明
1 B1 四处规范修正 默认命名空间改 .../core/2015/02;其余三条按 B1 表
2 新增量化模块 面色去重 → k-means/median-cut 压到 ≤16 → 产出 color → slot 映射表
3 paint_color 编码器 按 TriangleSelector::serialize 的 nibble 规则;黄金用例:槽 1=4、2=8、3=0C、4=1C、5=2C、16=DC
4 写 Metadata/project_settings.config 输出 filament_colour 数组,长度 = 槽位数,顺序与映射表一致
5 D6-a 非静默 导出前弹预览:原色数 N → 槽位数 M,列出被合并的色对;N>16 必须用户确认
6 export::threemf 单元测试 当前零覆盖:命名空间、pid/pindex 绑定、编码表黄金用例、量化确定性
7 双基线 E2E 最小 STL 导出 → OrcaSlicer 与 SnapmakerOrca 都要开 验色(D3 双基线)

REFUTE-3 裁决表(2026-08-07,双路独立证伪 + 我方亲验仲裁)

两个独立 agent 分别对上表证伪;结论冲突处由我在两份源码上亲验仲裁(投票制)。

议题 路 1 路 2 亲验 裁决
basematerials/displaycolor 零读者 成立 成立 stock Format/ grep 亦 0 命中 ✅ 收敛,成立
逐面颜色唯一机制 = paint_color 成立 成立 fork bbs_3mf.cpp:4830 / stock :3854 注释明写忽略 pid/p1/p2/p3 ✅ 成立
6 个黄金编码值 亲验正确,无 off-by-one 未查 我按 stock TriangleSelector.cpp:1696+ 逐位重推,6 值全对 ✅ 采信,锁进单测
槽位上限 255,量化模块多余 fork 255 / 上游 16 / 产品 64 stock ExtruderMax = Extruder16(int8_t);fork = 255(int16_t);两者 MAXIMUM_EXTRUDER_NUMBER = 64 ⚠️ 路 1 被推翻。取交集 = 16,D6 前提成立
未涂色面不写 paint_color 成立 成立 bbs_3mf.cpp:4829 if(!empty());TriangleSelector.cpp:1719 跳过 NONE ✅ 成立
hex 必须大写 M2 X3 stock Model.cpp:3580 只接受 'A'..'F',否则 assert(false),Release 静默解成 0 ✅ 收敛,成立
<triangle> 必须索引升序 M3 X11 Model.cpp lower_bound + 递增 assert ✅ 成立
project_settings.config = JSON M4 同 bbs_3mf.cpp:7471 save_to_json(..., "project_settings", "project", ver) ✅ 成立,含 version/name/from 头部键
该 config 读取是否有门禁 无门禁 必须写 bbs_3mf.cpp:1839 是裸 if,不受 dont_load_config 也不受 m_is_bbl_3mf ✅ 无门禁,写了就读
Application 须伪装成 BambuStudio-/Snapmaker_Orca- 未提 X2 声称必须 paint_color 应用点不受 m_is_bbl_3mf 门禁,非 BBL 分支(:4958起)同样应用 ❌ 路 2 的 X2 被推翻,无需伪装
E2E 可 CLI 自动化 M5:--export-3mf/--slice 可用 未查 未验 🟡 待验,不计入本轮 DoD

由裁决产生的 5 条硬约束(写进实现,逐条对应单测)

ID 约束 违反后果
C1 槽位上限 16(stock Orca 的 Extruder16)。Snapmaker Orca 虽支持 255,但取交集才能双基线通过 第 17 色起在 stock 上静默变未涂色
C2 hex 串必须大写 A-F Release 下静默错色,切片器不报错
C3 未涂色面不写 paint_color 属性;<triangle> 严格按面索引升序输出 前者浪费槽位,后者 lower_bound 取到错条目
C4 必须写 Metadata/project_settings.config,否则 max_supported_filament_id 返回 INT_MAX,颜色回落到用户当前预设 颜色与设计完全无关
C5 量化必须确定性 → 用 median-cut,禁用 k-means(随机初始化与「同输入同输出」的单测目标直接冲突) 单测不可重复,用户两次导出配色不同

改造清单 v2(REVISE 后,按可交付性重排)

# 项 轮次 说明
1 paint_color nibble 编码器 本轮 纯函数 + 6 黄金值单测。C2 大写、C3 升序
2 median-cut 量化模块 本轮 确定性(C5);≤16 时恒等映射不改色;上限常量化(C1)
3 project_settings.config writer 本轮 JSON + filament_colour(C4)
4 3dmodel.model 改写 本轮 删 basematerials/displaycolor(零读者);三角面按需挂 paint_color;B1 命名空间顺带修正(对 Orca 无影响,但对通用 3MF 消费者是正确性)
5 单元测试 本轮 覆盖 C1–C5
6 D6-a 非静默预览 UI 下轮 后端出 quantize_preview 命令(纯查询、不写盘),前端弹窗消费。与 1–5 无耦合,两路证伪均确认可拆
7 双基线 E2E 下轮 本机双份源码均在,可从 CLI 起步
REFUTE-4(2026-08-07,需求方二轮反馈「箭头指的地方」驱动)

需求方二轮实测:project_settings.config 扩到 41 字段(b10bd20)后,spool 卡仍 Bed type 空 / Diameter 显示 <<mm / spool 名截断。需求方原话「缺少 resources」精准——缺的是 embedded preset files(ORCA 的 resources)。派独立 Explore subagent 在双基线(Snapmaker fork bbs_3mf.cpp + 上游 stock)复核 _extract_project_embedded_presets_from_archive,推翻 4 点:

# 被推翻的原判断 修正 源码证据
① project_settings 顶层 41 字段足以填 spool 卡 spool 卡读的是 embedded preset files,每个 preset 必须带 printer_settings_id / print_settings_id / filament_settings_id 作 name,缺则提取器 return; 静默丢 bbs_3mf.cpp:2557-2620(fork)/ 上游同偏移
② 6 个 filament 预设可同名 提取器以 filament_settings_id[0] 为 preset 名,同名被去重覆盖→六色塌一色,须唯一名 Generic PLA @slot1..6 bbs_3mf.cpp:2598
③ 写 temperature / bed_temperature 填温床 二者不在 PrintConfigDef,被列入 obsolete ignore 集(PrintConfig.cpp:7268)必被吞;改 nozzle_temperature / hot_plate_temp PrintConfig.cpp:759/729
④ default_bed_type 是 coEnum 实为 coString(PrintConfig.cpp:902),直接写 "Cool Plate" 字符串即正确 PrintConfig.cpp:902

前缀常量逐字节相同(Metadata/machine_settings_ / process_settings_ / filament_settings_ / print_setting_),读取端纯 istarts_with 前缀,.config 不需 [Content_Types].xml 注册。无 m_is_bbl_3mf 门禁。

REVISE:export/project_config.rs 拆为 4 类 builder(build_project_settings_config 仅留 name/from/version + *_settings_id 引用;build_machine/process/filament_profile_config 承载 machine/process/filament 元数据)+ 常量 + 10 单测;threemf.rs 改为写 project + 1 machine + 1 process + N filament 共 6 + N 份 config。cargo test --lib 26 passed;examples/paint_color_sample.3mf 解包验证 12 entry 全绿(6 份 filament 预设名唯一、含正确 settings_id、无 obsolete 字段)。

  • 失败分支已触发:格式需重写,单列子任务,不阻塞 Phase 1/2
REFUTE-6/7(2026-08-07,预设库实现前证伪,2e4e070 落地)

需求方需求升级:导出时可选厂商/机型/工艺/耗材,3mf 打开即识别为 Snapmaker U1 + 0.4mm 喷嘴(不依赖用户已装 Snapmaker vendor profile)。实现前派独立 Explore subagent 审计 SnapMakerOrcaMain 源码 + 双基线交叉验证,推翻两个关键假设:

# 被推翻的原判断 修正 源码证据
⑥ 写 <metadata key="printer_model_id">(3dmodel.model)即可让 Orca 识别 U1 无效。printer_model_id 是 plate 级切片元数据(bbs_3mf.cpp:4354 读 / 7882 写),只进 PlateData 供 SelectMachine 对话框比对,与预设选择无关。下拉框显示名来自被选中的预设:system 预设显示被改写为 printer_model(括号丢失,PresetComboBoxes.cpp:1352-1356),只有 project-embedded 预设保留全名(:1373-1380)。因此必须嵌入 machine_settings_1.config,其 printer_settings_id 与 project 文件逐字相等(Preset.cpp:2046 精确 find,不等则合成 Xxx(yourfile.3mf)) bbs_3mf.cpp:4354/7882、PresetComboBoxes.cpp:1352-1380、Preset.cpp:2046
⑦ 嵌入 machine 预设可写 diff(照抄 Orca 导出) 必须完整配置。inherits 落空(用户未装 vendor)时 Preset.cpp:1541-1545 回退默认打印机值(警告不丢弃)。Orca 自己导出的是 get_preset_differed_for_save diff,照抄会踩坑;萃取器白名单曾砍掉全部 gcode 宏(machine_start_gcode 等 6 个)→ 已补齐重萃 Preset.cpp:1541-1545/1371-1420
⑧ 同名 filament 预设会塌缩颜色 → 须 @slotN 唯一名 有害。颜色从 project_config.filament_colour[extruder_idx] 读(PresetComboBoxes.cpp:1273),与预设名无关;@slotN 匹配不到已安装预设 → 继承链断裂退化默认值。Orca 自己导出"stock filament + custom colours"时写零 filament 预设文件(bbs_3mf.cpp:7489 只写 is_project_embedded 标记的) PresetComboBoxes.cpp:1273、Plater.cpp:21932(force_filament_colors_update 被 #if 0)、bbs_3mf.cpp:7489
⑨ inherits_group 只要写 N 个名字 是 N+2 数组:[0] process 父、[1..=N] filament 父、[N+1] printer 父(PresetBundle.cpp:2861/2876/2943)。多槽时每槽配置由 set_at(other_opt,0,i) 按索引散射 → 混合耗材必须写 N 长数组 PresetBundle.cpp:2779/2925/2861/2876/2943

IMPL(2e4e070):tools/extract_orca_presets.py(vendor 索引白名单 → 265KB snapmaker_u1.json:4 变体/30 工艺/113 耗材);presets.rs(OnceLock + ExportSelection::resolve 悬空名报错);project_config.rs 重写(扁平 config + inherits_group + selected/generic 双路径);threemf.rs 3 参签名(selected 写 6 entry 无 filament 文件);前端 ExportDialog(厂商→机型→喷嘴→工艺→每槽耗材→目标切片器)+ zustand persist lastExportSelection。cargo test --lib 66 passed(含 selected-export zip 往返断言);verify_3mf.py 解包:printer_settings_id 相等、machine 78 keys 含 start gcode、inherits_group 8 元素、无 @slotN。

  • 待需求方复验:切片器打开新 examples/paint_color_sample.3mf → 下拉框 Snapmaker U1 (0.4 nozzle)、喷嘴 0.4、spool 卡完整。首次弹 "Customized Preset" 确认框属正常(validate_presets 对非 system 预设的行为,勾选"不再提醒")

Gate 0b′ — 撤销架构归一决策(R4 级,见 §2.5)✅ 已裁决(D7 + D8)

书面契约(用户已确认,2026-08-06)

条款 内容
唯一历史真源 Rust 后端。face_colors 的真源本就在后端,历史与真源同处一地是唯一能杜绝前后端状态漂移的结构
纳入历史的操作 涂色(笔刷 / Fill)、手动分区(lasso / 分区笔)、分区合并、分区拆分 —— 四类共用一条时间线
前端职责 只发命令、只读结果。appStore.pushUndo 全量快照栈 下线,连同其 19 层上限一并移除
记录粒度 增量 diff:{ face_indices: Vec<u32>, prev_colors: Vec<[u8;4]> }。不存全量 face_colors
应用规则 diff 必须严格逆序应用;redo 栈在任何新操作写入时清空
栈深与内存 栈深上限暂定 50(远大于原 19,因单条 diff 体积已降一到两个数量级);进程增量内存上限 256MB,由 Gate 0c 实测校准,超限则按 FIFO 丢弃最旧条目
IPC 代价 每次 undo/redo 过一次 IPC。可接受:撤销是低频人工操作,且返回的是 diff 而非 600 万个数的全量数组,载荷远小于现状

REVISE-2 — 实现方案经双路独立证伪后的修正(2026-08-06)

实现草案(begin/end_stroke 协议 + 统一 FaceDelta + 一律增量回传)被两个独立 agent 分别攻破,两者独立收敛到同一处根因,故按投票制采纳。修正如下:

# 被推翻的草案 修正 根因
V1 begin_stroke / end_stroke 两个命令划定笔画边界 删除该协议,改为涂色命令多带一个 stroke_id: u64 参数;后端 stroke_id 与上次相同则并入同一条 entry,否则新建 有状态协议有 5 条不闭合路径(pointerup 与异步涂色队列竞态、pointerleave/pointercancel 不对称、alt-tab、组件卸载、笔画中按 Ctrl+Z)。无状态参数把这些全部降级为「最坏多一条 entry」
V2 undo 结果一律走 updateFaceColors 增量写回 仅纯颜色 delta 走增量;凡 label 有变,仍走 updateSegmentLabels(换新 meshData 引用) segmentColorArray 的 useMemo 依赖是整个 meshData 对象引用(useMesh.ts:138),而 applyFaceColors 是原地改不换引用 → 原地 patch labels 会导致撤销分区操作在分区视图里完全不可见
V3 delta 面数不设上限,一律增量 面数 > 50 000 时改回传全量并整体重建 updateFaceColors 只发一个连续 addUpdateRange(useMesh.ts:243-247)。delta 若同时含首末面,span = 整个 buffer(150 万面 ≈ 54MB 上传),比全量重建更差
V4 patch 用 colors: Vec<[u8;4]> 改扁平 Vec<u8> Tauri 默认 JSON 序列化;嵌套数组比扁平多一层结构开销,现有 restore_face_colors 用的就是扁平(segment.rs:274),不能倒退
V5 单一 FaceDelta { face, prev_color, prev_label } 拆成 Vec<(u32,[u8;4])> 与稀疏 Vec<(u32,u32)> 两条 涂色类操作 label 恒不变,prev_label 白占 33% 预算
V6 「四类操作」按字面实现四个 OpKind 现阶段三个:Paint / SegmentPaint(paint_segment_face)/ ManualRegion。「拆分」无独立命令(是套索覆写 label 的副产品,§2.3),「合并」属 Phase 3 未实现,届时并入同一时间线 契约的「四类」是前瞻表述;lib.rs:22-40 全表无 merge/split 命令
V7 栈深 50 是主约束 字节预算才是主约束,50 降为次要上限;契约改写为「小笔画 ≥50 层 / 全模型级操作 ≥14 层」,并把 redo 栈也计入预算 150 万面全模型操作单条 entry ≈ 12MB,256MB 只够约 14 层。原写法让 50 成为空头支票
V8 auto_segment 后历史照常 auto_segment* 必须 history.clear() 现有代码已有此语义(segment.rs:56、:333,注释「rewrites all labels; drop stale history」),退役 manual_region_history 时不能把它一起丢掉
V9 Ctrl+Z 三级阶梯合并成单一 invoke("undo") 第一级留在前端:套索未闭合时 Ctrl+Z 仍是「撤销上一个点」(lassoPointsRef 纯前端态,后端不可见);后两级才合并 合并会静默吃掉一整级交互语义
V10 canUndo / canRedo 需查询 搭车返回,禁止轮询 涂色是 per-pointermove 的高频 IPC,任何额外往返都会放大

证伪中被确认成立、不需改的部分(同样重要):

  • Segment 完全可从 segment_labels 推导 —— name 是 format!("Region {}")、color 是 label 的哈希(model.rs:340-357),全仓无重命名/改色命令。故 label 的就地 swap + rebuild_segments() 能完整恢复分区的新建与消失。⚠️ 前提:一旦将来加「重命名分区」,Segment 即变为非派生态,本方案当场失效,必须同步改造。

  • 颜色链无过期覆盖风险 —— paintColorArray 的 useMemo 依赖 [meshData?.faceColors, meshData?.faceCount] 两个引用均不变 → 永不重算;paintColorRef.current 就是该 memo 的返回值本身,patch 缓存等同 patch memo。

  • 自反演(swap)+ 笔画内去重(保留首次 prev_color)组合正确,正反用皆成立。

  • 复用 AppState 那把唯一的 Mutex 无死锁(无嵌套加锁);load_model 整体替换 model,历史天然为空。

  • DoD:契约成文并经用户确认(R4 满足)

  • REVISE-2:实现方案经双路独立证伪并修正

  • 实现 S1(后端):mesh/history.rs + 8 个写命令接 stroke_id + undo/redo 命令 + 单测(5c85e80)

  • 实现 S2(前端):切到后端 undo,删除 pushUndo 等 8 项 + PaintSnapshot + 退役 restore_face_colors / manual_region_undo(7522659 + 7eaef39)

  • 实现 S3:Gate 0c 实测校准(backlog:待需求方确认撤销交互后迭代)

Gate 0c — A6 大规模撤销实测(性质已变更)

D8 裁决后,0c 由「决策门」降为「验收实测」。原设计让 0c 去回答「全量快照撑不撑得住 150 万面」;D8 已直接选定增量 diff,该问题作废。0c 现在的职责是验证 0b′ 契约的实现真的达标,并把契约里暂定的栈深 50 / 内存 256MB 校准成实测值。

  • 前置:0b′ 契约实现落地(后端增量 diff + 前端 pushUndo 下线)— 5c85e80/7eaef39/7522659,命名/合并/拆分均已接入时间线且可撤销
  • 150 万面模型 + Fill 大分区,VITE_NO_STRICT=1 下实测 10 次撤销 + 10 次重做的单次耗时与进程内存峰值
  • DoD:单次 undo/redo ≤ 200ms;50 层栈满时进程增量内存 ≤ 256MB;输出实测数字
  • 失败分支:调低栈深上限(50 → 实测可承受值)并在 UI 明示;仍不可行才 回退重裁 D2

Gate 0d — 高亮性能方案落地(升级为 Phase 1 前置必做,见 §2.2)✅ 已完成(方案 α)

  • 确认 β(indexed BufferGeometry) 不可行:主 geometry 非索引展开式,建 index 等价于 [0,1,2,...] 省不下任何东西,且会破坏 faceIndex === 后端 face 序号 前提(indirect raycast 失效);γ/δ 同理被 α 取代
  • 方案 α 落地(eea2fd1):主材质 onBeforeCompile 注入 uHighlightLabel/uHighlightColor + per-vertex aSegLabel(non-indexed 几何,面 f → 顶点 3f/3f+1/3f+2 同值);<color_fragment> 后 0.45 mix;hover 切换 O(1)。删 SegmentHighlight/FillFaceHighlight/giantSegmentIds/WHOLE_SEGMENT_MAX_SHARE/emptySet
  • 性能优化(485aa7d,经 /adversarial-development-loop REFUTE 证伪 P1 全局 demand / P2 gl_PrimitiveID / P3 Uint32):条件性 demand(IdleFrameloop,静止 1.5s 切 demand,任意交互唤醒);删 preserveDrawingBuffer:true;aSegLabel varying 加 flat
  • 工具门控(f192f98):高亮仅 fill|picker|segment 工具计算;其余工具 renderedHighlightLabel 返 null → uniform -1 短路,消除 brush 拖涂 60fps 重渲染风暴
  • DoD:tsc --noEmit exit 0;vite build 631 模块通过;cargo test --lib 67 passed。视觉一致性 + 卡顿消失待需求方装包复验(见 §0 待复验清单)
  • 失败分支(已规避):α 与主题色/vertexColors 耦合风险经 onBeforeCompile chunk 注入化解;若未来自定义 ShaderMaterial 冲突再回退 δ

Gate 0e — 切片器环境可获得性(新增,见 D3)✅ 通过

开发机实测已安装(C:\Program Files\):

基线 可执行文件 状态
OrcaSlicer OrcaSlicer\orca-slicer.exe ✅ 可用
SnapmakerOrca Snapmaker_Orca\snapmaker-orca.exe ✅ 可用
(旁证)Bambu Studio / Prusa3D 同目录已安装 可作交叉验证

⚠️ 基线更正(2026-08-07,REFUTE-3 发现,原记载完全反了)

原文写「OrcaHunSe\OrcaSlicer 是上游 OrcaSlicer 源码;SnapmakerOrca 本机无源码」。两句都错:

路径 实际身份 证据
C:\workDir\programing\OrcaHunSe\OrcaSlicer Snapmaker Orca 2.3.3(fork),分支 feature_bugfix_0603 version.inc:4 SLIC3R_APP_NAME "Snapmaker Orca";git remote = Snapmaker/OrcaSlicer.git
C:\workDir\programing\orcaslicer 上游 stock OrcaSlicer 2.5.0-dev,分支 main version.inc SLIC3R_APP_NAME "OrcaSlicer";git remote = OrcaSlicer/orcaslicer.git
C:\workDir\programing\SnapmakerOrca\OrcaSlicer 空壳,无 src/libslic3r ls 确认

后果:IMPL-1 记录的 Gate 0b 全部「实现层证据」取自 Snapmaker fork,却被标注为上游。已在 stock 上逐条复验(见 §Gate 0b REFUTE-3 裁决表),结论大部分仍成立,但槽位上限一条被推翻。双基线源码此后均可亲验,无需黑盒。

  • DoD:二值结论 = 可获得,D3 双基线维持不变,无需降级


Phase 1 — 删除式重构(解决 P0-1 / P0-2 / P0-3)

Gate 0a(仪器)、0d(性能方案)→ 两者均已完成。本 Phase 实际落地与 §2.1 字面方案有偏差(见下),但 D1/D2 语义目标已达成(hover 降级为纯视觉提示、整段填充不降级)。本 Phase 需单独 REFUTE 已不阻塞 —— 实现已过且需求方将装包复验。

1a — 统一 hover 语义(「所见」侧稳定)✅ 已完成(7b74707)

  • 替换 iter30 v7 过度粘性守卫(if (!(prevIsManual && lbl < MANUAL_SEGMENT_OFFSET))):改为真实区域一律接管高亮;仅光标落极小碎面 crumb(faceCount < max(16, 总面数*0.001))才允许手动段保持粘性(覆盖 seg=0 碎片误填,不阻挡正常区域切换)
  • 加 segmentFaceCount 映射(id→faceCount);加陈旧清除 effect(标签不在 segmentIds 立即清空);无命中时 nextHover = null
  • 删 lastValidHoveredSegmentRef 复活逻辑(§2.1 #5)
  • 手动验证:hover 手动区 A → 移到 auto 区 B → 高亮立即切到 B;移出模型 → 高亮消失(需求方多轮实测无粘滞回归,迭代 35 后 hover 始终跟随光标)

1b — 收敛 fill 决策 + 删闸门 ✅ 基本完成(eea2fd1 + 早轮 !opts?.wholeRegion 改动)

  • fill 目标权威源已为 segmentLabels[faceId](早轮 usePaintTool.ts 改 !opts?.wholeRegion && label !== undefined,同时删 !!seg 与 realSegs.length > 1 —— §2.1 #2/#3)
  • 删除 WHOLE_SEGMENT_MAX_SHARE / MANUAL_REGION_MAX_SHARE 常量及引用(§2.1 #3 配套);删除 giantSegmentIds 及 autoSegs.length <= 1 早退(§2.1 #4,原依赖 0d —— 现 0d 已完成故已删)
  • SegmentHighlight/FillFaceHighlight(几何重建叠加)已删,高亮改走方案 α 着色器(Gate 0d);hover 不再压制 giant 段
  • lastValidHoveredSegmentRef 复活通路(§2.1 #5)已在 1a 一并删除;Viewport.tsx sticky 块经 1a 重写为 cursor-follow
  • 手动验证:Fill 整段 = 高亮整段,HUD 无 MISMATCH(需求方实测 fill 整段填充,无"高亮 A 填 B"反馈;A4 系统复验仍留 Phase 2)

1c — 渲染契约前置改造(为 Phase 3 铺路,见 §2.7)✅ 由 0d 方案吸收,无需再做

原任务是把 SegmentHighlight/SegmentOutline 入参从 selectedSegment: number 改成集合,为 Phase 3 多选铺路。Gate 0d 已删除这两个几何重建组件,高亮统一走着色器 uniform(hover/选中都写 uHighlightLabel,不存在"两个面集"),§2.7 描述的叠加冲突被方案 α 结构性化解。

  • SegmentHighlight/SegmentOutline 几何重建组件已删(eea2fd1),无"入参改集合"对象
  • 「选中集 + hover 段」叠加规则由着色器 uniform 承载(Viewport.tsx 渲染单 label),无恒真判等残留
  • 选中集未进持久化(SegmentsPanel 多选为本地 state,见 SegmentsPanel.tsx),P1-6 条件门未触发
  • 结论:1c 关闭,Phase 3 直接落地(见下)

Phase 1 DoD:

  • npx tsc --noEmit exit 0(持续通过)
  • cargo test --lib 不回退(≥13,现 103)
  • HUD 无 MISMATCH(需求方实测无"高亮 A 填 B"反馈;A4 系统复验仍留 Phase 2)
  • 同步更新 PRD:FR-PNT-02(取消半径降级)、FR-PNT-03(click 权威)— ⚠️ PRD 仍未同步(当前 PRD 仍是旧语义),待补

Phase 2 — 复验 A4 / A5

  • A4:150 万面模型,分别在 auto 大分区、auto 小分区、手动区、单一真实分区模型上 Fill,HUD 四次均无 MISMATCH
  • A5:笔刷光标环位置 == 着色位置,无偏移
  • EXIT:A4 通过;若仍不一致,停止打补丁,派 REFUTE 重新取证(不得进入第 9 轮同思路修补)

Phase 3 — 分区合并(D4)✅ 已完成(并入智能分区泳道)

撤销架构(0b′)与渲染契约(1c)已在上游关闭;实现 f7d2f83(合并)+ 72455f3(拆分)+ 39736dd(命名)已全部落地并通过需求方实测。

  • 后端 merge_segments(target_label, source_labels):重写 segment_labels + rebuild_segments()(f7d2f83)
  • 后端单测(普通合并 / 含手动区 / 单分区入参 / 空入参 / 越界 label)
  • SegmentsPanel 多选交互 + 「合并」按钮(Ctrl+点击多选 + mergeInto 动作)
  • 按 0b′ 契约接入撤销(label-only 历史,增量 diff)
  • 修复 §2.4 配色不同源:SegmentsPanel.tsx 改为按 seg.id % len 取色,与 useMesh.ts:96 统一(788f7b9)
  • DoD:cargo test --lib 13 → ≥18(现 103);tsc exit 0;合并后撤销能完整还原
  • 拆分(D4 取证「拆分已具备」的强化):split_segment 沿内部棱角拆成多区,可撤销(72455f3 + 阈值滑块)
  • PRD FR-SEG-03 同步:当前 PRD 中 FR-SEG-03 仍是「分区元数据双写」旧条目,未补合并语义(Event-driven:选中 ≥2 分区触发合并 → 归并为单一分区保留目标 label)⚠️ 待补

Phase 4 — 3MF 收口(A9)

Gate 0b 已排除格式层风险,0e 已确认环境,此处做真实规模验证。

  • 真实彩色模型导出 3MF → OrcaSlicer 打开验色
  • 同一产物 → SnapmakerOrca 打开验色(若 0e 判定不可获得则跳过,A9 降为单基线)
  • DoD:基线全部通过,截图存档

Phase 5 — 发版收口

  • A1 / A2 / A3 / A7 / A8 逐条实测(此前未系统走过完整清单)
  • B1 npx tsc --noEmit exit 0
  • B2 cargo test --lib 全通过
  • B3 npx tauri build --bundles nsis 产出 setup.exe + 便携 exe,冒烟启动
  • CHANGELOG 补齐(当前实际停在 Round 3-4)
  • docs/04 缺陷清单状态回填 + 补录 §2.4 新缺陷
  • 打 tag v0.1.0(本仓库第一个 tag)

4. v0.1 验收清单(判定「可用」的唯一依据)

ID 验收项 判据 Phase 状态
A1 大模型导入 150 万面 STL <6 秒,无卡死,默认灰面 5 ⬜
A2 智能分区 双立方体分出 2 段;块体+薄板凹接合处边界合理(可配置多算法:曲率 K-Means / 形态直径 SDF / 二面角,前端 IntelligentSegmentPanel b087c3c+47004d9;golden 拓扑断言 cube→6 / sphere→1 / 双球→2 已通过;需求方多轮实测并驱动默认算法 eac4a64 + crease 阈值 82b7366 迭代) 5 ✅ 完成
A3 套索闭合 吸附跟随流畅;点回起点必闭合;无「点完消失」 5 🟡 修复中(156f673+71e8d34 已修两轮,待需求方最新包复验)
A4 Fill 一致性 所填面集 == 黄色高亮面集,HUD 无 MISMATCH 2 🟡 基本完成(Gate 0d 着色器高亮 + 1a/1b 收敛决策权;HUD MISMATCH 断言已就位,系统复验待做)
A5 笔刷精度 光标环位置即着色位置,无偏移 2 ⬜
A6 撤销 连续 10 次操作逐级撤销并重做,颜色完全还原 0c 🟡 实现完成(S1/S2 + 命名/合并/拆分全部可撤销,5c85e80~72455f3);150 万面性能实测(Gate 0c)未做
A7 偏好持久化 改主题/语言/笔刷后重启,设置全保留 5 ✅ 实现完成(zustand persist:theme/language/shading/brush/color/snap/lastExport/lastSegment*)
A8 主题无闪白 深色主题启动全程无白屏帧 5 ⬜
A9 3MF 导出 产物在 OrcaSlicer(+ SnapmakerOrca,视 0e)打开,几何正确且分色边界与工具内一致(判据已按 §2.8 修订:切片器颜色由 filament 槽位解析,不能要求 RGB 逐位一致) 0b→§2.8 裁决→4 🔴 阻塞(spool 复验 + D6-a + 双基线 E2E 未完成)
A10 分区合并(D4 新增) 选中 ≥2 分区合并为一,可撤销还原 3 ✅ 完成(f7d2f83 + SegmentsPanel 多选合并)
B1 npx tsc --noEmit exit code 0 5 ✅ 持续通过
B2 cargo test --lib 全通过(目标 ≥18 项) 5 ✅ 103 passed / 0 failed / 1 ignored
B3 npx tauri build --bundles nsis 产出 setup.exe 与便携 exe 5 ✅ 多次成功(B3 例行化)

B3 例行化(backlog M6):NSIS 构建在 journal 中已多次成功,属低风险,归入「每次提交必跑」例行项,不与 A9 捆绑为发版门槛。


5. 范围与技术债对照表

纳入 v0.1

分区合并(D4 → A10);§2.4 配色不同源修复(Phase 3);P1-8 render 副作用(Phase 0 前置,因其污染 Gate 测量);P1-3 addUpdateRange 截断(与 0d 同一战场,合并在同批 buffer 改造中完成,避免重复改同段代码)。

排除 v0.1(附理由)

项 理由
调色板预设 + 颜色历史 增量功能,不影响「现有功能可用」
批量处理同类模型 同上
OrcaSlicer 插件形态集成 与 A9「导出产物能被 OrcaSlicer 打开」是两回事,后者是 v0.1 硬门槛
拆分产物指定为 auto 区 / 按 SDF 自动细分 / 拆分 UI 显性化 拆分基础能力已具备(§2.3),这些是增强
P1-7 snapEnabled 无 UI 入口 ⚠️ 待定:属「已实现但用户摸不到」,v0.1 需二选一 —— 补 UI 或明确移除。留待 Phase 5 前决策

条件门与技术债

ID 内容 触发条件
P1-6 appStore.ts:418 version:1 无 migrate 条件门:若 v0.1 未改 partialize 8 项默认值 → 留 v0.2;一旦 Phase 1c 把选中集加入持久化、或触碰任一默认值 → 升级为发版前必做。⚠️ appStore.ts:110-115 的「不要用 migrate」注释仅针对 legacy cym-theme 键,补 migrate 前须在 journal 限定其适用范围
P1-4 stroke 去重 技术债
P2-9 Viewport.tsx 体量过大需拆分 技术债
P2-10 CI 缺失 技术债

6. 变更记录

版本 日期 变更
PLAN 2026-08-06 初版。基于 D1–D5 裁决,取代 docs/05 被证伪的候选路线图
IMPL-1 2026-08-06 Phase 0 首批落地 + 两处立论修正:①P1-8 已修(550289e),并推翻其原始立论——StrictMode 是确定性二次调用,危害是测量值稳定放大 ~2×(准确性),不是不可复现;配套加 VITE_NO_STRICT 逃生舱;②Gate 0a 已完成(8ea2217),引入唯一真源 renderedHighlightLabel,并修正 HUD 对 local/whole-region 路由的 MISMATCH 误报;③Gate 0b 调查完成,结论升级为阻塞:规范层 4 条缺陷(不止原记 3 条,默认命名空间也是错的),实现层证实 basematerials 在 OrcaSlicer 全 Format 目录零读者,且真实机制是逐面 paint_color 槽位序号而非 RGB;④由此新增 §2.8 色彩域基数差(R4 级,A9 判据同步修订)
REVISE-1 2026-08-06 吸收独立 REFUTE 的 3 blocker + 6 major:①§2.1 删除清单由 2 处扩至 5 处(补 sticky hover、残留 !!seg/realSegs gate);②§2.2 否决方案 β(主 geometry 非索引 + 破坏 indirect raycast),改推着色器方案 α;③新增 §2.5 撤销双栈架构问题并前移为 Gate 0b′(R4);④新增 §2.6 D2 对 0c 的条件依赖,0c 补失败分支;⑤新增 §2.4 面板/3D 配色不同源缺陷(docs/04 未收录);⑥Phase 0 扩为六 Gate + P1-8 前置;⑦Phase 1 拆 1a/1b/1c,渲染契约前移;⑧新增 Gate 0e 切片器环境;⑨§5 补全纳入/排除对照与理由
REFUTE-4 2026-08-07 Gate 0b 续:需求方二轮反馈 spool 卡 Bed type 空 / Diameter <<mm / spool 名截断。派独立 subagent 在双基线复核 embedded preset 提取机制,推翻 4 点(①spool 卡读 embedded preset,须带 *_settings_id;②6 filament 须唯一名防塌色;③temperature/bed_plate_temp 在 obsolete ignore 集改 nozzle_temperature/hot_plate_temp;④default_bed_type 是 coString 非 coEnum)。project_config.rs 拆 4 类 writer + 10 单测,threemf.rs 写 project+machine+process+N filament 共 6+N 份 config;cargo test --lib 26 passed,sample 解包 12 entry 全绿。新版 Tauri bundle(exe/msi/nsis)构建中(ztWW4u),待需求方复验三处
REFUTE-6/7 + IMPL 2026-08-07 Gate 0b 预设库落地(2e4e070):需求方需求升级为导出可选厂商/机型/工艺/耗材。实现前证伪 4 点(⑥printer_model_id 是 plate 切片元数据与预设选择无关,须嵌入 machine preset 且 printer_settings_id 逐字相等;⑦嵌入 machine preset 须完整配置含 gcode 宏,白名单已补;⑧filament 颜色与预设名无关,@slotN 有害,selected 路径写零 filament 文件;⑨inherits_group 是 N+2 数组)。落地:extract_orca_presets.py + snapmaker_u1.json(4 变体/30 工艺/113 耗材)、presets.rs、project_config.rs 重写、threemf.rs 3 参签名、前端 ExportDialog + persist。cargo test --lib 66 passed、tsc exit 0、vite build ok、verify_3mf.py 解包全绿。待需求方复验新 sample(下拉框应显示 U1 (0.4 nozzle))
IMPL-2(高亮 / 导入健壮性) 2026-08-08→09 用户亲验方案 B 后进入大幅优化与健壮性修复:①方案 α 着色器高亮落地(eea2fd1)—— 删 SegmentHighlight/FillFaceHighlight/giantSegmentIds/WHOLE_SEGMENT_MAX_SHARE,主材质 onBeforeCompile 注入 aSegLabel+uHighlightLabel/uHighlightColor,hover 切换 O(1),giant 段瞬时高亮;②着色器性能优化(485aa7d)—— 条件性 demand IdleFrameloop + 删 preserveDrawingBuffer + flat varying(REFUTE 推翻全局 demand / gl_PrimitiveID / Uint32 三候选);③高亮按工具门控(f192f98)—— 仅 fill/picker/segment 计算 hover,消除 brush 拖涂 60fps 重渲染;④hover 解粘滞(7b74707)—— 替换 iter30 v7 过度粘性守卫为 cursor-follow + crumb 粘性 + 陈旧立即清除;⑤Sphere.stl 导入崩溃修复(69a17ea)—— kiddo KdTree 在重合轴点 >32 时 panic,按 index 加 ~1e-4 确定性扰动,回归测试 kdtree_coincident_axis_no_panic,cargo test --lib 67 passed。NSIS 重打包供需求方装包复验
IMPL-3(智能分区全能力 / 性能 / lasso) 2026-08-09→10 ①智能分区闭环:特征感知分区 b087c3c + IntelligentSegmentPanel/参数持久化 47004d9/0d5f162;命名 39736dd;合并 f7d2f83;拆分 72455f3;手动区跨 auto 重跑保留 364a2be、手动 label 不回收 b7d64c4;segment 元数据单点构建 ec9af61;配色按 label 统一(§2.4 修复)788f7b9。②性能:auto_segment_v2 改 async + 移出 Mutex + 空 face_colors(点击不再卡死主线程)06303ee;阶段进度条 + 动画转圈 b1d523c;import 默认对齐 curvatureKMeans+useSdf eac4a64;crease 阈值 20°→45° 修过分割 82b7366。③lasso 修复:stale snap 156f673 + 闭合拓扑守卫(顶点 1-ring + 阈值 0.012→0.004)71e8d34。④知识沉淀:docs/08 算法参考与论文 e1d374e。cargo test --lib 103 passed。2026-08-10 路线图看板全面同步至真实状态
On this page