@things-factory/figure-ui 10.1.75 → 10.1.77

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 (282) hide show
  1. package/client/figure-assistant.ts +3 -3
  2. package/client/graphql/{v3-store.ts → store.ts} +6 -6
  3. package/client/modeller/{v3-ai-proposal.ts → ai-proposal.ts} +8 -8
  4. package/client/modeller/{v3-align.ts → align.ts} +14 -14
  5. package/client/modeller/{v3-canvas.ts → canvas.ts} +13 -13
  6. package/client/modeller/{v3-clips.ts → clips.ts} +5 -5
  7. package/client/modeller/{v3-controls.ts → controls.ts} +2 -2
  8. package/client/modeller/{v3-editor.ts → editor.ts} +117 -117
  9. package/client/modeller/{figure-v3-capability.ts → figure-capability-panel.ts} +7 -7
  10. package/client/modeller/{figure-v3-clips.ts → figure-clips.ts} +10 -10
  11. package/client/modeller/{figure-v3-controls.ts → figure-controls.ts} +9 -9
  12. package/client/modeller/{v3-figure-facts.ts → figure-facts.ts} +11 -11
  13. package/client/modeller/{figure-v3-first-save.ts → figure-first-save.ts} +3 -3
  14. package/client/modeller/figure-history-panel.ts +1 -1
  15. package/client/modeller/{figure-v3-links.ts → figure-links.ts} +35 -35
  16. package/client/modeller/{figure-v3-material.ts → figure-material-panel.ts} +20 -20
  17. package/client/modeller/{figure-v3-parts.ts → figure-parts.ts} +15 -15
  18. package/client/modeller/{figure-v3-preview.ts → figure-preview.ts} +14 -14
  19. package/client/modeller/{figure-v3-properties.ts → figure-properties.ts} +25 -25
  20. package/client/modeller/{figure-v3-recipe-catalog.ts → figure-recipe-catalog.ts} +10 -10
  21. package/client/modeller/{figure-v3-verdict.ts → figure-verdict.ts} +24 -24
  22. package/client/modeller/gate-findings.ts +2 -2
  23. package/client/modeller/{v3-gizmo.ts → gizmo.ts} +11 -11
  24. package/client/modeller/{v3-joint-drag.ts → joint-drag.ts} +11 -11
  25. package/client/modeller/{v3-joint-fan.ts → joint-fan.ts} +13 -13
  26. package/client/modeller/{v3-links.ts → links.ts} +59 -59
  27. package/client/modeller/{v3-live.ts → live.ts} +9 -9
  28. package/client/modeller/{v3-maturity.ts → maturity.ts} +65 -65
  29. package/client/modeller/modeller-route.ts +1 -1
  30. package/client/modeller/{v3-occupancy.ts → occupancy.ts} +6 -6
  31. package/client/modeller/{v3-palette.ts → palette.ts} +3 -3
  32. package/client/modeller/{v3-preview-model.ts → preview-model.ts} +8 -8
  33. package/client/modeller/{v3-product-session.ts → product-session.ts} +25 -25
  34. package/client/modeller/{v3-recipe-source.ts → recipe-source.ts} +2 -2
  35. package/client/modeller/{v3-recipes.ts → recipes.ts} +9 -9
  36. package/client/modeller/{v3-renderer.ts → renderer.ts} +16 -16
  37. package/client/modeller/{v3-stage-model.ts → stage-model.ts} +15 -15
  38. package/client/modeller/{v3-thumbnail.ts → thumbnail.ts} +8 -8
  39. package/client/pages/figure-list-page.ts +4 -4
  40. package/client/pages/{figure-v3-modeller-page.ts → figure-modeller-page.ts} +37 -37
  41. package/client/route.ts +1 -5
  42. package/dist-client/figure-assistant.js +3 -3
  43. package/dist-client/figure-assistant.js.map +1 -1
  44. package/dist-client/graphql/store.d.ts +3 -0
  45. package/dist-client/graphql/{v3-store.js → store.js} +6 -6
  46. package/dist-client/graphql/store.js.map +1 -0
  47. package/dist-client/modeller/{v3-ai-proposal.d.ts → ai-proposal.d.ts} +7 -7
  48. package/dist-client/modeller/{v3-ai-proposal.js → ai-proposal.js} +1 -1
  49. package/dist-client/modeller/ai-proposal.js.map +1 -0
  50. package/dist-client/modeller/{v3-align.d.ts → align.d.ts} +8 -8
  51. package/dist-client/modeller/{v3-align.js → align.js} +6 -6
  52. package/dist-client/modeller/align.js.map +1 -0
  53. package/dist-client/modeller/{v3-canvas.d.ts → canvas.d.ts} +7 -7
  54. package/dist-client/modeller/{v3-canvas.js → canvas.js} +20 -20
  55. package/dist-client/modeller/canvas.js.map +1 -0
  56. package/dist-client/modeller/{v3-clips.d.ts → clips.d.ts} +4 -4
  57. package/dist-client/modeller/{v3-clips.js → clips.js} +1 -1
  58. package/dist-client/modeller/clips.js.map +1 -0
  59. package/dist-client/modeller/{v3-controls.d.ts → controls.d.ts} +2 -2
  60. package/dist-client/modeller/{v3-controls.js → controls.js} +1 -1
  61. package/dist-client/modeller/controls.js.map +1 -0
  62. package/dist-client/modeller/{v3-editor.d.ts → editor.d.ts} +19 -19
  63. package/dist-client/modeller/{v3-editor.js → editor.js} +103 -103
  64. package/dist-client/modeller/editor.js.map +1 -0
  65. package/dist-client/modeller/{v3-fields.js → fields.js} +1 -1
  66. package/dist-client/modeller/fields.js.map +1 -0
  67. package/dist-client/modeller/{figure-v3-capability.d.ts → figure-capability-panel.d.ts} +4 -4
  68. package/dist-client/modeller/{figure-v3-capability.js → figure-capability-panel.js} +12 -12
  69. package/dist-client/modeller/figure-capability-panel.js.map +1 -0
  70. package/dist-client/modeller/{figure-v3-clips.d.ts → figure-clips.d.ts} +4 -4
  71. package/dist-client/modeller/{figure-v3-clips.js → figure-clips.js} +16 -16
  72. package/dist-client/modeller/figure-clips.js.map +1 -0
  73. package/dist-client/modeller/{figure-v3-controls.d.ts → figure-controls.d.ts} +5 -5
  74. package/dist-client/modeller/{figure-v3-controls.js → figure-controls.js} +16 -16
  75. package/dist-client/modeller/figure-controls.js.map +1 -0
  76. package/dist-client/modeller/{v3-figure-facts.d.ts → figure-facts.d.ts} +5 -5
  77. package/dist-client/modeller/{v3-figure-facts.js → figure-facts.js} +5 -5
  78. package/dist-client/modeller/figure-facts.js.map +1 -0
  79. package/dist-client/modeller/{figure-v3-first-save.d.ts → figure-first-save.d.ts} +1 -1
  80. package/dist-client/modeller/{figure-v3-first-save.js → figure-first-save.js} +11 -11
  81. package/dist-client/modeller/figure-first-save.js.map +1 -0
  82. package/dist-client/modeller/figure-history-panel.d.ts +1 -1
  83. package/dist-client/modeller/figure-history-panel.js.map +1 -1
  84. package/dist-client/modeller/{figure-v3-links.d.ts → figure-links.d.ts} +5 -5
  85. package/dist-client/modeller/{figure-v3-links.js → figure-links.js} +24 -24
  86. package/dist-client/modeller/figure-links.js.map +1 -0
  87. package/dist-client/modeller/{figure-v3-material.d.ts → figure-material-panel.d.ts} +9 -9
  88. package/dist-client/modeller/{figure-v3-material.js → figure-material-panel.js} +19 -19
  89. package/dist-client/modeller/figure-material-panel.js.map +1 -0
  90. package/dist-client/modeller/{figure-v3-parts.d.ts → figure-parts.d.ts} +12 -12
  91. package/dist-client/modeller/{figure-v3-parts.js → figure-parts.js} +14 -14
  92. package/dist-client/modeller/figure-parts.js.map +1 -0
  93. package/dist-client/modeller/{figure-v3-preview.d.ts → figure-preview.d.ts} +4 -4
  94. package/dist-client/modeller/{figure-v3-preview.js → figure-preview.js} +21 -21
  95. package/dist-client/modeller/figure-preview.js.map +1 -0
  96. package/dist-client/modeller/{figure-v3-properties.d.ts → figure-properties.d.ts} +8 -8
  97. package/dist-client/modeller/{figure-v3-properties.js → figure-properties.js} +25 -25
  98. package/dist-client/modeller/figure-properties.js.map +1 -0
  99. package/dist-client/modeller/{figure-v3-recipe-catalog.d.ts → figure-recipe-catalog.d.ts} +4 -4
  100. package/dist-client/modeller/{figure-v3-recipe-catalog.js → figure-recipe-catalog.js} +21 -21
  101. package/dist-client/modeller/figure-recipe-catalog.js.map +1 -0
  102. package/dist-client/modeller/{figure-v3-verdict.d.ts → figure-verdict.d.ts} +2 -2
  103. package/dist-client/modeller/{figure-v3-verdict.js → figure-verdict.js} +26 -26
  104. package/dist-client/modeller/figure-verdict.js.map +1 -0
  105. package/dist-client/modeller/{v3-first-save.js → first-save.js} +1 -1
  106. package/dist-client/modeller/first-save.js.map +1 -0
  107. package/dist-client/modeller/gate-findings.d.ts +2 -2
  108. package/dist-client/modeller/gate-findings.js +2 -2
  109. package/dist-client/modeller/gate-findings.js.map +1 -1
  110. package/dist-client/modeller/{v3-gizmo.d.ts → gizmo.d.ts} +5 -5
  111. package/dist-client/modeller/{v3-gizmo.js → gizmo.js} +3 -3
  112. package/dist-client/modeller/gizmo.js.map +1 -0
  113. package/dist-client/modeller/{v3-help.js → help.js} +1 -1
  114. package/dist-client/modeller/help.js.map +1 -0
  115. package/dist-client/modeller/joint-drag.d.ts +19 -0
  116. package/dist-client/modeller/{v3-joint-drag.js → joint-drag.js} +4 -4
  117. package/dist-client/modeller/joint-drag.js.map +1 -0
  118. package/dist-client/modeller/{v3-joint-fan.d.ts → joint-fan.d.ts} +3 -3
  119. package/dist-client/modeller/{v3-joint-fan.js → joint-fan.js} +6 -6
  120. package/dist-client/modeller/joint-fan.js.map +1 -0
  121. package/dist-client/modeller/{v3-keys.js → keys.js} +1 -1
  122. package/dist-client/modeller/keys.js.map +1 -0
  123. package/dist-client/modeller/{v3-links.d.ts → links.d.ts} +48 -48
  124. package/dist-client/modeller/{v3-links.js → links.js} +15 -15
  125. package/dist-client/modeller/links.js.map +1 -0
  126. package/dist-client/modeller/{v3-live.d.ts → live.d.ts} +6 -6
  127. package/dist-client/modeller/{v3-live.js → live.js} +5 -5
  128. package/dist-client/modeller/live.js.map +1 -0
  129. package/dist-client/modeller/{v3-maturity.d.ts → maturity.d.ts} +26 -26
  130. package/dist-client/modeller/{v3-maturity.js → maturity.js} +28 -28
  131. package/dist-client/modeller/maturity.js.map +1 -0
  132. package/dist-client/modeller/modeller-route.d.ts +1 -1
  133. package/dist-client/modeller/modeller-route.js +1 -1
  134. package/dist-client/modeller/modeller-route.js.map +1 -1
  135. package/dist-client/modeller/{v3-occupancy.d.ts → occupancy.d.ts} +4 -4
  136. package/dist-client/modeller/{v3-occupancy.js → occupancy.js} +6 -6
  137. package/dist-client/modeller/occupancy.js.map +1 -0
  138. package/dist-client/modeller/palette.d.ts +7 -0
  139. package/dist-client/modeller/{v3-palette.js → palette.js} +1 -1
  140. package/dist-client/modeller/palette.js.map +1 -0
  141. package/dist-client/modeller/{v3-preview-model.d.ts → preview-model.d.ts} +3 -3
  142. package/dist-client/modeller/{v3-preview-model.js → preview-model.js} +6 -6
  143. package/dist-client/modeller/preview-model.js.map +1 -0
  144. package/dist-client/modeller/{v3-product-session.d.ts → product-session.d.ts} +56 -56
  145. package/dist-client/modeller/{v3-product-session.js → product-session.js} +7 -7
  146. package/dist-client/modeller/product-session.js.map +1 -0
  147. package/dist-client/modeller/{v3-recipe-source.d.ts → recipe-source.d.ts} +2 -2
  148. package/dist-client/modeller/{v3-recipe-source.js → recipe-source.js} +3 -3
  149. package/dist-client/modeller/recipe-source.js.map +1 -0
  150. package/dist-client/modeller/{v3-recipes.d.ts → recipes.d.ts} +7 -7
  151. package/dist-client/modeller/{v3-recipes.js → recipes.js} +4 -4
  152. package/dist-client/modeller/recipes.js.map +1 -0
  153. package/dist-client/modeller/{v3-refusals.js → refusals.js} +1 -1
  154. package/dist-client/modeller/refusals.js.map +1 -0
  155. package/dist-client/modeller/{v3-renderer.d.ts → renderer.d.ts} +6 -6
  156. package/dist-client/modeller/{v3-renderer.js → renderer.js} +13 -13
  157. package/dist-client/modeller/renderer.js.map +1 -0
  158. package/dist-client/modeller/{v3-stage-model.d.ts → stage-model.d.ts} +7 -7
  159. package/dist-client/modeller/{v3-stage-model.js → stage-model.js} +5 -5
  160. package/dist-client/modeller/stage-model.js.map +1 -0
  161. package/dist-client/modeller/thumbnail.d.ts +3 -0
  162. package/dist-client/modeller/{v3-thumbnail.js → thumbnail.js} +6 -6
  163. package/dist-client/modeller/thumbnail.js.map +1 -0
  164. package/dist-client/pages/figure-list-page.d.ts +1 -1
  165. package/dist-client/pages/figure-list-page.js +4 -4
  166. package/dist-client/pages/figure-list-page.js.map +1 -1
  167. package/dist-client/pages/{figure-v3-modeller-page.d.ts → figure-modeller-page.d.ts} +10 -10
  168. package/dist-client/pages/{figure-v3-modeller-page.js → figure-modeller-page.js} +42 -42
  169. package/dist-client/pages/figure-modeller-page.js.map +1 -0
  170. package/dist-client/route.d.ts +1 -1
  171. package/dist-client/route.js +1 -5
  172. package/dist-client/route.js.map +1 -1
  173. package/dist-client/tsconfig.tsbuildinfo +1 -1
  174. package/docs/product-cutover.md +25 -0
  175. package/package.json +5 -5
  176. package/test/{v3-ai-proposal.test.ts → ai-proposal.test.ts} +6 -6
  177. package/test/{v3-align.test.ts → align.test.ts} +8 -8
  178. package/test/browser/modeller-page.js +30 -30
  179. package/test/browser/modeller.check.mjs +14 -14
  180. package/test/browser/serve.mjs +1 -1
  181. package/test/browser/thumbnail-page.js +6 -6
  182. package/test/{v3-clips.test.ts → clips.test.ts} +18 -18
  183. package/test/{v3-controls.test.ts → controls.test.ts} +15 -15
  184. package/test/{v3-fields.test.ts → fields.test.ts} +13 -13
  185. package/test/figure-assistant-surface.test.ts +3 -3
  186. package/test/{v3-first-save.test.ts → first-save.test.ts} +10 -10
  187. package/test/fixtures/joint-column.json +2 -2
  188. package/test/fixtures/joint-roller.json +2 -2
  189. package/test/fixtures/robot-arm-6dof.json +2 -2
  190. package/test/{v3-gizmo.test.ts → gizmo.test.ts} +33 -33
  191. package/test/{v3-joint-drag.test.ts → joint-drag.test.ts} +17 -17
  192. package/test/{v3-joint-fan.test.ts → joint-fan.test.ts} +18 -18
  193. package/test/joints.ts +6 -6
  194. package/test/{v3-keys.test.ts → keys.test.ts} +2 -2
  195. package/test/{v3-layout.test.ts → layout.test.ts} +8 -8
  196. package/test/{v3-links.test.ts → links.test.ts} +201 -201
  197. package/test/{v3-live.test.ts → live.test.ts} +18 -18
  198. package/test/{v3-material.test.ts → material.test.ts} +12 -12
  199. package/test/{v3-maturity.test.ts → maturity.test.ts} +74 -74
  200. package/test/modeller-route.test.ts +5 -6
  201. package/test/{v3-palette-icons.test.ts → palette-icons.test.ts} +1 -1
  202. package/test/{v3-parts-wiring.test.ts → parts-wiring.test.ts} +5 -5
  203. package/test/{v3-preview.test.ts → preview.test.ts} +12 -12
  204. package/test/{v3-product-session.test.ts → product-session.test.ts} +17 -17
  205. package/test/{v3-product-write.test.ts → product-write.test.ts} +20 -20
  206. package/test/{v3-properties.test.ts → properties.test.ts} +24 -24
  207. package/test/{v3-recipes.test.ts → recipes.test.ts} +9 -9
  208. package/test/{v3-refusals.test.ts → refusals.test.ts} +43 -43
  209. package/test/{v3-release-panel.test.ts → release-panel.test.ts} +21 -21
  210. package/test/renderer.test.ts +24 -0
  211. package/test/resolve-ts-siblings.mjs +1 -1
  212. package/test/{v3-scene-canvas.test.ts → scene-canvas.test.ts} +6 -6
  213. package/test/{v3-stage-model.test.ts → stage-model.test.ts} +5 -5
  214. package/test/{v3-theme.test.ts → theme.test.ts} +4 -4
  215. package/test/{v3-thumbnail.test.ts → thumbnail.test.ts} +4 -4
  216. package/test/{v3-verdict.test.ts → verdict.test.ts} +17 -17
  217. package/test/{v3-view.test.ts → view.test.ts} +2 -2
  218. package/things-factory.config.js +2 -3
  219. package/translations/en.json +2 -2
  220. package/translations/ja.json +2 -2
  221. package/translations/ko.json +2 -2
  222. package/translations/ms.json +2 -2
  223. package/translations/zh.json +2 -2
  224. package/dist-client/graphql/v3-store.d.ts +0 -3
  225. package/dist-client/graphql/v3-store.js.map +0 -1
  226. package/dist-client/modeller/figure-v3-capability.js.map +0 -1
  227. package/dist-client/modeller/figure-v3-clips.js.map +0 -1
  228. package/dist-client/modeller/figure-v3-controls.js.map +0 -1
  229. package/dist-client/modeller/figure-v3-first-save.js.map +0 -1
  230. package/dist-client/modeller/figure-v3-links.js.map +0 -1
  231. package/dist-client/modeller/figure-v3-material.js.map +0 -1
  232. package/dist-client/modeller/figure-v3-parts.js.map +0 -1
  233. package/dist-client/modeller/figure-v3-preview.js.map +0 -1
  234. package/dist-client/modeller/figure-v3-properties.js.map +0 -1
  235. package/dist-client/modeller/figure-v3-recipe-catalog.js.map +0 -1
  236. package/dist-client/modeller/figure-v3-verdict.js.map +0 -1
  237. package/dist-client/modeller/v3-ai-proposal.js.map +0 -1
  238. package/dist-client/modeller/v3-align.js.map +0 -1
  239. package/dist-client/modeller/v3-canvas.js.map +0 -1
  240. package/dist-client/modeller/v3-clips.js.map +0 -1
  241. package/dist-client/modeller/v3-controls.js.map +0 -1
  242. package/dist-client/modeller/v3-editor.js.map +0 -1
  243. package/dist-client/modeller/v3-fields.js.map +0 -1
  244. package/dist-client/modeller/v3-figure-facts.js.map +0 -1
  245. package/dist-client/modeller/v3-first-save.js.map +0 -1
  246. package/dist-client/modeller/v3-gizmo.js.map +0 -1
  247. package/dist-client/modeller/v3-help.js.map +0 -1
  248. package/dist-client/modeller/v3-joint-drag.d.ts +0 -19
  249. package/dist-client/modeller/v3-joint-drag.js.map +0 -1
  250. package/dist-client/modeller/v3-joint-fan.js.map +0 -1
  251. package/dist-client/modeller/v3-keys.js.map +0 -1
  252. package/dist-client/modeller/v3-links.js.map +0 -1
  253. package/dist-client/modeller/v3-live.js.map +0 -1
  254. package/dist-client/modeller/v3-maturity.js.map +0 -1
  255. package/dist-client/modeller/v3-occupancy.js.map +0 -1
  256. package/dist-client/modeller/v3-palette.d.ts +0 -7
  257. package/dist-client/modeller/v3-palette.js.map +0 -1
  258. package/dist-client/modeller/v3-preview-model.js.map +0 -1
  259. package/dist-client/modeller/v3-product-session.js.map +0 -1
  260. package/dist-client/modeller/v3-recipe-source.js.map +0 -1
  261. package/dist-client/modeller/v3-recipes.js.map +0 -1
  262. package/dist-client/modeller/v3-refusals.js.map +0 -1
  263. package/dist-client/modeller/v3-renderer.js.map +0 -1
  264. package/dist-client/modeller/v3-stage-model.js.map +0 -1
  265. package/dist-client/modeller/v3-thumbnail.d.ts +0 -3
  266. package/dist-client/modeller/v3-thumbnail.js.map +0 -1
  267. package/dist-client/pages/figure-v3-modeller-page.js.map +0 -1
  268. package/dist-client/tsconfig.chk.tsbuildinfo +0 -1
  269. package/dist-client/tsconfig.fui.tsbuildinfo +0 -1
  270. package/dist-client/tsconfig.scratch-fm.tsbuildinfo +0 -1
  271. package/docs/v3-product-cutover.md +0 -25
  272. package/test/v3-renderer.test.ts +0 -24
  273. /package/client/modeller/{v3-fields.ts → fields.ts} +0 -0
  274. /package/client/modeller/{v3-first-save.ts → first-save.ts} +0 -0
  275. /package/client/modeller/{v3-help.ts → help.ts} +0 -0
  276. /package/client/modeller/{v3-keys.ts → keys.ts} +0 -0
  277. /package/client/modeller/{v3-refusals.ts → refusals.ts} +0 -0
  278. /package/dist-client/modeller/{v3-fields.d.ts → fields.d.ts} +0 -0
  279. /package/dist-client/modeller/{v3-first-save.d.ts → first-save.d.ts} +0 -0
  280. /package/dist-client/modeller/{v3-help.d.ts → help.d.ts} +0 -0
  281. /package/dist-client/modeller/{v3-keys.d.ts → keys.d.ts} +0 -0
  282. /package/dist-client/modeller/{v3-refusals.d.ts → refusals.d.ts} +0 -0
