@faicad/faijs 0.30.9 → 0.31.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (284) hide show
  1. package/dist/api/align/index.d.ts +64 -0
  2. package/dist/api/align/index.d.ts.map +1 -0
  3. package/dist/api/align/index.js +101 -0
  4. package/dist/api/align/index.js.map +1 -0
  5. package/dist/api/api-namespace.d.ts +5 -4
  6. package/dist/api/api-namespace.d.ts.map +1 -1
  7. package/dist/api/api-namespace.js +36 -4
  8. package/dist/api/api-namespace.js.map +1 -1
  9. package/dist/api/boolean-op/index.d.ts +65 -0
  10. package/dist/api/boolean-op/index.d.ts.map +1 -0
  11. package/dist/api/boolean-op/index.js +113 -0
  12. package/dist/api/boolean-op/index.js.map +1 -0
  13. package/dist/api/boolean.d.ts.map +1 -1
  14. package/dist/api/boolean.js +15 -27
  15. package/dist/api/boolean.js.map +1 -1
  16. package/dist/api/brep-operations/healingFns.d.ts +13 -0
  17. package/dist/api/brep-operations/healingFns.d.ts.map +1 -1
  18. package/dist/api/brep-operations/healingFns.js +28 -1
  19. package/dist/api/brep-operations/healingFns.js.map +1 -1
  20. package/dist/api/brep-operations/topologyFns.d.ts +27 -6
  21. package/dist/api/brep-operations/topologyFns.d.ts.map +1 -1
  22. package/dist/api/brep-operations/topologyFns.js +46 -22
  23. package/dist/api/brep-operations/topologyFns.js.map +1 -1
  24. package/dist/api/chamfer.d.ts +5 -4
  25. package/dist/api/chamfer.d.ts.map +1 -1
  26. package/dist/api/chamfer.js +12 -11
  27. package/dist/api/chamfer.js.map +1 -1
  28. package/dist/api/curve-sketch/index.d.ts.map +1 -1
  29. package/dist/api/curve-sketch/index.js +9 -3
  30. package/dist/api/curve-sketch/index.js.map +1 -1
  31. package/dist/api/draft.d.ts.map +1 -1
  32. package/dist/api/draft.js +1 -3
  33. package/dist/api/draft.js.map +1 -1
  34. package/dist/api/engrave.d.ts.map +1 -1
  35. package/dist/api/engrave.js +3 -13
  36. package/dist/api/engrave.js.map +1 -1
  37. package/dist/api/export-3mf.d.ts +73 -0
  38. package/dist/api/export-3mf.d.ts.map +1 -0
  39. package/dist/api/export-3mf.js +121 -0
  40. package/dist/api/export-3mf.js.map +1 -0
  41. package/dist/api/export-brep.d.ts +6 -0
  42. package/dist/api/export-brep.d.ts.map +1 -1
  43. package/dist/api/export-brep.js +11 -2
  44. package/dist/api/export-brep.js.map +1 -1
  45. package/dist/api/export-step.d.ts +67 -0
  46. package/dist/api/export-step.d.ts.map +1 -0
  47. package/dist/api/export-step.js +98 -0
  48. package/dist/api/export-step.js.map +1 -0
  49. package/dist/api/export-stl.d.ts +8 -0
  50. package/dist/api/export-stl.d.ts.map +1 -1
  51. package/dist/api/export-stl.js +23 -8
  52. package/dist/api/export-stl.js.map +1 -1
  53. package/dist/api/feature-repair.d.ts +1 -1
  54. package/dist/api/feature-repair.d.ts.map +1 -1
  55. package/dist/api/feature-repair.js +1 -7
  56. package/dist/api/feature-repair.js.map +1 -1
  57. package/dist/api/fillet-variable.d.ts.map +1 -1
  58. package/dist/api/fillet-variable.js +0 -1
  59. package/dist/api/fillet-variable.js.map +1 -1
  60. package/dist/api/fillet.d.ts +3 -3
  61. package/dist/api/fillet.d.ts.map +1 -1
  62. package/dist/api/fillet.js +3 -4
  63. package/dist/api/fillet.js.map +1 -1
  64. package/dist/api/generated/operations.js +6 -6
  65. package/dist/api/generated/operations.js.map +1 -1
  66. package/dist/api/generated/script-face-manifest.d.ts +1 -1
  67. package/dist/api/generated/script-face-manifest.js +3 -3
  68. package/dist/api/generated/script-face-manifest.js.map +1 -1
  69. package/dist/api/generated/script-face.d.ts +1 -1
  70. package/dist/api/generated/script-face.js +1 -1
  71. package/dist/api/generated/script-face.js.map +1 -1
  72. package/dist/api/generated/sketching.js +1 -1
  73. package/dist/api/generated/sketching.js.map +1 -1
  74. package/dist/api/generated/topology.js +10 -10
  75. package/dist/api/generated/topology.js.map +1 -1
  76. package/dist/api/helix.d.ts +1 -1
  77. package/dist/api/helix.js +1 -1
  78. package/dist/api/helix.js.map +1 -1
  79. package/dist/api/index.d.ts +4 -1
  80. package/dist/api/index.d.ts.map +1 -1
  81. package/dist/api/index.js +16 -4
  82. package/dist/api/index.js.map +1 -1
  83. package/dist/api/internal/compat-op.d.ts +6 -6
  84. package/dist/api/internal/compat-op.d.ts.map +1 -1
  85. package/dist/api/internal/compat-op.js +7 -9
  86. package/dist/api/internal/compat-op.js.map +1 -1
  87. package/dist/api/internal/export-members.d.ts +80 -0
  88. package/dist/api/internal/export-members.d.ts.map +1 -0
  89. package/dist/api/internal/export-members.js +104 -0
  90. package/dist/api/internal/export-members.js.map +1 -0
  91. package/dist/api/internal/l3-bridge.d.ts +17 -0
  92. package/dist/api/internal/l3-bridge.d.ts.map +1 -1
  93. package/dist/api/internal/l3-bridge.js +23 -1
  94. package/dist/api/internal/l3-bridge.js.map +1 -1
  95. package/dist/api/measure-query/index.d.ts +262 -0
  96. package/dist/api/measure-query/index.d.ts.map +1 -0
  97. package/dist/api/measure-query/index.js +390 -0
  98. package/dist/api/measure-query/index.js.map +1 -0
  99. package/dist/api/pattern.d.ts.map +1 -1
  100. package/dist/api/pattern.js +0 -3
  101. package/dist/api/pattern.js.map +1 -1
  102. package/dist/api/primitives.d.ts +6 -6
  103. package/dist/api/primitives.d.ts.map +1 -1
  104. package/dist/api/primitives.js +12 -11
  105. package/dist/api/primitives.js.map +1 -1
  106. package/dist/api/profile.d.ts.map +1 -1
  107. package/dist/api/profile.js +0 -1
  108. package/dist/api/profile.js.map +1 -1
  109. package/dist/api/punch-hole.d.ts.map +1 -1
  110. package/dist/api/punch-hole.js +0 -1
  111. package/dist/api/punch-hole.js.map +1 -1
  112. package/dist/api/replicate.d.ts +5 -7
  113. package/dist/api/replicate.d.ts.map +1 -1
  114. package/dist/api/replicate.js +18 -56
  115. package/dist/api/replicate.js.map +1 -1
  116. package/dist/api/section-by-plane.d.ts.map +1 -1
  117. package/dist/api/section-by-plane.js +0 -1
  118. package/dist/api/section-by-plane.js.map +1 -1
  119. package/dist/api/shell.d.ts +1 -1
  120. package/dist/api/shell.d.ts.map +1 -1
  121. package/dist/api/shell.js +11 -2
  122. package/dist/api/shell.js.map +1 -1
  123. package/dist/api/sketch-on-face.d.ts +3 -3
  124. package/dist/api/sketch-on-face.d.ts.map +1 -1
  125. package/dist/api/sketch-on-face.js +6 -4
  126. package/dist/api/sketch-on-face.js.map +1 -1
  127. package/dist/api/sketch-on-plane.d.ts.map +1 -1
  128. package/dist/api/sketch-on-plane.js +0 -1
  129. package/dist/api/sketch-on-plane.js.map +1 -1
  130. package/dist/api/split-by-plane.d.ts.map +1 -1
  131. package/dist/api/split-by-plane.js +0 -1
  132. package/dist/api/split-by-plane.js.map +1 -1
  133. package/dist/api/split.d.ts +1 -1
  134. package/dist/api/split.js +1 -1
  135. package/dist/api/split.js.map +1 -1
  136. package/dist/api/surface/arg-spec.d.ts +3 -12
  137. package/dist/api/surface/arg-spec.d.ts.map +1 -1
  138. package/dist/api/surface/arg-spec.js +19 -26
  139. package/dist/api/surface/arg-spec.js.map +1 -1
  140. package/dist/api/sweep.d.ts +15 -3
  141. package/dist/api/sweep.d.ts.map +1 -1
  142. package/dist/api/sweep.js +5 -3
  143. package/dist/api/sweep.js.map +1 -1
  144. package/dist/api/thicken.d.ts.map +1 -1
  145. package/dist/api/thicken.js +11 -2
  146. package/dist/api/thicken.js.map +1 -1
  147. package/dist/api/transform.d.ts +3 -3
  148. package/dist/api/transform.d.ts.map +1 -1
  149. package/dist/api/transform.js +47 -17
  150. package/dist/api/transform.js.map +1 -1
  151. package/dist/boolean/index.d.ts +23 -0
  152. package/dist/boolean/index.d.ts.map +1 -0
  153. package/dist/boolean/index.js +27 -0
  154. package/dist/boolean/index.js.map +1 -0
  155. package/dist/brep/brep-chain.d.ts +1 -19
  156. package/dist/brep/brep-chain.d.ts.map +1 -1
  157. package/dist/brep/brep-chain.js +0 -1
  158. package/dist/brep/brep-chain.js.map +1 -1
  159. package/dist/brep/brep-ops.d.ts +4 -1
  160. package/dist/brep/brep-ops.d.ts.map +1 -1
  161. package/dist/brep/brep-ops.js +9 -5
  162. package/dist/brep/brep-ops.js.map +1 -1
  163. package/dist/brep/engine/adapters/brep-mock.d.ts.map +1 -1
  164. package/dist/brep/engine/adapters/brep-mock.js +0 -3
  165. package/dist/brep/engine/adapters/brep-mock.js.map +1 -1
  166. package/dist/brep/engine/adapters/brepkit.d.ts +4 -3
  167. package/dist/brep/engine/adapters/brepkit.d.ts.map +1 -1
  168. package/dist/brep/engine/adapters/brepkit.js +4 -98
  169. package/dist/brep/engine/adapters/brepkit.js.map +1 -1
  170. package/dist/brep/engine/adapters/occt.d.ts.map +1 -1
  171. package/dist/brep/engine/adapters/occt.js +0 -144
  172. package/dist/brep/engine/adapters/occt.js.map +1 -1
  173. package/dist/brep/engine/native-history.d.ts +27 -0
  174. package/dist/brep/engine/native-history.d.ts.map +1 -0
  175. package/dist/brep/engine/native-history.js +54 -0
  176. package/dist/brep/engine/native-history.js.map +1 -0
  177. package/dist/brep/engine/registry.d.ts +2 -4
  178. package/dist/brep/engine/registry.d.ts.map +1 -1
  179. package/dist/brep/engine/registry.js +1 -1
  180. package/dist/brep/engine/registry.js.map +1 -1
  181. package/dist/brep/engine/types.d.ts +35 -77
  182. package/dist/brep/engine/types.d.ts.map +1 -1
  183. package/dist/brep/engine/types.js.map +1 -1
  184. package/dist/brep/export/export-model.d.ts +21 -0
  185. package/dist/brep/export/export-model.d.ts.map +1 -1
  186. package/dist/brep/export/export-model.js +171 -73
  187. package/dist/brep/export/export-model.js.map +1 -1
  188. package/dist/brep/export/step.d.ts +17 -1
  189. package/dist/brep/export/step.d.ts.map +1 -1
  190. package/dist/brep/export/step.js +133 -0
  191. package/dist/brep/export/step.js.map +1 -1
  192. package/dist/brep/export/stl.d.ts +22 -1
  193. package/dist/brep/export/stl.d.ts.map +1 -1
  194. package/dist/brep/export/stl.js +58 -10
  195. package/dist/brep/export/stl.js.map +1 -1
  196. package/dist/brep/export/transform12.d.ts +47 -0
  197. package/dist/brep/export/transform12.d.ts.map +1 -0
  198. package/dist/brep/export/transform12.js +87 -0
  199. package/dist/brep/export/transform12.js.map +1 -0
  200. package/dist/brep/face-evolution.d.ts +53 -0
  201. package/dist/brep/face-evolution.d.ts.map +1 -1
  202. package/dist/brep/face-evolution.js +65 -0
  203. package/dist/brep/face-evolution.js.map +1 -1
  204. package/dist/browser-host/index.d.ts +4 -1
  205. package/dist/browser-host/index.d.ts.map +1 -1
  206. package/dist/browser-host/index.js +7 -1
  207. package/dist/browser-host/index.js.map +1 -1
  208. package/dist/browser.d.ts +4 -8
  209. package/dist/browser.d.ts.map +1 -1
  210. package/dist/browser.js +4 -26
  211. package/dist/browser.js.map +1 -1
  212. package/dist/cad-runtime/backend-dispatch.d.ts +13 -58
  213. package/dist/cad-runtime/backend-dispatch.d.ts.map +1 -1
  214. package/dist/cad-runtime/backend-dispatch.js +22 -80
  215. package/dist/cad-runtime/backend-dispatch.js.map +1 -1
  216. package/dist/cad-runtime/content-key.d.ts +3 -3
  217. package/dist/cad-runtime/content-key.js +3 -3
  218. package/dist/cad-runtime/content-key.js.map +1 -1
  219. package/dist/cad-runtime/ports.d.ts +28 -0
  220. package/dist/cad-runtime/ports.d.ts.map +1 -1
  221. package/dist/cad-runtime/ports.js.map +1 -1
  222. package/dist/cad-runtime/runtime.d.ts +33 -12
  223. package/dist/cad-runtime/runtime.d.ts.map +1 -1
  224. package/dist/cad-runtime/runtime.js +44 -48
  225. package/dist/cad-runtime/runtime.js.map +1 -1
  226. package/dist/define-op.d.ts +6 -11
  227. package/dist/define-op.d.ts.map +1 -1
  228. package/dist/define-op.js +3 -20
  229. package/dist/define-op.js.map +1 -1
  230. package/dist/env-agnostic.d.ts +2 -2
  231. package/dist/env-agnostic.d.ts.map +1 -1
  232. package/dist/env-agnostic.js +1 -1
  233. package/dist/env-agnostic.js.map +1 -1
  234. package/dist/index.d.ts +13 -64
  235. package/dist/index.d.ts.map +1 -1
  236. package/dist/index.js +17 -75
  237. package/dist/index.js.map +1 -1
  238. package/dist/lang/symbol-table.generated.d.ts +17 -0
  239. package/dist/lang/symbol-table.generated.d.ts.map +1 -1
  240. package/dist/lang/symbol-table.generated.js +17 -0
  241. package/dist/lang/symbol-table.generated.js.map +1 -1
  242. package/dist/mesh/index.d.ts +18 -23
  243. package/dist/mesh/index.d.ts.map +1 -1
  244. package/dist/mesh/index.js +18 -20
  245. package/dist/mesh/index.js.map +1 -1
  246. package/dist/mesh/io.d.ts +42 -14
  247. package/dist/mesh/io.d.ts.map +1 -1
  248. package/dist/mesh/io.js +77 -13
  249. package/dist/mesh/io.js.map +1 -1
  250. package/dist/mesh/threemf-loader.d.ts +38 -4
  251. package/dist/mesh/threemf-loader.d.ts.map +1 -1
  252. package/dist/mesh/threemf-loader.js +115 -34
  253. package/dist/mesh/threemf-loader.js.map +1 -1
  254. package/dist/mesh/types.d.ts +30 -0
  255. package/dist/mesh/types.d.ts.map +1 -1
  256. package/dist/mesh/types.js.map +1 -1
  257. package/dist/node-host/execute-script.d.ts +2 -1
  258. package/dist/node-host/execute-script.d.ts.map +1 -1
  259. package/dist/node-host/execute-script.js +5 -4
  260. package/dist/node-host/execute-script.js.map +1 -1
  261. package/dist/node-host/index.d.ts +1 -0
  262. package/dist/node-host/index.d.ts.map +1 -1
  263. package/dist/node-host/index.js +4 -0
  264. package/dist/node-host/index.js.map +1 -1
  265. package/dist/primitives/index.d.ts +19 -0
  266. package/dist/primitives/index.d.ts.map +1 -0
  267. package/dist/primitives/index.js +21 -0
  268. package/dist/primitives/index.js.map +1 -0
  269. package/dist/runtime-state.d.ts +63 -29
  270. package/dist/runtime-state.d.ts.map +1 -1
  271. package/dist/runtime-state.js +57 -0
  272. package/dist/runtime-state.js.map +1 -1
  273. package/dist/sdf/index.d.ts +12 -0
  274. package/dist/sdf/index.d.ts.map +1 -0
  275. package/dist/sdf/index.js +13 -0
  276. package/dist/sdf/index.js.map +1 -0
  277. package/dist/sdk.d.ts +0 -1
  278. package/dist/sdk.d.ts.map +1 -1
  279. package/dist/sdk.js.map +1 -1
  280. package/dist/weapp.d.ts +2 -2
  281. package/dist/weapp.d.ts.map +1 -1
  282. package/dist/weapp.js +1 -1
  283. package/dist/weapp.js.map +1 -1
  284. package/package.json +276 -35
