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 不可覆盖):
- 智能分区效果 — 装甲人偶经
eac4a64(默认 curvatureKMeans)+82b7366(crease 45°)应拆成 ~7–10 个分区(头/躯干/臂/腿),不再是 89 区 / 1 整块。 - 阶段进度条 + 转圈(
b1d523c)— 点「智能分区」应看到「阶段 X/Y · 名称」+ 旋转环,不再出现"卡死/无反馈"。 - Lasso 第 3 点(
71e8d34)— 胸前细电缆上连续点 3 点:第 3 点应正常放置、不误报黄色"ready to close";只有点回起点顶点附近才变黄闭合。 - Sphere.stl 导入不再崩(
69a17ea)— 安装最新 NSIS,打开samples/_src/Sphere.stl确认不闪退。 - Gate 0b 预设库复验 — 在切片器打开
examples/paint_color_sample.3mf:下拉框 Snapmaker U1 (0.4 nozzle)、喷嘴 0.4、工艺 0.20 Standard、spool 卡完整;首次弹 "Customized Preset" 确认框属正常。 - (不阻塞) 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 语义冲突(用户仍会遇到「点了没高亮」) |
| 已否决:非索引几何 + 破坏 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.tsbaseGeometry/paintColorArray),后者改用useLayoutEffect在 commit 后发布- 新增测量逃生舱:
VITE_NO_STRICT=1时关闭 StrictMode,令 0c/0d 的内存与耗时反映生产行为- DoD:
npx tsc --noEmitexit 0;vite build通过;cargo test --lib13 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 --noEmitexit 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-vertexaSegLabel(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-loopREFUTE 证伪 P1 全局 demand / P2 gl_PrimitiveID / P3 Uint32):条件性 demand(IdleFrameloop,静止 1.5s 切 demand,任意交互唤醒);删preserveDrawingBuffer:true;aSegLabelvarying 加flat - 工具门控(
f192f98):高亮仅fill|picker|segment工具计算;其余工具renderedHighlightLabel返 null → uniform -1 短路,消除 brush 拖涂 60fps 重渲染风暴 - DoD:
tsc --noEmitexit 0;vite build631 模块通过;cargo test --lib67 passed。视觉一致性 + 卡顿消失待需求方装包复验(见 §0 待复验清单) - 失败分支(已规避):α 与主题色/vertexColors 耦合风险经
onBeforeCompilechunk 注入化解;若未来自定义 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.tsxsticky 块经 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 --noEmitexit 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 --lib13 → ≥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 --noEmitexit 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 路线图看板全面同步至真实状态 |