@@ -1,11 +1,11 @@
1
1
  import { test } from 'node:test'
2
2
  import assert from 'node:assert/strict'
3
- import { createV3Asset, addV3Box, serializeV3Asset } from '@hatiolab/figure-model'
4
- import { createV3ProductSession, type V3ProductRecord } from '../client/modeller/v3-product-session.js'
3
+ import { createFigureAsset, addFigureBox, serializeFigureAsset } from '@hatiolab/figure-model'
4
+ import { createFigureProductSession, type FigureProductRecord } from '../client/modeller/product-session.js'
5
5
 
6
6
  const source = (name = 'sample') =>
7
- serializeV3Asset(
8
- addV3Box(createV3Asset(name), {
7
+ serializeFigureAsset(
8
+ addFigureBox(createFigureAsset(name), {
9
9
  id: 'body',
10
10
  size: { x: 100, y: 100, z: 100 },
11
11
  position: { x: 0, y: 50, z: 0 }
@@ -25,7 +25,7 @@ const store = {
25
25
  }
26
26
 
27
27
  test('existing model -> new clears geometry, ID, history and derived state', async () => {
28
- const s = createV3ProductSession(store)
28
+ const s = createFigureProductSession(store)
29
29
  await s.open('A')
30
30
  assert.equal(s.read().geometry.length, 1)
31
31
  const v = s.startNew()
@@ -33,18 +33,18 @@ test('existing model -> new clears geometry, ID, history and derived state', asy
33
33
  assert.equal(v.geometry.length, 0)
34
34
  assert.equal(v.canUndo, false)
35
35
  assert.equal(v.dirty, false)
36
- assert.equal(JSON.parse(s.source()).version, 'figure-v3-asset-1')
36
+ assert.equal(JSON.parse(s.source()).version, 'figure-asset-1')
37
37
  })
38
38
  test('late load cannot overwrite another model or a new figure', async () => {
39
- const pending = deferred<V3ProductRecord>()
40
- const s = createV3ProductSession({ ...store, load: id => (id === 'A' ? pending.promise : store.load(id)) })
39
+ const pending = deferred<FigureProductRecord>()
40
+ const s = createFigureProductSession({ ...store, load: id => (id === 'A' ? pending.promise : store.load(id)) })
41
41
  const first = s.open('A')
42
42
  await s.open('B')
43
43
  pending.resolve({ id: 'A', source: source('A') })
44
44
  await first
45
45
  assert.equal(s.read().id, 'B')
46
- const p = deferred<V3ProductRecord>(),
47
- t = createV3ProductSession({ ...store, load: () => p.promise })
46
+ const p = deferred<FigureProductRecord>(),
47
+ t = createFigureProductSession({ ...store, load: () => p.promise })
48
48
  const loading = t.open('A')
49
49
  t.startNew()
50
50
  p.resolve({ id: 'A', source: source() })
@@ -53,7 +53,7 @@ test('late load cannot overwrite another model or a new figure', async () => {
53
53
  assert.equal(t.read().geometry.length, 0)
54
54
  })
55
55
  test('failed load hides previous geometry and cannot save under the failed ID', async () => {
56
- const s = createV3ProductSession({ ...store, load: async id => ({ id, source: id === 'bad' ? '{}' : source() }) })
56
+ const s = createFigureProductSession({ ...store, load: async id => ({ id, source: id === 'bad' ? '{}' : source() }) })
57
57
  await s.open('A')
58
58
  await s.open('bad')
59
59
  assert.equal(s.read().geometry.length, 0)
@@ -62,7 +62,7 @@ test('failed load hides previous geometry and cannot save under the failed ID',
62
62
  await assert.rejects(s.save())
63
63
  })
64
64
  test('UI and AI use a shared revision and history; stale/cross-model AI is rejected', async () => {
65
- const s = createV3ProductSession(store)
65
+ const s = createFigureProductSession(store)
66
66
  await s.open('A')
67
67
  const before = s.read(),
68
68
  changes = [{ kind: 'set-design' as const, input: 'body.size.y', unit: 'mm' as const, value: 150 }]
@@ -76,7 +76,7 @@ test('UI and AI use a shared revision and history; stale/cross-model AI is rejec
76
76
  })
77
77
  test('save carries exact baseline and acknowledges only matching response', async () => {
78
78
  let baseline: string | undefined
79
- const s = createV3ProductSession({
79
+ const s = createFigureProductSession({
80
80
  ...store,
81
81
  save: async q => {
82
82
  baseline = q.expectedSource
@@ -93,8 +93,8 @@ test('save carries exact baseline and acknowledges only matching response', asyn
93
93
  assert.equal(s.read().dirty, true)
94
94
  })
95
95
  test('pending save locks edits/navigation and duplicate create', async () => {
96
- const p = deferred<V3ProductRecord>(),
97
- s = createV3ProductSession({ ...store, save: () => p.promise })
96
+ const p = deferred<FigureProductRecord>(),
97
+ s = createFigureProductSession({ ...store, save: () => p.promise })
98
98
  s.startNew()
99
99
  const text = s.source(),
100
100
  saving = s.save()
@@ -106,7 +106,7 @@ test('pending save locks edits/navigation and duplicate create', async () => {
106
106
  assert.equal(s.read().id, 'new')
107
107
  })
108
108
  test('save failure preserves edits and allows retry; wrong acknowledgement never clears dirty', async () => {
109
- const s = createV3ProductSession({ ...store, save: async () => ({ id: 'wrong', source: source() }) })
109
+ const s = createFigureProductSession({ ...store, save: async () => ({ id: 'wrong', source: source() }) })
110
110
  await s.open('A')
111
111
  s.apply([{ kind: 'set-design', input: 'body.size.y', unit: 'mm', value: 200 }])
112
112
  await assert.rejects(s.save())
@@ -116,7 +116,7 @@ test('save failure preserves edits and allows retry; wrong acknowledgement never
116
116
  })
117
117
  test('retry after a lost create response reuses the same creation identity', async () => {
118
118
  const keys: string[] = []
119
- const s = createV3ProductSession({
119
+ const s = createFigureProductSession({
120
120
  ...store,
121
121
  save: async q => {
122
122
  keys.push(q.creationId!)
@@ -1,12 +1,12 @@
1
1
  import { test } from 'node:test'
2
2
  import assert from 'node:assert/strict'
3
- import { addV3Box, createV3Asset, serializeV3Asset } from '@hatiolab/figure-model'
4
- import { prepareV3Write, compareAndSwapV3 } from '../../figure-service/server/service/figure/figure-v3-write.js'
3
+ import { addFigureBox, createFigureAsset, serializeFigureAsset } from '@hatiolab/figure-model'
4
+ import { prepareFigureWrite, compareAndSwapFigure } from '../../figure-service/server/service/figure/figure-write.js'
5
5
  import { DatabaseSync } from 'node:sqlite'
6
6
 
7
- test('V3 saves preserve the graph and clear unmeasured previous scores', () => {
8
- const source = serializeV3Asset(createV3Asset('빈 자산'))
9
- const saved = prepareV3Write(source)
7
+ test('saves preserve the graph and clear unmeasured previous scores', () => {
8
+ const source = serializeFigureAsset(createFigureAsset('빈 자산'))
9
+ const saved = prepareFigureWrite(source)
10
10
  assert.equal(saved.source, source)
11
11
  assert.equal(saved.name, '빈 자산')
12
12
  /* 점수는 저장할 때 재지 않으므로 비운다. */
@@ -16,26 +16,26 @@ test('V3 saves preserve the graph and clear unmeasured previous scores', () => {
16
16
  assert.equal(saved.groups, 0)
17
17
  assert.equal(saved.state, 'draft')
18
18
  })
19
- test('V3 예산은 실제로 그리는 것을 센다 — 상자 하나는 삼각형 12 다', () => {
20
- const asset = addV3Box(createV3Asset('상자'), {
19
+ test('예산은 실제로 그리는 것을 센다 — 상자 하나는 삼각형 12 다', () => {
20
+ const asset = addFigureBox(createFigureAsset('상자'), {
21
21
  id: 'body',
22
22
  size: { x: 100, y: 100, z: 100 },
23
23
  position: { x: 0, y: 50, z: 0 }
24
24
  })
25
- const saved = prepareV3Write(serializeV3Asset(asset))
25
+ const saved = prepareFigureWrite(serializeFigureAsset(asset))
26
26
  assert.equal(saved.triangles, 12)
27
27
  assert.equal(saved.groups, 1)
28
28
  })
29
29
  test('source concurrency check requires exact baseline; no implicit overwrite', () => {
30
- const source = serializeV3Asset(createV3Asset('A'))
31
- assert.throws(() => prepareV3Write(source, { source }))
32
- assert.throws(() => prepareV3Write(source, { source }, 'changed'))
33
- assert.equal(prepareV3Write(source, { source }, source).source, source)
30
+ const source = serializeFigureAsset(createFigureAsset('A'))
31
+ assert.throws(() => prepareFigureWrite(source, { source }))
32
+ assert.throws(() => prepareFigureWrite(source, { source }, 'changed'))
33
+ assert.equal(prepareFigureWrite(source, { source }, source).source, source)
34
34
  })
35
- test('old/invalid/duplicate-key sources never become a V3 saved draft', () => {
36
- assert.throws(() => prepareV3Write('{"version":2,"parts":[]}'))
37
- assert.throws(() => prepareV3Write('{"version":1,"version":2}'))
38
- assert.throws(() => prepareV3Write('{'))
35
+ test('old/invalid/duplicate-key sources never become a saved draft', () => {
36
+ assert.throws(() => prepareFigureWrite('{"version":2,"parts":[]}'))
37
+ assert.throws(() => prepareFigureWrite('{"version":1,"version":2}'))
38
+ assert.throws(() => prepareFigureWrite('{'))
39
39
  })
40
40
 
41
41
  test('SQLite compare-and-swap rejects a stale editor and another tenant', async () => {
@@ -49,10 +49,10 @@ test('SQLite compare-and-swap rejects a stale editor and another tenant', async
49
49
  .run('next', where.id, where.domain.id, where.source)
50
50
  return { affected: Number(result.changes) }
51
51
  }
52
- await assert.rejects(compareAndSwapV3(update, 'A', 'tenant-B', 'original'))
53
- await compareAndSwapV3(update, 'A', 'tenant-A', 'original')
54
- await assert.rejects(compareAndSwapV3(update, 'A', 'tenant-A', 'original'))
55
- await assert.rejects(compareAndSwapV3(update, 'A', 'tenant-A', undefined))
52
+ await assert.rejects(compareAndSwapFigure(update, 'A', 'tenant-B', 'original'))
53
+ await compareAndSwapFigure(update, 'A', 'tenant-A', 'original')
54
+ await assert.rejects(compareAndSwapFigure(update, 'A', 'tenant-A', 'original'))
55
+ await assert.rejects(compareAndSwapFigure(update, 'A', 'tenant-A', undefined))
56
56
  assert.equal(db.prepare('SELECT source FROM figures WHERE id=?').get('A')!.source, 'next')
57
57
  } finally {
58
58
  db.close()
@@ -9,18 +9,18 @@ import assert from 'node:assert/strict'
9
9
  import { readFileSync } from 'node:fs'
10
10
  import { fileURLToPath } from 'node:url'
11
11
 
12
- import { addV3Box, applyV3Authoring, createV3Asset, inspectV3Asset, parseV3Asset, serializeV3Asset } from '@hatiolab/figure-model'
12
+ import { addFigureBox, applyFigureAuthoring, createFigureAsset, inspectFigureAsset, parseFigureAsset, serializeFigureAsset } from '@hatiolab/figure-model'
13
13
 
14
- import { tagsOf, v3FigureFacts } from '../client/modeller/v3-figure-facts.ts'
15
- import { occupancyFromParts } from '../client/modeller/v3-occupancy.ts'
14
+ import { tagsOf, figureFigureFacts } from '../client/modeller/figure-facts.ts'
15
+ import { occupancyFromParts } from '../client/modeller/occupancy.ts'
16
16
 
17
17
  const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
18
18
  const probe = () => {
19
- let asset = createV3Asset('Probe')
20
- asset = addV3Box(asset, { id: 'body', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 } })
19
+ let asset = createFigureAsset('Probe')
20
+ asset = addFigureBox(asset, { id: 'body', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 } })
21
21
  return asset
22
22
  }
23
- const facts = (asset: ReturnType<typeof probe>) => v3FigureFacts(serializeV3Asset(asset))
23
+ const facts = (asset: ReturnType<typeof probe>) => figureFigureFacts(serializeFigureAsset(asset))
24
24
 
25
25
  test('탭이 읽는 사실 — 선언 전에는 배치 기준도 장착면도 「없음」이다', () => {
26
26
  const read = facts(probe())
@@ -32,25 +32,25 @@ test('탭이 읽는 사실 — 선언 전에는 배치 기준도 장착면도
32
32
  })
33
33
 
34
34
  test('배치 기준을 고르면 그 기준으로 공간을 선언한다 — 판정도 풀린다', () => {
35
- const source = serializeV3Asset(probe())
36
- const declared = applyV3Authoring(parseV3Asset(source), occupancyFromParts(source, 'ceiling'))
35
+ const source = serializeFigureAsset(probe())
36
+ const declared = applyFigureAuthoring(parseFigureAsset(source), occupancyFromParts(source, 'ceiling'))
37
37
 
38
38
  assert.equal(facts(declared).placement, 'ceiling')
39
39
  assert.equal(facts(declared).declared, true)
40
- assert.ok(!inspectV3Asset(declared).violations.some(v => v.code === 'OCCUPANCY_UNDECLARED'))
40
+ assert.ok(!inspectFigureAsset(declared).violations.some(v => v.code === 'OCCUPANCY_UNDECLARED'))
41
41
  })
42
42
 
43
43
  test('장착면을 정하면 탭이 그 부품과 면을 다시 읽고, 해제하면 없어진다', () => {
44
- const source = serializeV3Asset(probe())
45
- const declared = applyV3Authoring(parseV3Asset(source), occupancyFromParts(source))
46
- const mounted = applyV3Authoring(declared, { kind: 'set-mounting-face', part: 'body', face: 'bottom' })
44
+ const source = serializeFigureAsset(probe())
45
+ const declared = applyFigureAuthoring(parseFigureAsset(source), occupancyFromParts(source))
46
+ const mounted = applyFigureAuthoring(declared, { kind: 'set-mounting-face', part: 'body', face: 'bottom' })
47
47
 
48
48
  assert.deepEqual(facts(mounted).mounting, { part: 'body', face: 'bottom' })
49
- assert.equal(facts(applyV3Authoring(mounted, { kind: 'clear-mounting-face' })).mounting, undefined)
49
+ assert.equal(facts(applyFigureAuthoring(mounted, { kind: 'clear-mounting-face' })).mounting, undefined)
50
50
  })
51
51
 
52
52
  test('이름을 바꾸면 자산의 이름이 바뀐다 — 저장하면 목록 이름이 이것을 따른다', () => {
53
- assert.equal(facts(applyV3Authoring(probe(), { kind: 'rename', name: 'Roller conveyor' })).name, 'Roller conveyor')
53
+ assert.equal(facts(applyFigureAuthoring(probe(), { kind: 'rename', name: 'Roller conveyor' })).name, 'Roller conveyor')
54
54
  })
55
55
 
56
56
  test('태그 줄은 쉼표로 나뉘어 목록이 된다', () => {
@@ -58,14 +58,14 @@ test('태그 줄은 쉼표로 나뉘어 목록이 된다', () => {
58
58
  assert.deepEqual(tagsOf(''), [], '빈 줄은 전부 지우는 것이다')
59
59
  })
60
60
 
61
- const page = read('../client/pages/figure-v3-modeller-page.ts')
61
+ const page = read('../client/pages/figure-modeller-page.ts')
62
62
 
63
63
  test('화면이 속성 탭을 걸고, 명령은 세션의 이력 하나로, 분류 · 태그는 행으로 간다', () => {
64
- const editor = read('../client/modeller/v3-editor.ts')
64
+ const editor = read('../client/modeller/editor.ts')
65
65
  assert.ok(/renderTab\('properties', 'tune'/.test(editor), '속성 탭은 편집기의 오른쪽 칸에')
66
- assert.ok(/<figure-v3-properties\s+slot="figure"/.test(page), '도형 속성은 부품을 고르지 않았을 때 속성 탭에 선다')
66
+ assert.ok(/<figure-properties\s+slot="figure"/.test(page), '도형 속성은 부품을 고르지 않았을 때 속성 탭에 선다')
67
67
  assert.ok(editor.includes('<slot name="figure"></slot>'))
68
- assert.ok(/@v3-command=\$\{this\.onCommand\}/.test(page) && /@v3-catalog=\$\{this\.onCatalog\}/.test(page))
68
+ assert.ok(/@command=\$\{this\.onCommand\}/.test(page) && /@catalog=\$\{this\.onCatalog\}/.test(page))
69
69
  const run = page.slice(page.indexOf('private runCommand'), page.indexOf('private onCatalog'))
70
70
  assert.ok(/this\.session\.command\(actionOf\(\)\)/.test(run), '명령은 세션을 지난다 — 되돌리기가 먹는다')
71
71
  const catalog = page.slice(page.indexOf('private onCatalog'), page.indexOf('private released'))
@@ -83,7 +83,7 @@ test('발행한 뒤에도 행의 타입이 남는다 — 새로 짜면 잠긴
83
83
  })
84
84
 
85
85
  test('속성 탭의 말은 다섯 말 모두에 있다', () => {
86
- const tab = read('../client/modeller/figure-v3-properties.ts')
86
+ const tab = read('../client/modeller/figure-properties.ts')
87
87
  assert.ok(tab.includes('occupancyFromParts(this.source, placement)'), '고른 배치 기준으로 선언한다')
88
88
  const keys = [...tab.matchAll(/'(figure\.[a-z.-]*[a-z])'/g)].map(m => m[1])
89
89
  for (const face of ['left', 'right', 'bottom', 'top', 'back', 'front']) keys.push(`figure.label.face-${face}`)
@@ -97,12 +97,12 @@ test('속성 탭의 말은 다섯 말 모두에 있다', () => {
97
97
  /* Walk 1 (2026-09-26): the mounting face offered is where the figure meets its plane -- a conveyor's legs, not its bed. */
98
98
  test('장착면은 도형이 평면에 닿는 부품으로 권한다 — 다리 위 바닥이 아니라 다리', async () => {
99
99
  const fm = await import('@hatiolab/figure-model')
100
- let asset = fm.addV3Box(fm.createV3Asset('Conveyor'), { id: 'bed', size: { x: 3000, y: 700, z: 800 }, position: { x: 0, y: 1050, z: 0 } })
101
- asset = fm.addV3Box(asset, { id: 'leg', size: { x: 60, y: 700, z: 60 }, position: { x: -1400, y: 350, z: -300 } })
102
- asset = fm.applyV3Authoring(asset, { kind: 'attach', part: 'leg', face: 'top', to: { part: 'bed', face: 'bottom' }, align: { x: 'min', z: 'min' } })
103
- const facts = v3FigureFacts(fm.serializeV3Asset(asset))
100
+ let asset = fm.addFigureBox(fm.createFigureAsset('Conveyor'), { id: 'bed', size: { x: 3000, y: 700, z: 800 }, position: { x: 0, y: 1050, z: 0 } })
101
+ asset = fm.addFigureBox(asset, { id: 'leg', size: { x: 60, y: 700, z: 60 }, position: { x: -1400, y: 350, z: -300 } })
102
+ asset = fm.applyFigureAuthoring(asset, { kind: 'attach', part: 'leg', face: 'top', to: { part: 'bed', face: 'bottom' }, align: { x: 'min', z: 'min' } })
103
+ const facts = figureFigureFacts(fm.serializeFigureAsset(asset))
104
104
  assert.deepEqual(facts.suggested, { part: 'leg', face: 'bottom' })
105
105
  assert.equal(facts.parts[0], 'bed', '목록의 첫 부품은 바닥이다')
106
- const panel = readFileSync(fileURLToPath(new URL('../client/modeller/figure-v3-properties.ts', import.meta.url)), 'utf8')
106
+ const panel = readFileSync(fileURLToPath(new URL('../client/modeller/figure-properties.ts', import.meta.url)), 'utf8')
107
107
  assert.ok(panel.includes('[this.pickedPart, mounting?.part, facts.suggested?.part, facts.parts[0]]'), '고른 것 · 정한 것 다음이 권하는 것')
108
108
  })
@@ -7,7 +7,7 @@ import assert from 'node:assert/strict'
7
7
  import { readFileSync } from 'node:fs'
8
8
  import { fileURLToPath } from 'node:url'
9
9
 
10
- import { recipeFieldOf, recipeReady, recipesMatching, recipeValueOf, recipeWord, recipeWorkings, templatesOf } from '../client/modeller/v3-recipes.ts'
10
+ import { recipeFieldOf, recipeReady, recipesMatching, recipeValueOf, recipeWord, recipeWorkings, templatesOf } from '../client/modeller/recipes.ts'
11
11
 
12
12
  const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
13
13
  /* The product's templates, as the server reads them. */
@@ -62,22 +62,22 @@ test('작동 방식은 레시피에서 셈한다 — 미닫이 문: 두 문짝
62
62
  })
63
63
 
64
64
  test('모델러 — 부품 칸의 단추가 카탈로그를 열고, 넣으면 한 단계로 들어가고 창이 닫힌다; 목록은 페이지가 서버에서 한 번 읽는다', () => {
65
- const parts = read('../client/modeller/figure-v3-parts.ts')
65
+ const parts = read('../client/modeller/figure-parts.ts')
66
66
  assert.ok(parts.includes("this.ask('open-recipe-catalog', {})"))
67
- const editor = read('../client/modeller/v3-editor.ts')
67
+ const editor = read('../client/modeller/editor.ts')
68
68
  const insert = editor.slice(editor.indexOf(' private insertRecipe('), editor.indexOf(' /** The design values of the asset being edited. */'))
69
- assert.ok(insert.includes('v3RecipeIdFor(asset, checked)') && insert.includes('this.session!.applyRecipe(steps)') && insert.includes('this.catalogOpen = false'))
69
+ assert.ok(insert.includes('figureRecipeIdFor(asset, checked)') && insert.includes('this.session!.applyRecipe(steps)') && insert.includes('this.catalogOpen = false'))
70
70
  assert.ok(insert.includes('this.recipeError = recipeRefusalOf(error, (key, params) => i18next.t(key, params)) ?? this.said(error)'), '거절은 사람의 말로, 막힌 단계와 함께 — 커널의 영어가 아니라 (2026-09-27)')
71
- const page = read('../client/pages/figure-v3-modeller-page.ts')
71
+ const page = read('../client/pages/figure-modeller-page.ts')
72
72
  assert.ok(page.includes("availableRecipes(i18next.language || 'en')") && page.includes('.templates=${this.templates}'))
73
- const catalog = read('../client/modeller/figure-v3-recipe-catalog.ts')
74
- assert.ok(catalog.includes('describeV3Recipe(t.recipe)'), '부품 수는 셈한다')
73
+ const catalog = read('../client/modeller/figure-recipe-catalog.ts')
74
+ assert.ok(catalog.includes('describeFigureRecipe(t.recipe)'), '부품 수는 셈한다')
75
75
  assert.ok(catalog.includes("${this.guide ? html`<ox-markdown"), '설명이 없으면 아무것도 두지 않는다 — 「준비 중」 없음')
76
76
  })
77
77
 
78
78
  test('넣기는 카탈로그에서 온다 — 편집기는 카탈로그 요소에서 insert-recipe 를 듣는다 (잘못 옮겨진 연결, 09-26)', () => {
79
- const editor = read('../client/modeller/v3-editor.ts')
80
- const catalog = editor.slice(editor.indexOf('<figure-v3-recipe-catalog'), editor.indexOf('</figure-v3-recipe-catalog>'))
79
+ const editor = read('../client/modeller/editor.ts')
80
+ const catalog = editor.slice(editor.indexOf('<figure-recipe-catalog'), editor.indexOf('</figure-recipe-catalog>'))
81
81
  assert.ok(catalog.includes('@insert-recipe=${(e: CustomEvent) => this.insertRecipe(e.detail.recipe, e.detail.values)}'))
82
82
  })
83
83
 
@@ -6,7 +6,7 @@ import assert from 'node:assert/strict'
6
6
  import { readFileSync } from 'node:fs'
7
7
  import { fileURLToPath } from 'node:url'
8
8
 
9
- import { sizeRangeRefusalOf } from '../client/modeller/v3-refusals.ts'
9
+ import { sizeRangeRefusalOf } from '../client/modeller/refusals.ts'
10
10
 
11
11
  const ko = JSON.parse(readFileSync(fileURLToPath(new URL('../translations/ko.json', import.meta.url)), 'utf8'))
12
12
  const say = (key: string, params: Record<string, string>) => ko[key].replace(/\{(\w+)\}/g, (_: string, k: string) => params[k] ?? '')
@@ -28,14 +28,14 @@ test('다른 거절은 건드리지 않는다', () => {
28
28
  })
29
29
 
30
30
  test('범위가 받을 수 없는 연결은 같은 연결을 범위를 좁혀 다시 권한다 — 다시 재기로는 풀리지 않는다 (사이징, 2026-09-25)', async () => {
31
- const { narrowingRetryOf } = await import('../client/modeller/v3-refusals.ts')
32
- const { addV3Box, applyV3Authoring, createV3Asset } = await import('@hatiolab/figure-model')
33
- let asset = addV3Box(createV3Asset('Panel'), { id: 'panel', size: { x: 400, y: 400, z: 400 }, position: { x: 0, y: 0, z: 0 } })
34
- asset = applyV3Authoring(asset, { kind: 'declare-instance-size', range: { x: { min: 1, max: 4000 }, y: { min: 1, max: 4000 }, z: { min: 1, max: 4000 } }, at: { x: 400, y: 400, z: 400 } })
31
+ const { narrowingRetryOf } = await import('../client/modeller/refusals.ts')
32
+ const { addFigureBox, applyFigureAuthoring, createFigureAsset } = await import('@hatiolab/figure-model')
33
+ let asset = addFigureBox(createFigureAsset('Panel'), { id: 'panel', size: { x: 400, y: 400, z: 400 }, position: { x: 0, y: 0, z: 0 } })
34
+ asset = applyFigureAuthoring(asset, { kind: 'declare-instance-size', range: { x: { min: 1, max: 4000 }, y: { min: 1, max: 4000 }, z: { min: 1, max: 4000 } }, at: { x: 400, y: 400, z: 400 } })
35
35
  const link = { kind: 'link-dimension' as const, part: 'panel', axis: 'x' as const, source: { of: 'instance' as const, axis: 'x' as const }, plus: -50 }
36
36
  let error: unknown
37
37
  try {
38
- applyV3Authoring(asset, link)
38
+ applyFigureAuthoring(asset, link)
39
39
  } catch (e) {
40
40
  error = e
41
41
  }
@@ -44,24 +44,24 @@ test('범위가 받을 수 없는 연결은 같은 연결을 범위를 좁혀
44
44
  assert.ok(offer, '권한다')
45
45
  assert.match(offer.label, /figure.button.narrow-range-from .*"axis":"X"/)
46
46
  assert.deepEqual(offer.retry, { ...link, narrowRange: true })
47
- assert.doesNotThrow(() => applyV3Authoring(asset, offer.retry as never), '권한 것은 받아들여진다')
47
+ assert.doesNotThrow(() => applyFigureAuthoring(asset, offer.retry as never), '권한 것은 받아들여진다')
48
48
  assert.equal(narrowingRetryOf({ kind: 'attach' }, error, say), null, '연결이 아닌 명령에는 권하지 않는다')
49
49
  })
50
50
 
51
51
  test('지우기는 매단 것 · 따라 움직이는 것을 풀어 남기고 그 이름을 한 줄로 말한다; 남는 거절은 공간 선언과 줄 (아키텍트 판정 2026-09-27)', async () => {
52
- const { partRefusalOf, removalNoticeOf, editRefusalOf } = await import('../client/modeller/v3-refusals.ts')
52
+ const { partRefusalOf, removalNoticeOf, editRefusalOf } = await import('../client/modeller/refusals.ts')
53
53
  const fm = await import('@hatiolab/figure-model')
54
54
  const say = (key: string, p: Record<string, string>) => `${key} ${JSON.stringify(p)}`
55
- let a = fm.createV3Asset('Bench')
56
- a = fm.addV3Box(a, { id: 'leg', size: { x: 60, y: 700, z: 60 }, position: { x: 0, y: 0, z: 0 } })
57
- a = fm.addV3Box(a, { id: 'top', size: { x: 1200, y: 40, z: 600 }, position: { x: 0, y: 0, z: 0 } })
58
- a = fm.addV3Box(a, { id: 'lamp', size: { x: 80, y: 80, z: 80 }, position: { x: 0, y: 0, z: 0 } })
59
- a = fm.addV3Box(a, { id: 'shade', size: { x: 40, y: 40, z: 40 }, position: { x: 0, y: 0, z: 0 } })
60
- a = fm.applyV3Authoring(a, { kind: 'attach', part: 'leg', face: 'bottom', to: { plane: 'mounting-plane' } })
61
- a = fm.applyV3Authoring(a, { kind: 'attach', part: 'top', face: 'bottom', to: { part: 'leg', face: 'top' } })
62
- a = fm.applyV3Authoring(a, { kind: 'attach', part: 'lamp', face: 'bottom', to: { part: 'top', face: 'top' }, align: { x: { mm: -300 } } })
63
- a = fm.applyV3Authoring(a, { kind: 'attach', part: 'shade', face: 'bottom', to: { part: 'top', face: 'top' }, align: { x: { mm: 300 } } })
64
- const turn = (asset: typeof a, part: string, extra: object) => fm.applyV3Authoring(asset, { kind: 'set-joint', joint: { ...asset.joints!.find(j => j.body1.part === part)!, type: 'revolute', axis: 'Y', ...extra } as never })
55
+ let a = fm.createFigureAsset('Bench')
56
+ a = fm.addFigureBox(a, { id: 'leg', size: { x: 60, y: 700, z: 60 }, position: { x: 0, y: 0, z: 0 } })
57
+ a = fm.addFigureBox(a, { id: 'top', size: { x: 1200, y: 40, z: 600 }, position: { x: 0, y: 0, z: 0 } })
58
+ a = fm.addFigureBox(a, { id: 'lamp', size: { x: 80, y: 80, z: 80 }, position: { x: 0, y: 0, z: 0 } })
59
+ a = fm.addFigureBox(a, { id: 'shade', size: { x: 40, y: 40, z: 40 }, position: { x: 0, y: 0, z: 0 } })
60
+ a = fm.applyFigureAuthoring(a, { kind: 'attach', part: 'leg', face: 'bottom', to: { plane: 'mounting-plane' } })
61
+ a = fm.applyFigureAuthoring(a, { kind: 'attach', part: 'top', face: 'bottom', to: { part: 'leg', face: 'top' } })
62
+ a = fm.applyFigureAuthoring(a, { kind: 'attach', part: 'lamp', face: 'bottom', to: { part: 'top', face: 'top' }, align: { x: { mm: -300 } } })
63
+ a = fm.applyFigureAuthoring(a, { kind: 'attach', part: 'shade', face: 'bottom', to: { part: 'top', face: 'top' }, align: { x: { mm: 300 } } })
64
+ const turn = (asset: typeof a, part: string, extra: object) => fm.applyFigureAuthoring(asset, { kind: 'set-joint', joint: { ...asset.joints!.find(j => j.body1.part === part)!, type: 'revolute', axis: 'Y', ...extra } as never })
65
65
  a = turn(a, 'lamp', { state: { id: 'lamp-turn' } })
66
66
  a = turn(a, 'shade', { mimic: { joint: 'lamp', multiplier: 1 } })
67
67
 
@@ -73,10 +73,10 @@ test('지우기는 매단 것 · 따라 움직이는 것을 풀어 남기고 그
73
73
  assert.match(lamp, /removal-own-control .*"part":"shade".*"control":"shade".*"min":"-180".*"max":"180"/, '따르던 것은 제 조작값을 받는다')
74
74
  assert.equal(removalNoticeOf(fm.removePart(a, { kind: 'remove-part', target: 'shade' }).notice, say), null, '아무것도 풀지 않았으면 말하지 않는다')
75
75
 
76
- const declared = fm.applyV3Authoring(a, { kind: 'declare-occupancy', proposal: fm.proposeV3Occupancy(a), placement: 'floor' })
76
+ const declared = fm.applyFigureAuthoring(a, { kind: 'declare-occupancy', proposal: fm.proposeFigureOccupancy(a), placement: 'floor' })
77
77
  const why = (asset: typeof a, part: string) => {
78
78
  try {
79
- fm.editV3Part(asset, { kind: 'remove-part', target: part })
79
+ fm.editFigurePart(asset, { kind: 'remove-part', target: part })
80
80
  return 'removed'
81
81
  } catch (e) {
82
82
  return partRefusalOf(e, part, say)
@@ -84,12 +84,12 @@ test('지우기는 매단 것 · 따라 움직이는 것을 풀어 남기고 그
84
84
  }
85
85
  assert.match(why(declared, 'shade')!, /part-in-declared-space .*"part":"shade"/, '잎인데 공간 선언에 들어 있다')
86
86
 
87
- let row = fm.createV3Asset('Conveyor')
88
- row = fm.addV3Box(row, { id: 'frame', size: { x: 2000, y: 100, z: 600 }, position: { x: 0, y: 0, z: 0 } })
89
- row = fm.addV3Box(row, { id: 'roller', size: { x: 60, y: 60, z: 560 }, position: { x: 0, y: 0, z: 0 } })
90
- row = fm.applyV3Authoring(row, { kind: 'attach', part: 'frame', face: 'bottom', to: { plane: 'mounting-plane' } })
91
- row = fm.applyV3Authoring(row, { kind: 'attach', part: 'roller', face: 'bottom', to: { part: 'frame', face: 'top' } })
92
- row = fm.applyV3Authoring(row, { kind: 'make-repeat', part: 'roller', axis: 'x', pitch: 200 })
87
+ let row = fm.createFigureAsset('Conveyor')
88
+ row = fm.addFigureBox(row, { id: 'frame', size: { x: 2000, y: 100, z: 600 }, position: { x: 0, y: 0, z: 0 } })
89
+ row = fm.addFigureBox(row, { id: 'roller', size: { x: 60, y: 60, z: 560 }, position: { x: 0, y: 0, z: 0 } })
90
+ row = fm.applyFigureAuthoring(row, { kind: 'attach', part: 'frame', face: 'bottom', to: { plane: 'mounting-plane' } })
91
+ row = fm.applyFigureAuthoring(row, { kind: 'attach', part: 'roller', face: 'bottom', to: { part: 'frame', face: 'top' } })
92
+ row = fm.applyFigureAuthoring(row, { kind: 'make-repeat', part: 'roller', axis: 'x', pitch: 200 })
93
93
  assert.match(why(row, 'frame')!, /part-row-along .*"part":"frame".*"rows":"roller"/, '줄이 따라 놓인 부품')
94
94
 
95
95
  /* A refusal no sentence knows is still said as a refusal, with its code -- never the kernel's English. */
@@ -99,37 +99,37 @@ test('지우기는 매단 것 · 따라 움직이는 것을 풀어 남기고 그
99
99
  })
100
100
 
101
101
  test('공간 선언에 든 부품 — 지우고 공간을 남은 부품에 맞추는 길을 한 번에 권한다(차단기의 떠도는 상자, 09-25)', async () => {
102
- const { inDeclaredRoom } = await import('../client/modeller/v3-refusals.ts')
102
+ const { inDeclaredRoom } = await import('../client/modeller/refusals.ts')
103
103
  const fm = await import('@hatiolab/figure-model')
104
- let a = fm.addV3Box(fm.createV3Asset('Gate'), { id: 'housing', size: { x: 300, y: 1000, z: 300 }, position: { x: 0, y: 0, z: 0 } })
105
- a = fm.addV3Box(a, { id: 'stray', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 } })
106
- a = fm.applyV3Authoring(a, { kind: 'attach', part: 'housing', face: 'bottom', to: { plane: 'mounting-plane' } })
107
- a = fm.applyV3Authoring(a, { kind: 'attach', part: 'stray', face: 'bottom', to: { plane: 'mounting-plane' } })
108
- a = fm.applyV3Authoring(a, { kind: 'declare-occupancy', proposal: fm.proposeV3Occupancy(a), placement: 'floor' })
104
+ let a = fm.addFigureBox(fm.createFigureAsset('Gate'), { id: 'housing', size: { x: 300, y: 1000, z: 300 }, position: { x: 0, y: 0, z: 0 } })
105
+ a = fm.addFigureBox(a, { id: 'stray', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 } })
106
+ a = fm.applyFigureAuthoring(a, { kind: 'attach', part: 'housing', face: 'bottom', to: { plane: 'mounting-plane' } })
107
+ a = fm.applyFigureAuthoring(a, { kind: 'attach', part: 'stray', face: 'bottom', to: { plane: 'mounting-plane' } })
108
+ a = fm.applyFigureAuthoring(a, { kind: 'declare-occupancy', proposal: fm.proposeFigureOccupancy(a), placement: 'floor' })
109
109
  let refused: unknown
110
110
  try {
111
- fm.editV3Part(a, { kind: 'remove-part', target: 'stray' })
111
+ fm.editFigurePart(a, { kind: 'remove-part', target: 'stray' })
112
112
  } catch (e) {
113
113
  refused = e
114
114
  }
115
115
  assert.equal(inDeclaredRoom(refused), true)
116
116
  assert.equal(inDeclaredRoom(Object.assign(new Error('PART_REFERENCED: top: top holds leg; take it off first'), { code: 'PART_REFERENCED' })), false, '매단 것은 이 길이 아니다')
117
- const after = fm.editV3Part(a, { kind: 'remove-part', target: 'stray', refitRoom: true })
117
+ const after = fm.editFigurePart(a, { kind: 'remove-part', target: 'stray', refitRoom: true })
118
118
  assert.equal(after.document.model.nodes.some(n => n.id === 'stray'), false)
119
119
  assert.ok(after.occupancy?.bounds, '공간은 남은 부품으로 다시 선언된다')
120
- const editor = readFileSync(fileURLToPath(new URL('../client/modeller/v3-editor.ts', import.meta.url)), 'utf8')
120
+ const editor = readFileSync(fileURLToPath(new URL('../client/modeller/editor.ts', import.meta.url)), 'utf8')
121
121
  assert.ok(editor.includes("if (!refitRoom && inDeclaredRoom(error)) this.retry = { label: i18next.t('figure.button.remove-and-refit-room', { part: id }), run: () => this.removePart(id, true) }"))
122
122
  assert.ok(ko['figure.button.remove-and-refit-room'].includes('{part}'))
123
123
  })
124
124
 
125
125
  test('배치를 바꿀 수 없는 까닭 — 평면에 붙은 부품과 새 평면이 받는 면을 말한다(벽 배치, 09-25)', async () => {
126
- const { placementRefusalOf } = await import('../client/modeller/v3-refusals.ts')
126
+ const { placementRefusalOf } = await import('../client/modeller/refusals.ts')
127
127
  const fm = await import('@hatiolab/figure-model')
128
- let a = fm.addV3Box(fm.createV3Asset('Post'), { id: 'post', size: { x: 100, y: 1000, z: 100 }, position: { x: 0, y: 0, z: 0 } })
129
- a = fm.applyV3Authoring(a, { kind: 'attach', part: 'post', face: 'bottom', to: { plane: 'mounting-plane' } })
128
+ let a = fm.addFigureBox(fm.createFigureAsset('Post'), { id: 'post', size: { x: 100, y: 1000, z: 100 }, position: { x: 0, y: 0, z: 0 } })
129
+ a = fm.applyFigureAuthoring(a, { kind: 'attach', part: 'post', face: 'bottom', to: { plane: 'mounting-plane' } })
130
130
  let refused: unknown
131
131
  try {
132
- fm.applyV3Authoring(a, { kind: 'declare-occupancy', proposal: fm.proposeV3Occupancy(a), placement: 'wall' as never })
132
+ fm.applyFigureAuthoring(a, { kind: 'declare-occupancy', proposal: fm.proposeFigureOccupancy(a), placement: 'wall' as never })
133
133
  } catch (e) {
134
134
  refused = e
135
135
  }
@@ -137,14 +137,14 @@ test('배치를 바꿀 수 없는 까닭 — 평면에 붙은 부품과 새 평
137
137
  assert.ok(said.includes('post (bottom)') && said.includes('벽 기반') && said.includes('back'), said)
138
138
  assert.equal(placementRefusalOf(new Error('x'), say), null)
139
139
  /* The placement list sends its command through the page, not the editor: the page says it in words too, and the list goes back. */
140
- const page = readFileSync(fileURLToPath(new URL('../client/pages/figure-v3-modeller-page.ts', import.meta.url)), 'utf8')
140
+ const page = readFileSync(fileURLToPath(new URL('../client/pages/figure-modeller-page.ts', import.meta.url)), 'utf8')
141
141
  assert.ok(page.includes('placementRefusalOf(error, (key, params) => i18next.t(key, params))'), '페이지 경로도 사람의 말로')
142
- const props = readFileSync(fileURLToPath(new URL('../client/modeller/figure-v3-properties.ts', import.meta.url)), 'utf8')
142
+ const props = readFileSync(fileURLToPath(new URL('../client/modeller/figure-properties.ts', import.meta.url)), 'utf8')
143
143
  assert.ok(props.includes('select.value = current'), '거절되면 목록이 원래 배치로 돌아온다')
144
144
  })
145
145
 
146
146
  test('범위 밖 값은 받을 수 있는 범위를 숫자로 말한다 — 커널의 영어가 아니다 (Figure 레인 :3300, 2026-09-26)', async () => {
147
- const { inputRangeRefusalOf } = await import('../client/modeller/v3-refusals.ts')
147
+ const { inputRangeRefusalOf } = await import('../client/modeller/refusals.ts')
148
148
  const range = (message: string) => Object.assign(new Error(message), { code: 'INPUT_RANGE' })
149
149
  const size = inputRangeRefusalOf(range('INPUT_RANGE: size.x: 6021100 outside [300,1200]'), say)!
150
150
  assert.ok(size.includes('X') && size.includes('300') && size.includes('1,200') && size.includes('6,021,100'), size)
@@ -156,7 +156,7 @@ test('범위 밖 값은 받을 수 있는 범위를 숫자로 말한다 — 커
156
156
  })
157
157
 
158
158
  test('레시피가 막히면 몇 번째 단계에서 왜 막혔는지 사람의 말로 — 커널의 영어가 아니라 (2026-09-27)', async () => {
159
- const { recipeRefusalOf } = await import('../client/modeller/v3-refusals.ts')
159
+ const { recipeRefusalOf } = await import('../client/modeller/refusals.ts')
160
160
  const say = (key: string, p: Record<string, string>) => `${key} ${JSON.stringify(p)}`
161
161
  const refusal = Object.assign(new Error('EDIT_TARGET: steps[2]: attach: door is wider than the opening'), { code: 'EDIT_TARGET', path: 'steps[2]' })
162
162
  const told = recipeRefusalOf(refusal, say)!
@@ -1,8 +1,8 @@
1
1
  /*
2
- * **V3 화면에서 발행할 수 있다** (ADR-0087 5단계의 선행 조건, 2026-09-24).
2
+ * **화면에서 발행할 수 있다** (ADR-0087 5단계의 선행 조건, 2026-09-24).
3
3
  *
4
- * V3 모델러에는 발행 패널이 없었다 — 저장은 되고 발행 · 판본 · 복원 · 판정을 볼 자리가 없었다. 복사본 검증의
5
- * 「32 개 발행 통과」는 스크립트가 서버의 문을 직접 부른 것이지 화면이 아니었다. 그리고 새로 만든 V3 도형은 전부
4
+ * 모델러에는 발행 패널이 없었다 — 저장은 되고 발행 · 판본 · 복원 · 판정을 볼 자리가 없었다. 복사본 검증의
5
+ * 「32 개 발행 통과」는 스크립트가 서버의 문을 직접 부른 것이지 화면이 아니었다. 그리고 새로 만든 도형은 전부
6
6
  * 「공간 선언 없음」으로 발행이 막히는데, 그 선언을 할 단추가 화면 어디에도 없었다. 5단계에서 `figure-modeller`
7
7
  * 가 이 화면으로 바뀌면 새 도형은 영영 발행할 수 없었다.
8
8
  *
@@ -15,37 +15,37 @@ import { readFileSync } from 'node:fs'
15
15
  import { fileURLToPath } from 'node:url'
16
16
 
17
17
  import {
18
- addV3Box,
19
- applyV3Authoring,
20
- createV3Asset,
21
- inspectV3Asset,
22
- proposeV3Occupancy,
23
- serializeV3Asset
18
+ addFigureBox,
19
+ applyFigureAuthoring,
20
+ createFigureAsset,
21
+ inspectFigureAsset,
22
+ proposeFigureOccupancy,
23
+ serializeFigureAsset
24
24
  } from '@hatiolab/figure-model'
25
25
 
26
- import { occupancyFromParts } from '../client/modeller/v3-occupancy.ts'
26
+ import { occupancyFromParts } from '../client/modeller/occupancy.ts'
27
27
 
28
- const codes = (asset: Parameters<typeof inspectV3Asset>[0]) => inspectV3Asset(asset).violations.map(v => v.code)
28
+ const codes = (asset: Parameters<typeof inspectFigureAsset>[0]) => inspectFigureAsset(asset).violations.map(v => v.code)
29
29
 
30
- test('새로 만든 V3 도형의 「공간 선언 없음」을 「부품에 맞추기」 한 번이 푼다', () => {
31
- let asset = createV3Asset('Probe')
32
- asset = addV3Box(asset, { id: 'body', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 } })
30
+ test('새로 만든 도형의 「공간 선언 없음」을 「부품에 맞추기」 한 번이 푼다', () => {
31
+ let asset = createFigureAsset('Probe')
32
+ asset = addFigureBox(asset, { id: 'body', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 } })
33
33
  assert.deepEqual(codes(asset), ['OCCUPANCY_UNDECLARED'], '전제: 선언 없는 도형은 막힌다')
34
34
 
35
- const fitted = applyV3Authoring(asset, occupancyFromParts(serializeV3Asset(asset)))
35
+ const fitted = applyFigureAuthoring(asset, occupancyFromParts(serializeFigureAsset(asset)))
36
36
 
37
37
  assert.deepEqual(codes(fitted), [])
38
38
  assert.equal(fitted.occupancy?.placement, 'floor', '처음 선언하는 도형은 바닥에 선다')
39
39
  })
40
40
 
41
41
  test('선언한 공간을 넘은 부품도 같은 한 번으로 풀리고, 배치 기준은 지킨다', () => {
42
- let asset = createV3Asset('Probe')
43
- asset = addV3Box(asset, { id: 'body', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 } })
44
- asset = applyV3Authoring(asset, { kind: 'declare-occupancy', proposal: proposeV3Occupancy(asset, { over: 'range' }), placement: 'center' })
45
- asset = addV3Box(asset, { id: 'arm', size: { x: 100, y: 100, z: 100 }, position: { x: 900, y: 50, z: 0 } })
42
+ let asset = createFigureAsset('Probe')
43
+ asset = addFigureBox(asset, { id: 'body', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 } })
44
+ asset = applyFigureAuthoring(asset, { kind: 'declare-occupancy', proposal: proposeFigureOccupancy(asset, { over: 'range' }), placement: 'center' })
45
+ asset = addFigureBox(asset, { id: 'arm', size: { x: 100, y: 100, z: 100 }, position: { x: 900, y: 50, z: 0 } })
46
46
  assert.ok(codes(asset).includes('PART_OUTSIDE_OCCUPANCY'), '전제: 넘은 부품은 막힌다')
47
47
 
48
- const fitted = applyV3Authoring(asset, occupancyFromParts(serializeV3Asset(asset)))
48
+ const fitted = applyFigureAuthoring(asset, occupancyFromParts(serializeFigureAsset(asset)))
49
49
 
50
50
  assert.deepEqual(codes(fitted), [])
51
51
  assert.equal(fitted.occupancy?.placement, 'center', '이미 선언된 배치 기준을 바꾸지 않는다')
@@ -54,7 +54,7 @@ test('선언한 공간을 넘은 부품도 같은 한 번으로 풀리고, 배
54
54
  const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
55
55
 
56
56
  test('모델러가 발행 패널을 걸고, 패널이 내는 셋을 받는다', () => {
57
- const page = read('../client/pages/figure-v3-modeller-page.ts')
57
+ const page = read('../client/pages/figure-modeller-page.ts')
58
58
 
59
59
  assert.ok(/<figure-history-panel/.test(page), '패널을 건다')
60
60
  for (const event of ['fit-base-to-parts', 'figure-released', 'figure-reverted']) {
@@ -0,0 +1,24 @@
1
+ import {test} from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import * as THREE from 'three'
4
+ import {replaceFigureMeshes} from '../client/modeller/renderer.ts'
5
+ import type {FigureRenderPart} from '../client/modeller/renderer.ts'
6
+ const box=(id:string):FigureRenderPart=>({id,shape:{provider:'box',dimensions:{width:100,height:200,depth:300}},pose:{r:[[1,0,0],[0,1,0],[0,0,1]],t:[0,100,0]},material:{token:'primary'}})
7
+ test('renderer replaces rather than accumulates; empty scene disposes old resources',()=>{
8
+ const group=new THREE.Group();replaceFigureMeshes(group,[box('a'),box('b')],{primary:'#123456'})
9
+ assert.equal(group.children.length,2);let disposed=0
10
+ const first=group.children[0] as THREE.Mesh;first.geometry.addEventListener('dispose',()=>disposed++)
11
+ replaceFigureMeshes(group,[box('c')],{primary:'#123456'});assert.equal(group.children.length,1);assert.equal(group.children[0].name,'c');assert.equal(disposed,1)
12
+ replaceFigureMeshes(group,[],{});assert.equal(group.children.length,0)
13
+ })
14
+ test('renderer preserves scene on unsupported geometry and resolves pose/material',()=>{
15
+ const group=new THREE.Group();replaceFigureMeshes(group,[box('a')],{primary:'#123456'})
16
+ const old=group.children[0],bounds=new THREE.Box3().setFromObject(group)
17
+ assert.equal(bounds.min.y,0);assert.equal(bounds.max.y,200)
18
+ assert.throws(()=>replaceFigureMeshes(group,[box('b'),{...box('c'),shape:{provider:'unknown',dimensions:{}}}],{primary:'#123456'}),/Unsupported/)
19
+ assert.equal(group.children[0],old)
20
+ const p=box('glow');p.material={token:'primary',preset:'metal',transparent:true,emissive:{token:'primary',intensity:2,on:true}}
21
+ replaceFigureMeshes(group,[p],{primary:'#123456'})
22
+ const material=(group.children[0] as THREE.Mesh).material as THREE.MeshStandardMaterial
23
+ assert.equal(material.metalness,0.8);assert.equal(material.transparent,true);assert.equal(material.emissiveIntensity,2)
24
+ })