package/dist/mesh/io.js CHANGED
@@ -6,17 +6,25 @@
6
6
  * 坐标永远以 faijs 基准长度单位(mm)存储 — Shape 不贴 unit 标签(D3)。
7
7
  *
8
8
  * 单位换算边界(unit-system §5.2):
9
- * - STL 无单位元数据 → 由 `opts.unit` 显式声明优先;未声明时 faijs 按历史
10
- * host 行为做启发式猜测(`guessStlUnit`,unified 方案 §6.1/§8 —— 3d_editor
11
- * 迁移回归的单位断言依赖此行为)。猜测收敛在 faijs 内(红线 R0:宿主不
12
- * 自实现单位换算),G0 = 折算后坐标,渲染 = 拓扑 = G0。
9
+ * - 本模块只处理 **mesh 格式(STL/3MF)**,坐标可直接折算 → `opts.unit` 提供即
10
+ * **强制设置模型的源单位**(直接指定,覆盖/忽略模型内部记录的单位——STL 无
11
+ * 内部单位、3MF `<model unit>` 声明被覆盖),按 `unitScale(unit)` 折算坐标。
12
+ * 它**只可能来自调用方的显式知识**(脚本作者手写 `cad.load({ file, unit })`、
13
+ * 或宿主让用户选定单位后传入);调用方不知道单位时**必须省略**——省略时:
14
+ * STL 走 faijs 启发式(`guessStlUnit`,unified 方案 §6.1/§8 —— 3d_editor
15
+ * 迁移回归的单位断言依赖此行为)、3MF 走文件 `<model unit>`(无声明 →
16
+ * millimeter)。猜测/折算收敛在 faijs 内(红线 R0:宿主不自实现单位换算),
17
+ * G0 = 折算后坐标,渲染 = 拓扑 = G0。
18
+ * - **STEP/.brep 不经过本模块**(走 BREP 链,load op 内 OCCT 读取):OCCT 按
19
+ * 文件内部单位声明折算(SI_UNIT),**不支持强制设置单位**——load op 对
20
+ * BREP 路径 + 用户显式 unit 直接**明确报错**(不缩放、不 workaround)。
13
21
  * - 3MF `<model unit>` 由解析器读取并换算(micron/mm/cm/inch/foot/meter)。
14
22
  *
15
23
  * 返回契约(unit-system §10.6):`importFile` 返回 `{ shape, unit }` — `unit`
16
- * 是文件源单位(必然有值;faijs 是单一真源):3MF/STEP 取文件声明(STEP 无声明
17
- * → 'mm',与 OCCT 一致),STL 取 opts.unit 或 guessStlUnit 启发式猜测。坐标恒
18
- * 为基准值 — `unit` 只作元数据(sourceUnit 记录、导出目标判定),绝不参与几何
19
- * 缩放(读入折算已由内核/解析器完成,二次缩放 = 双重换算)。
24
+ * 是文件源单位(必然有值;faijs 是单一真源):3MF 取 opts.unit(强制)或文件
25
+ * 声明(无声明 → millimeter),STL 取 opts.unit 或 guessStlUnit 启发式猜测。
26
+ * 坐标恒为基准值 — `unit` 只作元数据(sourceUnit 记录、导出目标判定),绝不
27
+ * 参与几何缩放(读入折算已由解析器完成,二次缩放 = 双重换算)。
20
28
  */
21
29
  import { geoToManifoldMesh } from '../boolean/geo-convert.js';
22
30
  import { parseStl } from './stl-loader.js';
@@ -97,17 +105,25 @@ export function detectStepUnit(stepText) {
97
105
  * @param buffer - the raw file bytes to parse (already decompressed; stpz
98
106
  * unzipping is the caller's job — faijs never unzips implicitly).
99
107
  * @param format - format identifier such as 'stl', '3mf'/'threemf' or 'step'
100
- * (defaults to 'stl').
101
- * @param opts - optional settings; `unit` (a UnitName string) declares the
102
- * source unit for formats without unit metadata (e.g. STL). Defaults to
103
- * the guessStlUnit heuristic when omitted.
108
+ * (defaults to 'stl'). **仅 mesh 格式(STL/3MF)走本模块**;STEP/BREP 走
109
+ * BREP 链(load op 内 OCCT 读取,不支持强制设置单位)。
110
+ * @param opts - optional settings; `unit` (a UnitName string) **force-sets the
111
+ * model's source unit** (STL/3MF): it is NOT a default, it directly specifies
112
+ * the unit and overrides/ignores any unit recorded inside the model (3MF
113
+ * `<model unit>`; STL has none), geometry scaled by `unitScale(unit)`. Only
114
+ * provide it when the caller actually knows the unit (author-written
115
+ * `cad.load({ file, unit })`, or a host unit picker). When the caller does
116
+ * NOT know the unit, it MUST omit `unit` — STL then guesses via
117
+ * `guessStlUnit` (the only allowed guessing path), 3MF uses the file
118
+ * declaration (none → millimeter). A wrong explicit unit scales geometry
119
+ * wrongly and is never re-guessed.
104
120
  * @returns the shape with base-unit coordinates plus the file's source unit
105
121
  * (`unit`, metadata only; always present — faijs is the single source of truth).
106
122
  */
107
123
  export async function importFile(buffer, format, opts) {
108
124
  const fmt = (format ?? 'stl').toLowerCase();
109
125
  if (fmt === '3mf' || fmt === 'threemf') {
110
- const archive = await parseThreemf(buffer);
126
+ const archive = await parseThreemf(buffer, opts);
111
127
  // P0(方案 §5.4):不再折叠——返回全部 `<build><item>` 对象(declaration order,
112
128
  // 坐标已在 parseThreemf 里折算为 faijs 基准单位)。`shape` 保留为 parts[0]
113
129
  // (单零件兼容,过渡期)。
@@ -153,6 +169,11 @@ export async function importFile(buffer, format, opts) {
153
169
  const bambuViews = buildBambuViews(bambu);
154
170
  if (bambuViews)
155
171
  importModel.bambuViews = bambuViews;
172
+ // 装配层级重建(方案 §2.1 / 第 5 步):含 `<components>` 时还原 CompoundShape,
173
+ // 叶节点携带相对父 transform;单零件平铺导入不挂载。
174
+ const assembly = archive.hierarchy && archive.localGeomById
175
+ ? buildAssemblyFromArchive(archive.hierarchy, archive.localGeomById)
176
+ : undefined;
156
177
  return {
157
178
  shape: parts[0],
158
179
  parts,
@@ -161,6 +182,7 @@ export async function importFile(buffer, format, opts) {
161
182
  importModel,
162
183
  // 整体级元数据(`<model><metadata>`)随导入结果上抛(方案 §5.1)。
163
184
  ...(archive.fileMeta ? { fileMeta: archive.fileMeta } : {}),
185
+ ...(assembly ? { assembly } : {}),
164
186
  };
165
187
  }
166
188
  if (fmt === 'stl') {
@@ -226,6 +248,48 @@ function threemfObjectToShape(obj) {
226
248
  ...(obj.meta ? { meta: obj.meta } : {}),
227
249
  };
228
250
  }
251
+ /**
252
+ * 由 3MF 装配层级重建 `CompoundShape`(方案 §2.1 / 第 5 步「导入归一」)。
253
+ *
254
+ * 见 `ThreemfArchive.hierarchy` / `localGeomById`:节点带**相对父**位姿(行主序 3×4,
255
+ * 平移在末列,与写出器 `transformToMatrix12` 同源);几何来自 `localGeomById` 的**未烘焙**
256
+ * 局部网格。叶节点的最终 `Shape.transform` 即该相对位姿(写回 `Shape.transform`,供
257
+ * 后续导出按 3MF `<component transform>` 原样还原——D1 往返)。容器节点落为
258
+ * `CompoundShape`(不含 `transform` / `meta`,与 `shape.ts` 的 `CompoundShape` 形态一致)。
259
+ *
260
+ * 注:`shape.ts` 的 `CompoundShape.children` 当前声明为 `Shape[]`(非递归),故嵌套装配
261
+ * 在类型层面暂不表达;运行时子节点可为子 `CompoundShape`(由 `isCompoundLike` 结构判定消费)。
262
+ * 把 `CompoundShape` 改为递归类型是独立的后续重构(会波及 faijs-extra / cad-runtime),本步
263
+ * 以最小改动桥接——见下方 `children` 处的收窄断言。
264
+ *
265
+ * 仅在存在真实装配(多 item 或任一 item 带 components)时返回非空;单零件平铺导入返回
266
+ * `undefined`(由调用方决定是否挂载到 `result.assembly`,避免改变单零件结果形状)。
267
+ */
268
+ function buildAssemblyFromArchive(hierarchy, localGeomById) {
269
+ const hasHierarchy = hierarchy.length > 1 || hierarchy.some((n) => (n.children?.length ?? 0) > 0);
270
+ if (!hasHierarchy)
271
+ return undefined;
272
+ const buildNode = (node) => {
273
+ const obj = localGeomById.get(node.objectId);
274
+ if (node.children && node.children.length > 0) {
275
+ // 收窄断言:共享 CompoundShape 非递归声明,运行时子节点可含子 CompoundShape。
276
+ return { kind: 'compound', children: node.children.map(buildNode) };
277
+ }
278
+ // 叶节点:局部几何 + 相对父位姿。
279
+ if (!obj || !obj.positions || !obj.indices) {
280
+ return { kind: 'compound', children: [] };
281
+ }
282
+ const leaf = threemfObjectToShape(obj);
283
+ if (node.transform) {
284
+ ;
285
+ leaf.transform = { matrix: Array.from(node.transform) };
286
+ }
287
+ return leaf;
288
+ };
289
+ return hierarchy.length === 1
290
+ ? buildNode(hierarchy[0])
291
+ : { kind: 'compound', children: hierarchy.map(buildNode) };
292
+ }
229
293
  /** Bambu 显示视图数据 → ImportModel.bambuViews(无任何 Bambu 结构时省略)。 */
230
294
  function buildBambuViews(bambu) {
231
295
  const plates = [...bambu.plates.keys()].sort((a, b) => a - b);
@@ -1 +1 @@
1
- {"version":3,"file":"io.js","sourceRoot":"","sources":["../../src/mesh/io.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * mesh IO API — headless 文件加载\n *\n * STL 由 core 自持的 `mesh/stl-loader.ts` 解析(零 three addons);3MF 由\n * `mesh/threemf-loader.ts` 解析(ZIP 解压 + XML,单位换算到 mm 基准)。\n * 坐标永远以 faijs 基准长度单位(mm)存储 — Shape 不贴 unit 标签(D3)。\n *\n * 单位换算边界(unit-system §5.2):\n * - STL 无单位元数据 → 由 `opts.unit` 显式声明优先;未声明时 faijs 按历史\n * host 行为做启发式猜测(`guessStlUnit`,unified 方案 §6.1/§8 —— 3d_editor\n * 迁移回归的单位断言依赖此行为)。猜测收敛在 faijs 内(红线 R0:宿主不\n * 自实现单位换算),G0 = 折算后坐标,渲染 = 拓扑 = G0。\n * - 3MF `<model unit>` 由解析器读取并换算(micron/mm/cm/inch/foot/meter)。\n *\n * 返回契约(unit-system §10.6):`importFile` 返回 `{ shape, unit }` — `unit`\n * 是文件源单位(必然有值;faijs 是单一真源):3MF/STEP 取文件声明(STEP 无声明\n * → 'mm',与 OCCT 一致),STL 取 opts.unit 或 guessStlUnit 启发式猜测。坐标恒\n * 为基准值 — `unit` 只作元数据(sourceUnit 记录、导出目标判定),绝不参与几何\n * 缩放(读入折算已由内核/解析器完成,二次缩放 = 双重换算)。\n */\n\nimport * as THREE from 'three'\nimport { geoToManifoldMesh } from '../boolean/geo-convert'\nimport { parseStl } from './stl-loader'\nimport { parseThreemf, type ThreemfObject } from './threemf-loader'\nimport { parseBambu3mfFromEntries } from './threemf-bambu'\nimport { unitScale, type UnitName } from '../units'\nimport { guessStlUnit } from './stl-unit'\nimport type { Shape } from './types'\nimport type { PbrAppearance } from '../api/appearance'\nimport type { FileMeta } from '../api/meta'\nimport type { ImportModel } from './import-model'\n\n/** Result of a unit-aware file import. */\nexport interface ImportFileResult {\n shape: Shape\n /**\n * All parts the file declared, in declaration order (3MF `<build><item>` 序 /\n * STL 单零件)。`shape` is `parts[0]`(单零件兼容,过渡期)。\n * P0(2026-10-06-step-3mf-multipart-import-plan.md §5.4):多对象 3MF 不再折叠。\n */\n parts: Shape[]\n /**\n * The file's source unit (always present; faijs is the single source of truth).\n * 3MF/STEP: the file's declared unit (STEP 无声明 → 'mm', 与 OCCT 一致).\n * STL: opts.unit if declared, else the guessStlUnit heuristic result.\n * Metadata only — coordinates are already in the faijs base unit (mm).\n */\n unit: UnitName\n /**\n * Total parts the single file declared (e.g. 3MF `<build>` object count),\n * present only when > 1. Single-part mesh (STL, single-object 3MF) omits it.\n * 保留为 `parts.length`(>1 时);多零件不再折叠后该字段仅供旧宿主判断。\n */\n multiPartCount?: number\n /**\n * 整体级元数据(文件级;方案 §5.1 3MF `<model><metadata>`)。STL/STEP 视实现\n * 是否解析;无声明则省略。仅随导入结果上抛,不挂 Shape。\n */\n fileMeta?: FileMeta\n /**\n * 本次导入的结构数据(P0 §5.1):零件身份表 + Bambu 视图(3MF 分支填充)。\n * 单/多零件均登记(单零件 parts 长度 1)。\n */\n importModel?: ImportModel\n}\n\n/** SI prefixes that may precede .METRE. in a STEP SI_UNIT, → faijs UnitName. */\nconst SI_PREFIX_TO_UNIT: Record<string, UnitName> = {\n '.MILLI.': 'mm',\n '.CENTI.': 'cm',\n '$': 'm',\n}\n\n/** CONVERSION_BASED_UNIT names (uppercased) found in real-world STEP files. */\nconst CONVERSION_NAME_TO_UNIT: Record<string, UnitName> = {\n 'METRE': 'm',\n 'METER': 'm',\n 'MILLIMETRE': 'mm',\n 'MILLIMETER': 'mm',\n 'CENTIMETRE': 'cm',\n 'CENTIMETER': 'cm',\n 'MICROMETRE': 'micron',\n 'MICROMETER': 'micron',\n 'INCH': 'inch',\n 'FOOT': 'foot',\n}\n\n/**\n * Bounded scan window for `detectStepUnit`. Unit entities sit at the front of\n * the DATA section (right after the product/context blocks); 256 KiB covers\n * every real-world header+context preamble observed so far. Recomputable\n * constant — raise if a fixture ever misses.\n */\nexport const STEP_UNIT_SCAN_PREFIX = 256 * 1024\n\n/**\n * Parse the declared length unit from STEP text (unit-system §10.4).\n *\n * Unit entities live in the DATA section (not HEADER — ISO 10303-21 reserves\n * HEADER for FILE_DESCRIPTION/FILE_NAME/FILE_SCHEMA), near its start. We scan\n * a bounded prefix; the justification is \"unit entities sit at the front of\n * the DATA section\", not \"top of file\".\n *\n * Recognized forms, both attached to a LENGTH_UNIT():\n * - `SI_UNIT(<prefix>, .METRE.)` — prefix `.MILLI.`/`.CENTI.`/`.MICRO.` or `$`.\n * - `CONVERSION_BASED_UNIT('<NAME>', #n)` — inch/foot/etc.\n *\n * Metadata only — NEVER used to scale geometry (OCCT already normalizes on\n * read; re-scaling = double conversion).\n *\n * @param stepText - the full STEP file text to scan (only a bounded prefix is read).\n * @returns the faijs UnitName, or null when no length-unit declaration is found\n * (STEP permits omitting it; caller falls back to the base unit).\n */\nexport function detectStepUnit(stepText: string): UnitName | null {\n const head = stepText.slice(0, STEP_UNIT_SCAN_PREFIX)\n\n // 1) CONVERSION_BASED_UNIT('<NAME>', #n) — non-SI units (inch/foot/…).\n const conv = head.match(/CONVERSION_BASED_UNIT\\s*\\(\\s*'([^']+)'/i)\n if (conv) {\n const mapped = CONVERSION_NAME_TO_UNIT[conv[1].toUpperCase()]\n if (mapped) return mapped\n }\n\n // 2) SI_UNIT(<prefix>, .METRE.) — the .METRE. guard excludes angle entities\n // (they carry .RADIAN./.STERADIAN., never .METRE.).\n const si = head.match(/SI_UNIT\\s*\\(\\s*([^)]*?)\\s*,\\s*\\.METRE\\.\\s*\\)/i)\n if (si) {\n const prefix = si[1].trim().toUpperCase()\n if (prefix === '.MICRO.') return 'micron'\n const mapped = SI_PREFIX_TO_UNIT[prefix]\n if (mapped) return mapped\n }\n\n return null\n}\n\n/**\n * Load geometry from raw file bytes (headless variant).\n *\n * @param buffer - the raw file bytes to parse (already decompressed; stpz\n * unzipping is the caller's job — faijs never unzips implicitly).\n * @param format - format identifier such as 'stl', '3mf'/'threemf' or 'step'\n * (defaults to 'stl').\n * @param opts - optional settings; `unit` (a UnitName string) declares the\n * source unit for formats without unit metadata (e.g. STL). Defaults to\n * the guessStlUnit heuristic when omitted.\n * @returns the shape with base-unit coordinates plus the file's source unit\n * (`unit`, metadata only; always present — faijs is the single source of truth).\n */\nexport async function importFile(\n buffer: ArrayBuffer,\n format?: string,\n opts?: { unit?: UnitName },\n): Promise<ImportFileResult> {\n const fmt = (format ?? 'stl').toLowerCase()\n\n if (fmt === '3mf' || fmt === 'threemf') {\n const archive = await parseThreemf(buffer)\n // P0(方案 §5.4):不再折叠——返回全部 `<build><item>` 对象(declaration order,\n // 坐标已在 parseThreemf 里折算为 faijs 基准单位)。`shape` 保留为 parts[0]\n // (单零件兼容,过渡期)。\n if (archive.objects.length === 0) {\n throw new Error('[mesh/io] 3MF contains no objects')\n }\n // Bambu 元数据(盘号/挤出机/视图变换):有 Bambu 配置才产生内容,普通 3MF 返回空表。\n // P4:根 model XML 直传(3MF `<build>` 在 resources 前;id 两套编号经父\n // object components 关联——leafParts 表承载)。\n const bambu = parseBambu3mfFromEntries(archive.extraEntries, archive.modelXml)\n const parts = archive.objects.map(threemfObjectToShape)\n const multiPartCount = parts.length > 1 ? parts.length : undefined\n const importModel: ImportModel = {\n format: '3mf',\n unit: archive.unit,\n parts: archive.objects.map((obj, i) => {\n const part: ImportModel['parts'][number] = {\n index: i,\n name: obj.name ?? `imported:${i}`,\n }\n if (obj.baseColor) part.color = [obj.baseColor[0], obj.baseColor[1], obj.baseColor[2]]\n if (obj.meta) part.meta = obj.meta\n // Bambu 身份:3MF `<object id>`(叶子实例)与 Bambu objects 表(键为\n // model_settings 父对象 id)是两套编号——经父 object components 关联\n // (ThreemfObject.parentObjectId/componentIndex → bambu.leafParts)。\n const bm = obj.parentObjectId !== undefined\n ? bambu.leafParts.get(`${obj.parentObjectId}:${obj.componentIndex ?? 1}`)\n : undefined\n if (bm) {\n part.objectId = bm.objectId\n if (bm.plateId > 0) part.plateId = bm.plateId\n // Bambu extruder 1-based(默认 1):显式登记文件声明值\n part.extruder = bm.extruder\n part.partId = bm.partId\n }\n return part\n }),\n ...(archive.fileMeta ? { fileMeta: archive.fileMeta } : {}),\n }\n const bambuViews = buildBambuViews(bambu)\n if (bambuViews) importModel.bambuViews = bambuViews\n return {\n shape: parts[0],\n parts,\n unit: archive.unit,\n multiPartCount,\n importModel,\n // 整体级元数据(`<model><metadata>`)随导入结果上抛(方案 §5.1)。\n ...(archive.fileMeta ? { fileMeta: archive.fileMeta } : {}),\n }\n }\n\n if (fmt === 'stl') {\n const geo = parseStl(buffer)\n // STL 无单位声明:opts.unit 显式声明优先;未声明时做启发式猜测\n // (guessStlUnit —— 历史 host 行为,unified §6.1/§8 迁移基线)。猜测与\n // 折算都在 faijs 内完成(红线 R0),G0 即基准值,宿主不再二次换算。\n // unit 字段回传源单位(声明值或猜测值),供宿主记录 sourceUnit 元数据。\n let scale = 1\n let sourceUnit: UnitName = 'mm'\n const declared = opts?.unit\n if (declared) {\n sourceUnit = declared\n scale = unitScale(declared)\n } else {\n geo.computeBoundingBox()\n const bb = geo.boundingBox\n if (bb) {\n sourceUnit = guessStlUnit({\n min: [bb.min.x, bb.min.y, bb.min.z],\n max: [bb.max.x, bb.max.y, bb.max.z],\n })\n scale = unitScale(sourceUnit)\n }\n }\n if (scale !== 1) {\n const attr = geo.getAttribute('position') as THREE.BufferAttribute\n const arr = attr.array as Float32Array\n for (let i = 0; i < arr.length; i++) arr[i] *= scale\n }\n const shape = geoToManifoldMesh(geo)\n return { shape, parts: [shape], unit: sourceUnit }\n }\n\n // STEP goes through the BREP chain (loadBrep → OCCT, which normalizes to the\n // base unit on read). The mesh io path has no kernel-independent STEP parser\n // and must not fake one — a missing path is an error, not a fallback.\n throw new Error(`[mesh/io] unsupported format: ${fmt}`)\n}\n\n/** 单个 3MF `<build><item>` 对象 → Shape(P0:逐对象构造,替换原\"只取第一个\"收敛)。 */\nfunction threemfObjectToShape(obj: ThreemfObject): Shape {\n return {\n positions: obj.positions as Float32Array,\n indices: obj.indices as Uint32Array,\n // P2(方案 §5 3MF 行):对象级 baseColor → appearance.color(sRGB 原值,\n // 宽容兼容:无 baseColor 不带 appearance)。colorgroup(vertexColors)与\n // 逐三角形材质属 Phase 2(materialGroups / vertexColors),P2 不映射。\n ...(obj.baseColor ? { appearance: { color: [obj.baseColor[0], obj.baseColor[1], obj.baseColor[2]] } } : {}),\n // P4(方案 §5 3MF 行):逐三角形 basematerials(p1=p2=p3)→ materialGroups\n // (三角形区间 + appearance.color);无则不带。vertexColors(colorgroup)\n // 由上层(编辑器导入路径)消费,io 单零件收敛不展开为材质分组。\n ...(obj.materialGroups && obj.materialGroups.length > 0\n ? {\n materialGroups: obj.materialGroups.map((g) => ({\n start: g.start,\n count: g.count,\n appearance: { color: [g.color[0], g.color[1], g.color[2]] } as PbrAppearance,\n })),\n }\n : {}),\n // 零件级元数据(方案 §5.1 3MF 行):对象 name/partnumber/metadatagroup → meta。\n ...(obj.meta ? { meta: obj.meta } : {}),\n }\n}\n\n/** Bambu 显示视图数据 → ImportModel.bambuViews(无任何 Bambu 结构时省略)。 */\nfunction buildBambuViews(\n bambu: import('./threemf-bambu').Bambu3mfMetadata,\n): ImportModel['bambuViews'] | undefined {\n const plates = [...bambu.plates.keys()].sort((a, b) => a - b)\n if (plates.length === 0 && !bambu.assembleTransforms && !bambu.importTransforms) return undefined\n const views: NonNullable<ImportModel['bambuViews']> = { plates }\n if (bambu.assembleTransforms) {\n views.assembleTransforms = Object.fromEntries(\n [...bambu.assembleTransforms].map(([k, v]) => [k, { transform: v.transform, offset: v.offset }]),\n )\n }\n if (bambu.importTransforms) {\n views.importTransforms = Object.fromEntries(\n [...bambu.importTransforms].map(([k, v]) => [k, { matrix: v.matrix, sourceOffset: v.sourceOffset }]),\n )\n }\n // P4:完整消费面随结果回传——宿主重建 Bambu3mfMetadata 不再回读 archive。\n if (bambu.buildItems) {\n views.buildItems = bambu.buildItems.map((b) => ({\n objectId: b.objectId,\n transform: b.transform ?? null,\n }))\n }\n if (bambu.filamentColors.length > 0) views.filamentColors = bambu.filamentColors\n if (bambu.filamentTypes.length > 0) views.filamentTypes = bambu.filamentTypes\n if (bambu.parts.length > 0) {\n views.parts = bambu.parts.map((p) => ({\n partIndex: p.partIndex,\n objectId: p.objectId,\n partId: p.partId,\n name: p.name,\n extruder: p.extruder,\n plateId: p.plateId,\n }))\n }\n return views\n}\n"]}
1
+ {"version":3,"file":"io.js","sourceRoot":"","sources":["../../src/mesh/io.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * mesh IO API — headless 文件加载\n *\n * STL 由 core 自持的 `mesh/stl-loader.ts` 解析(零 three addons);3MF 由\n * `mesh/threemf-loader.ts` 解析(ZIP 解压 + XML,单位换算到 mm 基准)。\n * 坐标永远以 faijs 基准长度单位(mm)存储 — Shape 不贴 unit 标签(D3)。\n *\n * 单位换算边界(unit-system §5.2):\n * - 本模块只处理 **mesh 格式(STL/3MF)**,坐标可直接折算 → `opts.unit` 提供即\n * **强制设置模型的源单位**(直接指定,覆盖/忽略模型内部记录的单位——STL 无\n * 内部单位、3MF `<model unit>` 声明被覆盖),按 `unitScale(unit)` 折算坐标。\n * 它**只可能来自调用方的显式知识**(脚本作者手写 `cad.load({ file, unit })`、\n * 或宿主让用户选定单位后传入);调用方不知道单位时**必须省略**——省略时:\n * STL 走 faijs 启发式(`guessStlUnit`,unified 方案 §6.1/§8 —— 3d_editor\n * 迁移回归的单位断言依赖此行为)、3MF 走文件 `<model unit>`(无声明 →\n * millimeter)。猜测/折算收敛在 faijs 内(红线 R0:宿主不自实现单位换算),\n * G0 = 折算后坐标,渲染 = 拓扑 = G0。\n * - **STEP/.brep 不经过本模块**(走 BREP 链,load op 内 OCCT 读取):OCCT 按\n * 文件内部单位声明折算(SI_UNIT),**不支持强制设置单位**——load op 对\n * BREP 路径 + 用户显式 unit 直接**明确报错**(不缩放、不 workaround)。\n * - 3MF `<model unit>` 由解析器读取并换算(micron/mm/cm/inch/foot/meter)。\n *\n * 返回契约(unit-system §10.6):`importFile` 返回 `{ shape, unit }` — `unit`\n * 是文件源单位(必然有值;faijs 是单一真源):3MF 取 opts.unit(强制)或文件\n * 声明(无声明 → millimeter),STL 取 opts.unit 或 guessStlUnit 启发式猜测。\n * 坐标恒为基准值 — `unit` 只作元数据(sourceUnit 记录、导出目标判定),绝不\n * 参与几何缩放(读入折算已由解析器完成,二次缩放 = 双重换算)。\n */\n\nimport * as THREE from 'three'\nimport { geoToManifoldMesh } from '../boolean/geo-convert'\nimport { parseStl } from './stl-loader'\nimport { parseThreemf, type ThreemfObject, type ThreemfNode } from './threemf-loader'\nimport { parseBambu3mfFromEntries } from './threemf-bambu'\nimport { unitScale, type UnitName } from '../units'\nimport { guessStlUnit } from './stl-unit'\nimport type { Shape } from './types'\nimport type { CompoundShape } from '../shape'\nimport type { PbrAppearance } from '../api/appearance'\nimport type { FileMeta } from '../api/meta'\nimport type { ImportModel } from './import-model'\n\n/** Result of a unit-aware file import. */\nexport interface ImportFileResult {\n shape: Shape\n /**\n * All parts the file declared, in declaration order (3MF `<build><item>` 序 /\n * STL 单零件)。`shape` is `parts[0]`(单零件兼容,过渡期)。\n * P0(2026-10-06-step-3mf-multipart-import-plan.md §5.4):多对象 3MF 不再折叠。\n */\n parts: Shape[]\n /**\n * The file's source unit (always present; faijs is the single source of truth).\n * 3MF: opts.unit (forced) if provided, else the file's declared unit\n * (无声明 → millimeter). STL: opts.unit (forced) if provided, else the\n * guessStlUnit heuristic result. STEP/BREP 不经过本模块(OCCT 链不支持\n * 强制设置单位,load op 报错)。\n * Metadata only — coordinates are already in the faijs base unit (mm).\n */\n unit: UnitName\n /**\n * Total parts the single file declared (e.g. 3MF `<build>` object count),\n * present only when > 1. Single-part mesh (STL, single-object 3MF) omits it.\n * 保留为 `parts.length`(>1 时);多零件不再折叠后该字段仅供旧宿主判断。\n */\n multiPartCount?: number\n /**\n * 整体级元数据(文件级;方案 §5.1 3MF `<model><metadata>`)。STL/STEP 视实现\n * 是否解析;无声明则省略。仅随导入结果上抛,不挂 Shape。\n */\n fileMeta?: FileMeta\n /**\n * 本次导入的结构数据(P0 §5.1):零件身份表 + Bambu 视图(3MF 分支填充)。\n * 单/多零件均登记(单零件 parts 长度 1)。\n */\n importModel?: ImportModel\n /**\n * 装配层级(方案 2026-10-08 §2.1 / 第 5 步「导入归一」):当 3MF 含 `<components>`\n * 装配结构时,按层级重建为 `CompoundShape`,每个子节点携带相对父的 `transform`\n * (来自 3MF `<component transform>` / build `<item transform>`,行主序 3×4,平移在\n * 末列,与写出器同源)。仅当存在真实装配(多 item 或任一 item 带 components)时存在;\n * 单零件平铺导入不走它(`shape` / `parts` 已覆盖)。viewer 用 `parts`(已烘焙实例),\n * 往返/层级消费者用 `assembly`(局部几何 + 节点 transform)。\n */\n assembly?: Shape | CompoundShape\n}\n\n/** SI prefixes that may precede .METRE. in a STEP SI_UNIT, → faijs UnitName. */\nconst SI_PREFIX_TO_UNIT: Record<string, UnitName> = {\n '.MILLI.': 'mm',\n '.CENTI.': 'cm',\n '$': 'm',\n}\n\n/** CONVERSION_BASED_UNIT names (uppercased) found in real-world STEP files. */\nconst CONVERSION_NAME_TO_UNIT: Record<string, UnitName> = {\n 'METRE': 'm',\n 'METER': 'm',\n 'MILLIMETRE': 'mm',\n 'MILLIMETER': 'mm',\n 'CENTIMETRE': 'cm',\n 'CENTIMETER': 'cm',\n 'MICROMETRE': 'micron',\n 'MICROMETER': 'micron',\n 'INCH': 'inch',\n 'FOOT': 'foot',\n}\n\n/**\n * Bounded scan window for `detectStepUnit`. Unit entities sit at the front of\n * the DATA section (right after the product/context blocks); 256 KiB covers\n * every real-world header+context preamble observed so far. Recomputable\n * constant — raise if a fixture ever misses.\n */\nexport const STEP_UNIT_SCAN_PREFIX = 256 * 1024\n\n/**\n * Parse the declared length unit from STEP text (unit-system §10.4).\n *\n * Unit entities live in the DATA section (not HEADER — ISO 10303-21 reserves\n * HEADER for FILE_DESCRIPTION/FILE_NAME/FILE_SCHEMA), near its start. We scan\n * a bounded prefix; the justification is \"unit entities sit at the front of\n * the DATA section\", not \"top of file\".\n *\n * Recognized forms, both attached to a LENGTH_UNIT():\n * - `SI_UNIT(<prefix>, .METRE.)` — prefix `.MILLI.`/`.CENTI.`/`.MICRO.` or `$`.\n * - `CONVERSION_BASED_UNIT('<NAME>', #n)` — inch/foot/etc.\n *\n * Metadata only — NEVER used to scale geometry (OCCT already normalizes on\n * read; re-scaling = double conversion).\n *\n * @param stepText - the full STEP file text to scan (only a bounded prefix is read).\n * @returns the faijs UnitName, or null when no length-unit declaration is found\n * (STEP permits omitting it; caller falls back to the base unit).\n */\nexport function detectStepUnit(stepText: string): UnitName | null {\n const head = stepText.slice(0, STEP_UNIT_SCAN_PREFIX)\n\n // 1) CONVERSION_BASED_UNIT('<NAME>', #n) — non-SI units (inch/foot/…).\n const conv = head.match(/CONVERSION_BASED_UNIT\\s*\\(\\s*'([^']+)'/i)\n if (conv) {\n const mapped = CONVERSION_NAME_TO_UNIT[conv[1].toUpperCase()]\n if (mapped) return mapped\n }\n\n // 2) SI_UNIT(<prefix>, .METRE.) — the .METRE. guard excludes angle entities\n // (they carry .RADIAN./.STERADIAN., never .METRE.).\n const si = head.match(/SI_UNIT\\s*\\(\\s*([^)]*?)\\s*,\\s*\\.METRE\\.\\s*\\)/i)\n if (si) {\n const prefix = si[1].trim().toUpperCase()\n if (prefix === '.MICRO.') return 'micron'\n const mapped = SI_PREFIX_TO_UNIT[prefix]\n if (mapped) return mapped\n }\n\n return null\n}\n\n/**\n * Load geometry from raw file bytes (headless variant).\n *\n * @param buffer - the raw file bytes to parse (already decompressed; stpz\n * unzipping is the caller's job — faijs never unzips implicitly).\n * @param format - format identifier such as 'stl', '3mf'/'threemf' or 'step'\n * (defaults to 'stl'). **仅 mesh 格式(STL/3MF)走本模块**;STEP/BREP 走\n * BREP 链(load op 内 OCCT 读取,不支持强制设置单位)。\n * @param opts - optional settings; `unit` (a UnitName string) **force-sets the\n * model's source unit** (STL/3MF): it is NOT a default, it directly specifies\n * the unit and overrides/ignores any unit recorded inside the model (3MF\n * `<model unit>`; STL has none), geometry scaled by `unitScale(unit)`. Only\n * provide it when the caller actually knows the unit (author-written\n * `cad.load({ file, unit })`, or a host unit picker). When the caller does\n * NOT know the unit, it MUST omit `unit` — STL then guesses via\n * `guessStlUnit` (the only allowed guessing path), 3MF uses the file\n * declaration (none → millimeter). A wrong explicit unit scales geometry\n * wrongly and is never re-guessed.\n * @returns the shape with base-unit coordinates plus the file's source unit\n * (`unit`, metadata only; always present — faijs is the single source of truth).\n */\nexport async function importFile(\n buffer: ArrayBuffer,\n format?: string,\n opts?: { unit?: UnitName },\n): Promise<ImportFileResult> {\n const fmt = (format ?? 'stl').toLowerCase()\n\n if (fmt === '3mf' || fmt === 'threemf') {\n const archive = await parseThreemf(buffer, opts)\n // P0(方案 §5.4):不再折叠——返回全部 `<build><item>` 对象(declaration order,\n // 坐标已在 parseThreemf 里折算为 faijs 基准单位)。`shape` 保留为 parts[0]\n // (单零件兼容,过渡期)。\n if (archive.objects.length === 0) {\n throw new Error('[mesh/io] 3MF contains no objects')\n }\n // Bambu 元数据(盘号/挤出机/视图变换):有 Bambu 配置才产生内容,普通 3MF 返回空表。\n // P4:根 model XML 直传(3MF `<build>` 在 resources 前;id 两套编号经父\n // object components 关联——leafParts 表承载)。\n const bambu = parseBambu3mfFromEntries(archive.extraEntries, archive.modelXml)\n const parts = archive.objects.map(threemfObjectToShape)\n const multiPartCount = parts.length > 1 ? parts.length : undefined\n const importModel: ImportModel = {\n format: '3mf',\n unit: archive.unit,\n parts: archive.objects.map((obj, i) => {\n const part: ImportModel['parts'][number] = {\n index: i,\n name: obj.name ?? `imported:${i}`,\n }\n if (obj.baseColor) part.color = [obj.baseColor[0], obj.baseColor[1], obj.baseColor[2]]\n if (obj.meta) part.meta = obj.meta\n // Bambu 身份:3MF `<object id>`(叶子实例)与 Bambu objects 表(键为\n // model_settings 父对象 id)是两套编号——经父 object components 关联\n // (ThreemfObject.parentObjectId/componentIndex → bambu.leafParts)。\n const bm = obj.parentObjectId !== undefined\n ? bambu.leafParts.get(`${obj.parentObjectId}:${obj.componentIndex ?? 1}`)\n : undefined\n if (bm) {\n part.objectId = bm.objectId\n if (bm.plateId > 0) part.plateId = bm.plateId\n // Bambu extruder 1-based(默认 1):显式登记文件声明值\n part.extruder = bm.extruder\n part.partId = bm.partId\n }\n return part\n }),\n ...(archive.fileMeta ? { fileMeta: archive.fileMeta } : {}),\n }\n const bambuViews = buildBambuViews(bambu)\n if (bambuViews) importModel.bambuViews = bambuViews\n // 装配层级重建(方案 §2.1 / 第 5 步):含 `<components>` 时还原 CompoundShape,\n // 叶节点携带相对父 transform;单零件平铺导入不挂载。\n const assembly = archive.hierarchy && archive.localGeomById\n ? buildAssemblyFromArchive(archive.hierarchy, archive.localGeomById)\n : undefined\n return {\n shape: parts[0],\n parts,\n unit: archive.unit,\n multiPartCount,\n importModel,\n // 整体级元数据(`<model><metadata>`)随导入结果上抛(方案 §5.1)。\n ...(archive.fileMeta ? { fileMeta: archive.fileMeta } : {}),\n ...(assembly ? { assembly } : {}),\n }\n }\n\n if (fmt === 'stl') {\n const geo = parseStl(buffer)\n // STL 无单位声明:opts.unit 显式声明优先;未声明时做启发式猜测\n // (guessStlUnit —— 历史 host 行为,unified §6.1/§8 迁移基线)。猜测与\n // 折算都在 faijs 内完成(红线 R0),G0 即基准值,宿主不再二次换算。\n // unit 字段回传源单位(声明值或猜测值),供宿主记录 sourceUnit 元数据。\n let scale = 1\n let sourceUnit: UnitName = 'mm'\n const declared = opts?.unit\n if (declared) {\n sourceUnit = declared\n scale = unitScale(declared)\n } else {\n geo.computeBoundingBox()\n const bb = geo.boundingBox\n if (bb) {\n sourceUnit = guessStlUnit({\n min: [bb.min.x, bb.min.y, bb.min.z],\n max: [bb.max.x, bb.max.y, bb.max.z],\n })\n scale = unitScale(sourceUnit)\n }\n }\n if (scale !== 1) {\n const attr = geo.getAttribute('position') as THREE.BufferAttribute\n const arr = attr.array as Float32Array\n for (let i = 0; i < arr.length; i++) arr[i] *= scale\n }\n const shape = geoToManifoldMesh(geo)\n return { shape, parts: [shape], unit: sourceUnit }\n }\n\n // STEP goes through the BREP chain (loadBrep → OCCT, which normalizes to the\n // base unit on read). The mesh io path has no kernel-independent STEP parser\n // and must not fake one — a missing path is an error, not a fallback.\n throw new Error(`[mesh/io] unsupported format: ${fmt}`)\n}\n\n/** 单个 3MF `<build><item>` 对象 → Shape(P0:逐对象构造,替换原\"只取第一个\"收敛)。 */\nfunction threemfObjectToShape(obj: ThreemfObject): Shape {\n return {\n positions: obj.positions as Float32Array,\n indices: obj.indices as Uint32Array,\n // P2(方案 §5 3MF 行):对象级 baseColor → appearance.color(sRGB 原值,\n // 宽容兼容:无 baseColor 不带 appearance)。colorgroup(vertexColors)与\n // 逐三角形材质属 Phase 2(materialGroups / vertexColors),P2 不映射。\n ...(obj.baseColor ? { appearance: { color: [obj.baseColor[0], obj.baseColor[1], obj.baseColor[2]] } } : {}),\n // P4(方案 §5 3MF 行):逐三角形 basematerials(p1=p2=p3)→ materialGroups\n // (三角形区间 + appearance.color);无则不带。vertexColors(colorgroup)\n // 由上层(编辑器导入路径)消费,io 单零件收敛不展开为材质分组。\n ...(obj.materialGroups && obj.materialGroups.length > 0\n ? {\n materialGroups: obj.materialGroups.map((g) => ({\n start: g.start,\n count: g.count,\n appearance: { color: [g.color[0], g.color[1], g.color[2]] } as PbrAppearance,\n })),\n }\n : {}),\n // 零件级元数据(方案 §5.1 3MF 行):对象 name/partnumber/metadatagroup → meta。\n ...(obj.meta ? { meta: obj.meta } : {}),\n }\n}\n\n/**\n * 由 3MF 装配层级重建 `CompoundShape`(方案 §2.1 / 第 5 步「导入归一」)。\n *\n * 见 `ThreemfArchive.hierarchy` / `localGeomById`:节点带**相对父**位姿(行主序 3×4,\n * 平移在末列,与写出器 `transformToMatrix12` 同源);几何来自 `localGeomById` 的**未烘焙**\n * 局部网格。叶节点的最终 `Shape.transform` 即该相对位姿(写回 `Shape.transform`,供\n * 后续导出按 3MF `<component transform>` 原样还原——D1 往返)。容器节点落为\n * `CompoundShape`(不含 `transform` / `meta`,与 `shape.ts` 的 `CompoundShape` 形态一致)。\n *\n * 注:`shape.ts` 的 `CompoundShape.children` 当前声明为 `Shape[]`(非递归),故嵌套装配\n * 在类型层面暂不表达;运行时子节点可为子 `CompoundShape`(由 `isCompoundLike` 结构判定消费)。\n * 把 `CompoundShape` 改为递归类型是独立的后续重构(会波及 faijs-extra / cad-runtime),本步\n * 以最小改动桥接——见下方 `children` 处的收窄断言。\n *\n * 仅在存在真实装配(多 item 或任一 item 带 components)时返回非空;单零件平铺导入返回\n * `undefined`(由调用方决定是否挂载到 `result.assembly`,避免改变单零件结果形状)。\n */\nfunction buildAssemblyFromArchive(\n hierarchy: ThreemfNode[],\n localGeomById: Map<number, ThreemfObject>,\n): Shape | CompoundShape | undefined {\n const hasHierarchy = hierarchy.length > 1 || hierarchy.some((n) => (n.children?.length ?? 0) > 0)\n if (!hasHierarchy) return undefined\n\n const buildNode = (node: ThreemfNode): Shape | CompoundShape => {\n const obj = localGeomById.get(node.objectId)\n if (node.children && node.children.length > 0) {\n // 收窄断言:共享 CompoundShape 非递归声明,运行时子节点可含子 CompoundShape。\n return { kind: 'compound', children: node.children.map(buildNode) as Shape[] }\n }\n // 叶节点:局部几何 + 相对父位姿。\n if (!obj || !obj.positions || !obj.indices) {\n return { kind: 'compound', children: [] }\n }\n const leaf = threemfObjectToShape(obj)\n if (node.transform) {\n ;(leaf as Shape & { transform?: unknown }).transform = { matrix: Array.from(node.transform) }\n }\n return leaf\n }\n\n return hierarchy.length === 1\n ? buildNode(hierarchy[0])\n : { kind: 'compound', children: hierarchy.map(buildNode) as Shape[] }\n}\n\n/** Bambu 显示视图数据 → ImportModel.bambuViews(无任何 Bambu 结构时省略)。 */\nfunction buildBambuViews(\n bambu: import('./threemf-bambu').Bambu3mfMetadata,\n): ImportModel['bambuViews'] | undefined {\n const plates = [...bambu.plates.keys()].sort((a, b) => a - b)\n if (plates.length === 0 && !bambu.assembleTransforms && !bambu.importTransforms) return undefined\n const views: NonNullable<ImportModel['bambuViews']> = { plates }\n if (bambu.assembleTransforms) {\n views.assembleTransforms = Object.fromEntries(\n [...bambu.assembleTransforms].map(([k, v]) => [k, { transform: v.transform, offset: v.offset }]),\n )\n }\n if (bambu.importTransforms) {\n views.importTransforms = Object.fromEntries(\n [...bambu.importTransforms].map(([k, v]) => [k, { matrix: v.matrix, sourceOffset: v.sourceOffset }]),\n )\n }\n // P4:完整消费面随结果回传——宿主重建 Bambu3mfMetadata 不再回读 archive。\n if (bambu.buildItems) {\n views.buildItems = bambu.buildItems.map((b) => ({\n objectId: b.objectId,\n transform: b.transform ?? null,\n }))\n }\n if (bambu.filamentColors.length > 0) views.filamentColors = bambu.filamentColors\n if (bambu.filamentTypes.length > 0) views.filamentTypes = bambu.filamentTypes\n if (bambu.parts.length > 0) {\n views.parts = bambu.parts.map((p) => ({\n partIndex: p.partIndex,\n objectId: p.objectId,\n partId: p.partId,\n name: p.name,\n extruder: p.extruder,\n plateId: p.plateId,\n }))\n }\n return views\n}\n"]}
@@ -56,12 +56,38 @@ export interface ThreemfObject {
56
56
  */
57
57
  meta?: ShapeMeta;
58
58
  }
59
+ /**
60
+ * 装配层级节点(方案 2026-10-08 §2.1 / 第 5 步「导入归一」):一个 `<build><item>`
61
+ * 或 `<components><component>` 引用,携带**相对父**的位姿(3MF `<component transform>`
62
+ * 或 build `<item transform>`)。`children` 递归展开嵌套装配。几何由 `localGeomById`
63
+ * 按 `objectId` 取未烘焙的局部网格(与 `objects` 的已烘焙实例不同轴:assembly 走
64
+ * 局部几何 + 节点 transform,viewer 走 `objects` 的已烘焙实例)。
65
+ */
66
+ export interface ThreemfNode {
67
+ /** 引用的 `<object id>`(必为 `<resources>` 中的对象)。 */
68
+ objectId: number;
69
+ /** 相对父的位姿(行主序 3×4,平移在末列,与写出器 `transformToMatrix12` 同源);无则缺省。 */
70
+ transform?: readonly number[] | null;
71
+ /** 子节点(嵌套装配);叶节点缺省。 */
72
+ children?: ThreemfNode[];
73
+ }
59
74
  /** Structured 3MF parse result. */
60
75
  export interface ThreemfArchive {
61
76
  /** The declared `<model unit>` mapped to a faijs UnitName. */
62
77
  unit: UnitName;
63
78
  /** One object per build `<item>` (transform baked), or per `<resources>` object. */
64
79
  objects: ThreemfObject[];
80
+ /**
81
+ * 装配层级(方案 §2.1 / 第 5 步):`<build>` 顶层 item + 其 `<components>` 递归展开,
82
+ * 节点带**相对父**位姿。仅当文件含装配结构(多 item 或任一 item 带 components)时存在。
83
+ */
84
+ hierarchy?: ThreemfNode[];
85
+ /**
86
+ * 未烘焙的局部几何,按 `<object id>` 索引(mesh 对象 carrying positions/indices;
87
+ * 组件容器只带 meta)。assembly 重建走它 + `hierarchy` 的节点 transform,与 `objects`
88
+ * 的已烘焙实例互不干扰。
89
+ */
90
+ localGeomById?: Map<number, ThreemfObject>;
65
91
  /** Every non-model archive entry (key → raw bytes) for downstream consumers. */
66
92
  extraEntries: Map<string, Uint8Array>;
67
93
  /** P4:根 model 文档(3D/3dmodel.model)XML——Bambu 层解析 build/组件结构用。 */
@@ -78,9 +104,17 @@ export interface ThreemfArchive {
78
104
  * (sub-models first, root last — root wins collisions).
79
105
  *
80
106
  * @param buffer - the raw .3mf (ZIP) bytes.
81
- * @returns an `ThreemfArchive` with mm-base per-instance objects, the declared
82
- * source unit, and every non-model archive entry.
83
- * @throws when the buffer is not a 3MF/ZIP archive or the unit is invalid.
107
+ * @param opts - optional settings; `unit`(用户显式指定)**强制设置模型源单位**:
108
+ * 覆盖/忽略文件内部 `<model unit>` 声明(unit 对所有格式一视同仁,unit-system
109
+ * §5.2);未提供时读文件声明。无声明 → millimeter。
110
+ * @returns an `ThreemfArchive` with mm-base per-instance objects, the source
111
+ * unit (forced value when provided, else the file declaration), and every
112
+ * non-model archive entry.
113
+ * @throws when the buffer is not a 3MF/ZIP archive or the unit is invalid
114
+ * (no forced unit → an unsupported declaration throws; forced unit skips
115
+ * declaration parsing entirely).
84
116
  */
85
- export declare function parseThreemf(buffer: ArrayBuffer): Promise<ThreemfArchive>;
117
+ export declare function parseThreemf(buffer: ArrayBuffer, opts?: {
118
+ unit?: UnitName;
119
+ }): Promise<ThreemfArchive>;
86
120
  //# sourceMappingURL=threemf-loader.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"threemf-loader.d.ts","sourceRoot":"","sources":["../../src/mesh/threemf-loader.ts"],"names":[],"mappings":"AA4BA,OAAO,EAAc,KAAK,QAAQ,EAAE,MAAM,UAAU,CAAA;AACpD,OAAO,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AAyHtD,8EAA8E;AAC9E,MAAM,WAAW,aAAa;IAC5B,qEAAqE;IACrE,EAAE,EAAE,MAAM,CAAA;IACV;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,uEAAuE;IACvE,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,YAAY,CAAA;IACvB,OAAO,EAAE,WAAW,CAAA;IACpB;;;;;;OAMG;IACH,SAAS,CAAC,EAAE,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;IAC7C;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,YAAY,CAAA;IAC3B;;;;;;;OAOG;IACH,cAAc,CAAC,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,CAAC,CAAA;IACzF;;;;;OAKG;IACH,IAAI,CAAC,EAAE,SAAS,CAAA;CACjB;AAED,mCAAmC;AACnC,MAAM,WAAW,cAAc;IAC7B,8DAA8D;IAC9D,IAAI,EAAE,QAAQ,CAAA;IACd,oFAAoF;IACpF,OAAO,EAAE,aAAa,EAAE,CAAA;IACxB,gFAAgF;IAChF,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAA;IACrC,iEAAiE;IACjE,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,QAAQ,CAAA;CACpB;AAwpBD;;;;;;;;;;;;GAYG;AACH,wBAAsB,YAAY,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,cAAc,CAAC,CAwC/E"}
1
+ {"version":3,"file":"threemf-loader.d.ts","sourceRoot":"","sources":["../../src/mesh/threemf-loader.ts"],"names":[],"mappings":"AA4BA,OAAO,EAAc,KAAK,QAAQ,EAAE,MAAM,UAAU,CAAA;AACpD,OAAO,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AAkItD,8EAA8E;AAC9E,MAAM,WAAW,aAAa;IAC5B,qEAAqE;IACrE,EAAE,EAAE,MAAM,CAAA;IACV;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,uEAAuE;IACvE,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,YAAY,CAAA;IACvB,OAAO,EAAE,WAAW,CAAA;IACpB;;;;;;OAMG;IACH,SAAS,CAAC,EAAE,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;IAC7C;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,YAAY,CAAA;IAC3B;;;;;;;OAOG;IACH,cAAc,CAAC,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,CAAC,CAAA;IACzF;;;;;OAKG;IACH,IAAI,CAAC,EAAE,SAAS,CAAA;CACjB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,WAAW;IAC1B,gDAAgD;IAChD,QAAQ,EAAE,MAAM,CAAA;IAChB,gEAAgE;IAChE,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAAA;IACpC,uBAAuB;IACvB,QAAQ,CAAC,EAAE,WAAW,EAAE,CAAA;CACzB;AAED,mCAAmC;AACnC,MAAM,WAAW,cAAc;IAC7B,8DAA8D;IAC9D,IAAI,EAAE,QAAQ,CAAA;IACd,oFAAoF;IACpF,OAAO,EAAE,aAAa,EAAE,CAAA;IACxB;;;OAGG;IACH,SAAS,CAAC,EAAE,WAAW,EAAE,CAAA;IACzB;;;;OAIG;IACH,aAAa,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,CAAA;IAC1C,gFAAgF;IAChF,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAA;IACrC,iEAAiE;IACjE,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,QAAQ,CAAA;CACpB;AA4tBD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,YAAY,CAChC,MAAM,EAAE,WAAW,EACnB,IAAI,CAAC,EAAE;IAAE,IAAI,CAAC,EAAE,QAAQ,CAAA;CAAE,GACzB,OAAO,CAAC,cAAc,CAAC,CA6CzB"}
@@ -46,44 +46,53 @@ function hexToRgb(hex) {
46
46
  return null;
47
47
  return [parseInt(m[1], 16) / 255, parseInt(m[2], 16) / 255, parseInt(m[3], 16) / 255];
48
48
  }
49
- /** Bake a column-major ST_Matrix3D (translation = last three tokens) into positions. */
49
+ /**
50
+ * Bake a 3MF ST_Matrix3D into positions.
51
+ *
52
+ * 3MF 约定(Core spec §4.3.1):12 元组 **行主序** 3×4,平移在**末列**——token
53
+ * 序为 `m00 m01 m02 tx | m10 m11 m12 ty | m20 m21 m22 tz`,平移 = token[3]/[7]/[11]。
54
+ * 与写出器 `export-model.ts#transformToMatrix12`(同一行主序约定)必须同源,否则
55
+ * 写出 → 读回的 transform 会被旋转/平移错位(步骤 5 修复:此前本函数误用列主序)。
56
+ */
50
57
  function bakeTransform(positions, m) {
51
- const [a, b, c, d, e, f, g, h, iz, p, q, r] = m;
58
+ const [a, b, c, d, e, f, g, h, i, j, k, l] = m;
52
59
  const out = new Float32Array(positions.length);
53
60
  for (let v = 0; v < positions.length; v += 3) {
54
61
  const x = positions[v];
55
62
  const y = positions[v + 1];
56
63
  const z = positions[v + 2];
57
- out[v] = a * x + d * y + g * z + p;
58
- out[v + 1] = b * x + e * y + h * z + q;
59
- out[v + 2] = c * x + f * y + iz * z + r;
64
+ out[v] = a * x + b * y + c * z + d;
65
+ out[v + 1] = e * x + f * y + g * z + h;
66
+ out[v + 2] = i * x + j * y + k * z + l;
60
67
  }
61
68
  return out;
62
69
  }
63
70
  /**
64
- * Compose two ST_Matrix3D column-major transforms (12 tokens each, same
65
- * layout as `bakeTransform`: `[a b c, d e f, g h i, p q r]`, translation last).
71
+ * Compose two 3MF ST_Matrix3D transforms (12 tokens each, **row-major** 3×4,
72
+ * translation in the last column — same layout as `bakeTransform`).
66
73
  *
67
74
  * Semantics: the child transform is applied first, then the parent —
68
75
  * `M = parent · child` (the child lives in the parent's space, exactly like
69
- * three.js `clone.applyMatrix4(parent).applyMatrix4(child)`).
76
+ * three.js `clone.applyMatrix4(parent).applyMatrix4(child)`). For the 3×3 part
77
+ * `M_rot = parent_rot · child_rot`; for the translation
78
+ * `M_t = parent_rot · child_t + parent_t`.
70
79
  */
71
80
  function composeTransform(parent, child) {
72
- const [a1, b1, c1, d1, e1, f1, g1, h1, i1, p1, q1, r1] = parent;
73
- const [a2, b2, c2, d2, e2, f2, g2, h2, i2, p2, q2, r2] = child;
81
+ const [a1, b1, c1, d1, e1, f1, g1, h1, i1, j1, k1, l1] = parent;
82
+ const [a2, b2, c2, d2, e2, f2, g2, h2, i2, j2, k2, l2] = child;
74
83
  return [
75
- a1 * a2 + d1 * b2 + g1 * c2,
76
- b1 * a2 + e1 * b2 + h1 * c2,
77
- c1 * a2 + f1 * b2 + i1 * c2,
78
- a1 * d2 + d1 * e2 + g1 * f2,
79
- b1 * d2 + e1 * e2 + h1 * f2,
80
- c1 * d2 + f1 * e2 + i1 * f2,
81
- a1 * g2 + d1 * h2 + g1 * i2,
82
- b1 * g2 + e1 * h2 + h1 * i2,
83
- c1 * g2 + f1 * h2 + i1 * i2,
84
- a1 * p2 + d1 * q2 + g1 * r2 + p1,
85
- b1 * p2 + e1 * q2 + h1 * r2 + q1,
86
- c1 * p2 + f1 * q2 + i1 * r2 + r1,
84
+ a1 * a2 + b1 * e2 + c1 * i2,
85
+ a1 * b2 + b1 * f2 + c1 * j2,
86
+ a1 * c2 + b1 * g2 + c1 * k2,
87
+ a1 * d2 + b1 * h2 + c1 * l2 + d1,
88
+ e1 * a2 + f1 * e2 + g1 * i2,
89
+ e1 * b2 + f1 * f2 + g1 * j2,
90
+ e1 * c2 + f1 * g2 + g1 * k2,
91
+ e1 * d2 + f1 * h2 + g1 * l2 + h1,
92
+ i1 * a2 + j1 * e2 + k1 * i2,
93
+ i1 * b2 + j1 * f2 + k1 * j2,
94
+ i1 * c2 + j1 * g2 + k1 * k2,
95
+ i1 * d2 + j1 * h2 + k1 * l2 + l1,
87
96
  ];
88
97
  }
89
98
  /**
@@ -481,15 +490,26 @@ function parseModelMetadata(rootDoc) {
481
490
  *
482
491
  * `unit` is read from the ROOT model document (the file the build items
483
492
  * belong to); all documents share the same declared unit in practice.
493
+ * `forcedUnit`(opts.unit,用户显式指定)时**覆盖/忽略文件内部声明**——unit
494
+ * 参数对所有格式一视同仁:直接指定模型的源单位(unit-system §5.2),
495
+ * 未提供才回落到文件声明。
484
496
  */
485
- function parseModelXml(docTexts) {
497
+ function parseModelXml(docTexts, forcedUnit) {
486
498
  // Last document = root model (caller appends it last).
487
499
  const rootDoc = docTexts[docTexts.length - 1];
488
- const unitMatch = /<model\b[^>]*\bunit="([^"]+)"/.exec(rootDoc);
489
- const unit = unitMatch ? unitMatch[1] : 'millimeter';
490
- const unitName = threemfUnitToFaijs(unit);
491
- if (unitName === null) {
492
- throw new Error(`[mesh/threemf] unsupported <model unit>: ${JSON.stringify(unit)}`);
500
+ let unitName;
501
+ if (forcedUnit) {
502
+ // 强制设置模型单位:跳过文件声明(含非法声明)——用户显式指定优先。
503
+ unitName = forcedUnit;
504
+ }
505
+ else {
506
+ const unitMatch = /<model\b[^>]*\bunit="([^"]+)"/.exec(rootDoc);
507
+ const unit = unitMatch ? unitMatch[1] : 'millimeter';
508
+ const mapped = threemfUnitToFaijs(unit);
509
+ if (mapped === null) {
510
+ throw new Error(`[mesh/threemf] unsupported <model unit>: ${JSON.stringify(unit)}`);
511
+ }
512
+ unitName = mapped;
493
513
  }
494
514
  const scale = UNIT_SCALE[unitName];
495
515
  // 整体级元数据(`<model>` 直接子级 `<metadata>`;well-known 名映射到
@@ -756,10 +776,60 @@ function parseModelXml(docTexts) {
756
776
  out.push(...materialize(src, undefined, new Set([src.id])));
757
777
  }
758
778
  }
779
+ // 装配层级 + 局部几何(方案 §2.1 / 第 5 步「导入归一」)。
780
+ // `hierarchy`:build 顶层 item + 其 `<components>` 递归,节点带**相对父**位姿
781
+ // (行主序 3×4,平移末列——与写出器 `transformToMatrix12` 同源)。`localGeomById`:
782
+ // 未烘焙的局部几何(mesh 对象),供 assembly 重建(与 `out` 的已烘焙实例不同轴)。
783
+ const localGeomById = new Map();
784
+ for (const src of byObjectId.values()) {
785
+ if (src.positions && src.indices) {
786
+ localGeomById.set(src.id, materializeMesh(src, undefined));
787
+ }
788
+ else if (src.components && src.components.length > 0) {
789
+ localGeomById.set(src.id, { id: src.id, name: src.name, positions: new Float32Array(), indices: new Uint32Array() });
790
+ }
791
+ }
792
+ const nodeOf = (objId, tf, visited) => {
793
+ const src = byObjectId.get(objId);
794
+ const node = { objectId: objId };
795
+ if (tf)
796
+ node.transform = tf;
797
+ if (src?.components && src.components.length > 0) {
798
+ // 路径守卫:组件回指祖先(含自指)即时剪枝,避免无限递归(测试「component
799
+ // reference cycles terminate」)。被剪的重复引用不再展开为子节点。
800
+ const next = new Set(visited);
801
+ next.add(objId);
802
+ node.children = src.components
803
+ .filter((c) => !next.has(c.objectId))
804
+ .map((c) => nodeOf(c.objectId, c.transform ?? null, next));
805
+ }
806
+ return node;
807
+ };
808
+ const roots = [];
809
+ let hi;
810
+ const hItemRe = /<item\b[^>]*>/g;
811
+ while ((hi = hItemRe.exec(buildSec)) !== null) {
812
+ const objId = Number(/(?:^|\s)objectid="([^"]+)"/.exec(hi[0])?.[1]);
813
+ if (!byObjectId.has(objId))
814
+ continue;
815
+ const tf = parseTransformAttr(/\btransform="([^"]*)"/.exec(hi[0])?.[1] ?? null);
816
+ roots.push(nodeOf(objId, tf, new Set()));
817
+ }
818
+ if (roots.length === 0) {
819
+ for (const src of byObjectId.values())
820
+ roots.push(nodeOf(src.id, null, new Set()));
821
+ }
822
+ const hierarchy = roots;
759
823
  if (out.length === 0) {
760
824
  throw new Error('[mesh/threemf] no buildable geometry found in 3dmodel.model');
761
825
  }
762
- return { unitName, objects: out, ...(fileMeta ? { fileMeta } : {}) };
826
+ return {
827
+ unitName,
828
+ objects: out,
829
+ hierarchy,
830
+ localGeomById,
831
+ ...(fileMeta ? { fileMeta } : {}),
832
+ };
763
833
  }
764
834
  /**
765
835
  * Parse 3MF bytes into a structured archive.
@@ -770,11 +840,17 @@ function parseModelXml(docTexts) {
770
840
  * (sub-models first, root last — root wins collisions).
771
841
  *
772
842
  * @param buffer - the raw .3mf (ZIP) bytes.
773
- * @returns an `ThreemfArchive` with mm-base per-instance objects, the declared
774
- * source unit, and every non-model archive entry.
775
- * @throws when the buffer is not a 3MF/ZIP archive or the unit is invalid.
843
+ * @param opts - optional settings; `unit`(用户显式指定)**强制设置模型源单位**:
844
+ * 覆盖/忽略文件内部 `<model unit>` 声明(unit 对所有格式一视同仁,unit-system
845
+ * §5.2);未提供时读文件声明。无声明 → millimeter。
846
+ * @returns an `ThreemfArchive` with mm-base per-instance objects, the source
847
+ * unit (forced value when provided, else the file declaration), and every
848
+ * non-model archive entry.
849
+ * @throws when the buffer is not a 3MF/ZIP archive or the unit is invalid
850
+ * (no forced unit → an unsupported declaration throws; forced unit skips
851
+ * declaration parsing entirely).
776
852
  */
777
- export async function parseThreemf(buffer) {
853
+ export async function parseThreemf(buffer, opts) {
778
854
  let entries;
779
855
  try {
780
856
  entries = readZipEntries(new Uint8Array(buffer));
@@ -796,7 +872,7 @@ export async function parseThreemf(buffer) {
796
872
  }
797
873
  docKeys.push(rootKey);
798
874
  const docTexts = docKeys.map((k) => new TextDecoder().decode(entries.get(k)));
799
- const parsed = parseModelXml(docTexts);
875
+ const parsed = parseModelXml(docTexts, opts?.unit);
800
876
  // `extraEntries` = everything that is NOT a model document — Bambu's
801
877
  // `Metadata/model_settings.config`, `Metadata/project_settings.config`,
802
878
  // `Metadata/thumbnail.png`, etc.
@@ -809,6 +885,11 @@ export async function parseThreemf(buffer) {
809
885
  return {
810
886
  unit: parsed.unitName,
811
887
  objects: parsed.objects,
888
+ // 装配层级 + 局部几何(方案 §2.1 / 第 5 步「导入归一」):3MF `<components>`
889
+ // 重建 CompoundShape 时需要的相对父位姿树与未烘焙局部几何(与 `objects` 的
890
+ // 已烘焙实例不同轴)。必须透传,否则 importFile 拿不到 assembly。
891
+ ...(parsed.hierarchy ? { hierarchy: parsed.hierarchy } : {}),
892
+ ...(parsed.localGeomById ? { localGeomById: parsed.localGeomById } : {}),
812
893
  extraEntries,
813
894
  // P4:根 model XML(Bambu 层解析 build items / 父对象 components 需要;
814
895
  // 根 model 不在 extraEntries——extraEntries 只收非 model 条目)。