@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
@@ -0,0 +1,25 @@
1
+ # V3 제품 현황
2
+
3
+ 형식은 V3(`figure-asset-1`) 하나다. V1·V2 형식은 2026-09-26 코드와 명세에서 제거했다.
4
+
5
+ ## 제품 코드
6
+
7
+ - `client/modeller/product-session.ts`: 모델 ID·로드·저장 경계를 workspace에 연결한다. 다른 모델의 지연 응답을 버리며 로드 실패 시 이전 형상을 노출하지 않는다. UI/AI는 같은 리비전과 undo/redo를 사용한다. AI 편집은 요청 당시 context/revision을 필수로 받는다.
8
+ - `client/modeller/canvas.ts`: things-scene 편집 모드의 판 위에 평가된 V3 형상을 부품마다 `figure-part` 컴포넌트로 세운다. 새 문맥에서 렌더링이 실패하면 이전 모델을 지운다.
9
+ - `client/modeller/editor.ts`: 부품 목록 · 캔버스 · 오른쪽 탭(속성 · 능력 · 움직임 · 판정 · 환경 · 이력). 실행 취소/다시 실행과 저장 연결.
10
+ - `client/graphql/store.ts`: 정본을 그대로 `saveFigure`로 보낸다.
11
+ - figure-service의 `saveFigure`: 도메인 권한 검사, V3 검증, 정확한 이전 정본을 조건으로 하는 atomic UPDATE, 생성 요청 ID를 이용한 재시도. SQLite에서 지원하지 않는 행 잠금은 사용하지 않는다. 재지 않은 성능 점수/삼각형 수는 null로 둔다. 실제 운영 DB 저장 테스트는 아직 하지 않았다.
12
+ - `client/pages/figure-modeller-page.ts`: 위 요소를 묶는 페이지. `route.ts`의 `figure-modeller`와 `figure-modeller`가 이 페이지를 연다.
13
+
14
+ ## 남은 항목
15
+
16
+ 1. operato-figure의 AI-dock과 figure-service 도구/제안 생성기를 V3 정본 및 승인된 트랜잭션 계약으로 연결한다. 페이지 이벤트만 추가한 것은 AI 통합 완료가 아니다.
17
+ 2. 발행 게이트, 썸네일, 카탈로그/배치 소비처가 V3 평가 결과를 사용하는지 확인한다.
18
+
19
+ ## 검증
20
+
21
+ - figure-ui `npm test`: 251개 통과(2026-09-26).
22
+ - SQLite in-memory 테스트: 테넌트 경계·이전 정본 비교·오래된 편집 거절. 실제 TypeORM 리졸버/제품 DB end-to-end 검증을 대체하지 않는다.
23
+ - 브라우저 검사(`npm run test:browser`): 기즈모 끌기 · 자세 슬라이더를 실제 마우스로 조작한다.
24
+
25
+ 배포는 사용자가 담당한다.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/figure-ui",
3
- "version": "10.1.75",
3
+ "version": "10.1.77",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "@things-factory:registry": "https://registry.npmjs.org"
@@ -27,8 +27,8 @@
27
27
  "clean": "npm run clean:server && npm run clean:client"
28
28
  },
29
29
  "dependencies": {
30
- "@hatiolab/figure-model": "^0.1.92",
31
- "@hatiolab/things-scene": "^10.1.60",
30
+ "@hatiolab/figure-model": "^0.1.93",
31
+ "@hatiolab/things-scene": "^10.1.61",
32
32
  "@material/web": "^2.0.0",
33
33
  "@operato/data-grist": "^10.0.0",
34
34
  "@operato/graphql": "^10.0.0",
@@ -41,8 +41,8 @@
41
41
  "@operato/styles": "^10.0.0",
42
42
  "@operato/utils": "^10.0.0",
43
43
  "@things-factory/ai-assistant": "^10.1.58",
44
- "@things-factory/figure-service": "^10.1.75",
44
+ "@things-factory/figure-service": "^10.1.76",
45
45
  "three": "^0.185.1"
46
46
  },
47
- "gitHead": "931154ec5ab1b4d3bd2530a9a689363157f21f5f"
47
+ "gitHead": "f5ff07816a0f96325cc599c73d7fba06dd719c6f"
48
48
  }
@@ -6,10 +6,10 @@ import { test } from 'node:test'
6
6
  import assert from 'node:assert/strict'
7
7
  import { readFileSync } from 'node:fs'
8
8
  import { fileURLToPath } from 'node:url'
9
- import { createV3Asset, addV3Box, serializeV3Asset } from '@hatiolab/figure-model'
9
+ import { createFigureAsset, addFigureBox, serializeFigureAsset } from '@hatiolab/figure-model'
10
10
 
11
- import { createV3ProductSession } from '../client/modeller/v3-product-session.js'
12
- import { pendNewFigure, runFigureProposal, takePendingNewFigure, type FigureApplyDetail } from '../client/modeller/v3-ai-proposal.js'
11
+ import { createFigureProductSession } from '../client/modeller/product-session.js'
12
+ import { pendNewFigure, runFigureProposal, takePendingNewFigure, type FigureApplyDetail } from '../client/modeller/ai-proposal.js'
13
13
 
14
14
  const ko = JSON.parse(readFileSync(fileURLToPath(new URL('../translations/ko.json', import.meta.url)), 'utf8'))
15
15
  const say = (key: string, params: Record<string, string> = {}) => {
@@ -18,7 +18,7 @@ const say = (key: string, params: Record<string, string> = {}) => {
18
18
  }
19
19
 
20
20
  const source = () =>
21
- serializeV3Asset(addV3Box(createV3Asset('A'), { id: 'body', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 } }))
21
+ serializeFigureAsset(addFigureBox(createFigureAsset('A'), { id: 'body', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 } }))
22
22
  const store = {
23
23
  load: async (id: string) => ({ id, source: source() }),
24
24
  save: async (q: { id?: string; source: string }) => ({ id: q.id ?? 'created', source: q.source })
@@ -30,7 +30,7 @@ const steps = [
30
30
  ] as any[]
31
31
 
32
32
  test('a proposal is one step of the history on the version the model saw, undone whole', async () => {
33
- const s = createV3ProductSession(store)
33
+ const s = createFigureProductSession(store)
34
34
  await s.open('A')
35
35
  const seen = s.read()
36
36
  s.applyProposal(steps, seen.revision!, seen.context!)
@@ -43,7 +43,7 @@ test('a proposal is one step of the history on the version the model saw, undone
43
43
  })
44
44
 
45
45
  test('a person\'s edit after the proposal was asked makes it stale -- refused, nothing applied', async () => {
46
- const s = createV3ProductSession(store)
46
+ const s = createFigureProductSession(store)
47
47
  await s.open('A')
48
48
  const seen = s.read()
49
49
  s.apply([{ kind: 'set-design', input: 'body.size.y', unit: 'mm', value: 150 }])
@@ -1,7 +1,7 @@
1
1
  /*
2
2
  * **부품 맞추기 — 가운데 · 바닥 · 대칭 · 눈금** (2026-09-24).
3
3
  *
4
- * 「맞추기」 여섯 단추. V3 의 틀은 바닥이 y 0, X · Z 가 0 에 가운데다. 각 도구가 부품의 **실제로 차지하는
4
+ * 「맞추기」 여섯 단추. 이 모델의 틀은 바닥이 y 0, X · Z 가 0 에 가운데다. 각 도구가 부품의 **실제로 차지하는
5
5
  * 상자**(돌린 부품이면 돈 부피)를 기준으로 맞추는지, 그 결과를 잰다.
6
6
  */
7
7
  import { test } from 'node:test'
@@ -9,15 +9,15 @@ 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, createV3Asset, editV3Part } from '@hatiolab/figure-model'
12
+ import { addFigureBox, createFigureAsset, editFigurePart } from '@hatiolab/figure-model'
13
13
 
14
- import { alignChanges, boxOf, canMirror, gridStepOf, mirrorChanges, niceStep } from '../client/modeller/v3-align.ts'
15
- import { gizmoReachOf } from '../client/modeller/v3-gizmo.ts'
14
+ import { alignChanges, boxOf, canMirror, gridStepOf, mirrorChanges, niceStep } from '../client/modeller/align.ts'
15
+ import { gizmoReachOf } from '../client/modeller/gizmo.ts'
16
16
 
17
17
  const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
18
18
  const probe = (at = { x: 130, y: 90, z: -70 }, rotation?: { x: number; y: number; z: number }) => {
19
- let asset = createV3Asset('Probe')
20
- asset = addV3Box(asset, { id: 'body', size: { x: 200, y: 100, z: 100 }, position: at, ...(rotation ? { rotation } : {}) })
19
+ let asset = createFigureAsset('Probe')
20
+ asset = addFigureBox(asset, { id: 'body', size: { x: 200, y: 100, z: 100 }, position: at, ...(rotation ? { rotation } : {}) })
21
21
  return asset
22
22
  }
23
23
  const applied = (asset: ReturnType<typeof probe>, changes: ReturnType<typeof alignChanges>) => {
@@ -52,7 +52,7 @@ test('눈금은 1 · 2 · 5 간격으로 자리와 크기를 맞추고, 크기
52
52
 
53
53
  test('대칭은 복제본을 가운데 면 반대편 같은 거리에 둔다 · 가운데에 있으면 내지 않는다', () => {
54
54
  const asset = probe()
55
- const copied = editV3Part(asset, { kind: 'duplicate-part', target: 'body' })
55
+ const copied = editFigurePart(asset, { kind: 'duplicate-part', target: 'body' })
56
56
  const mirrored = applied(copied, mirrorChanges(copied, 'body', 'body-copy', 'x'))
57
57
  assert.equal(Math.round(boxOf(mirrored, 'body-copy')!.centre.x), -Math.round(boxOf(mirrored, 'body')!.centre.x))
58
58
  assert.equal(canMirror(probe({ x: 0, y: 50, z: 0 }) as never, 'body', 'x', gizmoReachOf(probe() as never, 'body')), false)
@@ -60,7 +60,7 @@ test('대칭은 복제본을 가운데 면 반대편 같은 거리에 둔다 ·
60
60
 
61
61
  test('붙인 부품처럼 옮길 수 없는 것은 맞추기도 없다', () => {
62
62
  assert.deepEqual(alignChanges(probe(), 'body', { translate: false, rotate: false, scale: false }, 'floor'), [])
63
- assert.match(read('../client/modeller/v3-editor.ts'), /if \(!this\.selected \|\| !reach\.translate\) return null/)
63
+ assert.match(read('../client/modeller/editor.ts'), /if \(!this\.selected \|\| !reach\.translate\) return null/)
64
64
  })
65
65
 
66
66
  test('the grid step reads well -- 27 or 33 would make coordinates unreadable -- and is never 0', () => {
@@ -1,12 +1,12 @@
1
1
  /*
2
- * The page the browser check drives: the V3 editor as the modeller page mounts it, over a store in memory, so the check
2
+ * The page the browser check drives: the editor as the modeller page mounts it, over a store in memory, so the check
3
3
  * needs no server and no sign-in. Everything the editor draws and does is the product's own code, emitted fresh from
4
4
  * `client/` by the runner.
5
5
  */
6
- import 'figure-ui-client/modeller/v3-editor.js'
7
- import { budgetOf, v3ReportOf } from 'figure-ui-client/modeller/v3-maturity.js'
8
- import { createV3ProductSession } from 'figure-ui-client/modeller/v3-product-session.js'
9
- import { addV3Box, addV3Primitive, applyV3Authoring, createV3Asset, parseV3Asset, robotArmV3, serializeV3Asset } from '@hatiolab/figure-model'
6
+ import 'figure-ui-client/modeller/editor.js'
7
+ import { budgetOf, figureReportOf } from 'figure-ui-client/modeller/maturity.js'
8
+ import { createFigureProductSession } from 'figure-ui-client/modeller/product-session.js'
9
+ import { addFigureBox, addFigurePrimitive, applyFigureAuthoring, createFigureAsset, parseFigureAsset, robotArmFigure, serializeFigureAsset } from '@hatiolab/figure-model'
10
10
 
11
11
  const PALETTE = {
12
12
  'palette.dark': '#2b2b2b',
@@ -18,38 +18,38 @@ const PALETTE = {
18
18
 
19
19
  function sourceOf(kind, text) {
20
20
  if (text) return text
21
- if (kind === 'arm') return serializeV3Asset(robotArmV3())
21
+ if (kind === 'arm') return serializeFigureAsset(robotArmFigure())
22
22
  if (kind === 'box') {
23
- const asset = createV3Asset('Probe')
23
+ const asset = createFigureAsset('Probe')
24
24
  asset.palette = { ...asset.palette, ...PALETTE }
25
- return serializeV3Asset(addV3Box(asset, { id: 'box', size: { x: 400, y: 400, z: 400 }, position: { x: 0, y: 200, z: 0 } }))
25
+ return serializeFigureAsset(addFigureBox(asset, { id: 'box', size: { x: 400, y: 400, z: 400 }, position: { x: 0, y: 200, z: 0 } }))
26
26
  }
27
27
  if (kind === 'door') {
28
28
  /* A frame, and a leaf set off it: 250 mm to the side and 100 mm in front, by its own numbers. */
29
- let asset = createV3Asset('Door')
29
+ let asset = createFigureAsset('Door')
30
30
  asset.palette = { ...asset.palette, ...PALETTE }
31
- asset = addV3Box(asset, { id: 'frame', size: { x: 1600, y: 100, z: 150 }, position: { x: 0, y: 50, z: 0 } })
32
- return serializeV3Asset(addV3Box(asset, { id: 'leaf', size: { x: 1400, y: 2200, z: 40 }, position: { x: 250, y: 1100, z: 100 } }))
31
+ asset = addFigureBox(asset, { id: 'frame', size: { x: 1600, y: 100, z: 150 }, position: { x: 0, y: 50, z: 0 } })
32
+ return serializeFigureAsset(addFigureBox(asset, { id: 'leaf', size: { x: 1400, y: 2200, z: 40 }, position: { x: 250, y: 1100, z: 100 } }))
33
33
  }
34
34
  if (kind === 'room') {
35
35
  /* A room a recipe is put on: a 4 x 3 x 6 m box standing on the floor. */
36
- const asset = createV3Asset('Room')
36
+ const asset = createFigureAsset('Room')
37
37
  asset.palette = { ...asset.palette, ...PALETTE }
38
- return serializeV3Asset(addV3Box(asset, { id: 'room', size: { x: 4000, y: 3000, z: 6000 }, position: { x: 0, y: 1500, z: 0 } }))
38
+ return serializeFigureAsset(addFigureBox(asset, { id: 'room', size: { x: 4000, y: 3000, z: 6000 }, position: { x: 0, y: 1500, z: 0 } }))
39
39
  }
40
40
  if (kind === 'rollers') {
41
41
  /* A roller row whose rollers spin in place, each carrying a mark (ADR-0093, 2026-09-26). */
42
- let asset = createV3Asset('Rollers')
42
+ let asset = createFigureAsset('Rollers')
43
43
  asset.palette = { ...asset.palette, ...PALETTE }
44
- asset = addV3Box(asset, { id: 'bed', size: { x: 2000, y: 400, z: 600 }, position: { x: 0, y: 200, z: 0 } })
45
- asset = applyV3Authoring(asset, { kind: 'attach', part: 'bed', face: 'bottom', to: { plane: 'mounting-plane' } })
46
- asset = addV3Primitive(asset, { id: 'roller', kind: 'cylinder', dimensions: { radius: 40, length: 560 }, position: { x: 0, y: 0, z: 0 }, rotation: { x: 90, y: 0, z: 0 } })
47
- asset = applyV3Authoring(asset, { kind: 'attach', part: 'roller', face: 'centre', to: { part: 'bed', face: 'top' }, gap: 40 })
48
- asset = addV3Box(asset, { id: 'mark', size: { x: 30, y: 30, z: 560 }, position: { x: 0, y: 0, z: 0 } })
49
- asset = applyV3Authoring(asset, { kind: 'attach', part: 'mark', face: 'left', to: { part: 'roller', face: 'right' } })
44
+ asset = addFigureBox(asset, { id: 'bed', size: { x: 2000, y: 400, z: 600 }, position: { x: 0, y: 200, z: 0 } })
45
+ asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'bed', face: 'bottom', to: { plane: 'mounting-plane' } })
46
+ asset = addFigurePrimitive(asset, { id: 'roller', kind: 'cylinder', dimensions: { radius: 40, length: 560 }, position: { x: 0, y: 0, z: 0 }, rotation: { x: 90, y: 0, z: 0 } })
47
+ asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'roller', face: 'centre', to: { part: 'bed', face: 'top' }, gap: 40 })
48
+ asset = addFigureBox(asset, { id: 'mark', size: { x: 30, y: 30, z: 560 }, position: { x: 0, y: 0, z: 0 } })
49
+ asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'mark', face: 'left', to: { part: 'roller', face: 'right' } })
50
50
  const j = asset.joints.find(o => o.body1.part === 'roller')
51
- asset = applyV3Authoring(asset, { kind: 'set-joint', joint: { ...j, type: 'revolute', axis: 'Z', lowerLimit: -360, upperLimit: 360, state: { id: 'spin' } } })
52
- return serializeV3Asset(applyV3Authoring(asset, { kind: 'make-repeat', part: 'roller', axis: 'x', pitch: 200 }))
51
+ asset = applyFigureAuthoring(asset, { kind: 'set-joint', joint: { ...j, type: 'revolute', axis: 'Z', lowerLimit: -360, upperLimit: 360, state: { id: 'spin' } } })
52
+ return serializeFigureAsset(applyFigureAuthoring(asset, { kind: 'make-repeat', part: 'roller', axis: 'x', pitch: 200 }))
53
53
  }
54
54
  throw Error(`no figure for ${kind}`)
55
55
  }
@@ -60,8 +60,8 @@ function deep(selector, root = document) {
60
60
  return found
61
61
  }
62
62
 
63
- const editor = () => document.querySelector('figure-v3-editor')
64
- const canvas = () => deep('figure-v3-canvas')[0]
63
+ const editor = () => document.querySelector('figure-editor')
64
+ const canvas = () => deep('figure-canvas')[0]
65
65
  const three = () => canvas().scene.root._threeCapability
66
66
  const controls = () => three().gizmoManager.controls
67
67
 
@@ -244,28 +244,28 @@ window.probe = {
244
244
  window.harness = {
245
245
  async mount(kind, text, templates = [], guides = {}) {
246
246
  const source = sourceOf(kind, text)
247
- const session = createV3ProductSession({
247
+ const session = createFigureProductSession({
248
248
  load: async id => ({ id, source }),
249
249
  save: async request => ({ id: request.id ?? 'probe', source: request.source })
250
250
  })
251
251
  await session.open('probe')
252
- const editor = document.createElement('figure-v3-editor')
252
+ const editor = document.createElement('figure-editor')
253
253
  editor.session = session
254
- /* The cost line as the modeller page gives it: the maturity report of what is authored (figure-v3-verdict). */
254
+ /* The cost line as the modeller page gives it: the maturity report of what is authored (figure-verdict). */
255
255
  const report = () => {
256
256
  try {
257
- return budgetOf(v3ReportOf(parseV3Asset(session.source())))
257
+ return budgetOf(figureReportOf(parseFigureAsset(session.source())))
258
258
  } catch {
259
259
  return undefined
260
260
  }
261
261
  }
262
262
  editor.budget = report()
263
- editor.addEventListener('v3-changed', () => (editor.budget = report()))
263
+ editor.addEventListener('changed', () => (editor.budget = report()))
264
264
  editor.templates = templates
265
265
  editor.guideOf = async name => guides[name] ?? ''
266
266
  editor.revision = 0
267
267
  /* The modeller page counts a change the same way: the editor re-reads the session on a new revision. */
268
- editor.addEventListener('v3-changed', () => (editor.revision = editor.revision + 1))
268
+ editor.addEventListener('changed', () => (editor.revision = editor.revision + 1))
269
269
  document.body.replaceChildren(editor)
270
270
  await editor.updateComplete
271
271
  return session.read().revision
@@ -1,5 +1,5 @@
1
1
  /*
2
- * The V3 modeller driven with the mouse, in a real browser.
2
+ * The modeller driven with the mouse, in a real browser.
3
3
  *
4
4
  * The unit suite proves the rules; it cannot prove what happens between a pointer going down on a gizmo handle and
5
5
  * coming up -- the scene writes the component on every move, the editor must not fight it, and the drag must land as
@@ -252,7 +252,7 @@ describe('a pose slider dragged with the mouse (user 2026-09-24: the arm jumped
252
252
  })
253
253
  })
254
254
 
255
- describe('the V3 robot arm: a drag on a part edits its joint (ADR-0093)', () => {
255
+ describe('the robot arm: a drag on a part edits its joint (ADR-0093)', () => {
256
256
  before(async () => {
257
257
  await mount('arm')
258
258
  await click(await read(() => probe.row('upper-arm')))
@@ -348,7 +348,7 @@ describe('the fastening form belongs to the part it is for', () => {
348
348
  await click(await read(() => probe.row('elbow')))
349
349
  /* The elbow opens on its own fastening (walk 1, 2026-09-26), not on the forearm's choices. */
350
350
  const own = await read(() => {
351
- const joint = JSON.parse(document.querySelector('figure-v3-editor').session.source()).joints.find(j => j.body1.part === 'elbow')
351
+ const joint = JSON.parse(document.querySelector('figure-editor').session.source()).joints.find(j => j.body1.part === 'elbow')
352
352
  return { target: joint.body0.part, x: joint.origin?.align?.x ?? 'centre' }
353
353
  })
354
354
  const nextTarget = await alignSelect('figure.label.target-part')
@@ -390,7 +390,7 @@ describe('a figure made in the modeller is given its size from its parts (user 2
390
390
  }, id)
391
391
  const declared = () =>
392
392
  page().evaluate(() => {
393
- const deep = (root = document) => [...root.querySelectorAll('figure-v3-editor')].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
393
+ const deep = (root = document) => [...root.querySelectorAll('figure-editor')].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
394
394
  const inputs = JSON.parse(deep()[0].session.source()).document.model.inputs
395
395
  return Object.fromEntries(inputs.filter(i => i.id.startsWith('size.')).map(i => [i.id, [i.min, i.max]]))
396
396
  })
@@ -479,7 +479,7 @@ describe('a figure made in the modeller is given its size from its parts (user 2
479
479
  assert.equal(after.error, '', 'not refused')
480
480
  assert.equal(after.revision, start.revision + 1, 'one step')
481
481
  const follows = await page().evaluate(() => {
482
- const deep = (root = document) => [...root.querySelectorAll('figure-v3-editor')].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
482
+ const deep = (root = document) => [...root.querySelectorAll('figure-editor')].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
483
483
  const model = JSON.parse(deep()[0].session.source()).document.model
484
484
  return ['x', 'y', 'z'].map(axis => {
485
485
  const ref = String(model.nodes.find(n => n.id === `box.dimension.${axis}`)?.args[0] ?? '')
@@ -499,7 +499,7 @@ describe('a motion is changed where it is (2026-09-25)', () => {
499
499
 
500
500
  test('the shoulder\'s range is changed from the motion tab: one step, the same joint, its new limit', async () => {
501
501
  await click(await read(() => probe.tab('figure.label.motion')))
502
- const jointOf = part => read(part => (JSON.parse(document.querySelector('figure-v3-editor').session.source()).joints ?? []).find(j => j.body1.part === part) ?? null, part)
502
+ const jointOf = part => read(part => (JSON.parse(document.querySelector('figure-editor').session.source()).joints ?? []).find(j => j.body1.part === part) ?? null, part)
503
503
  const was = await jointOf('upper-arm')
504
504
  assert.ok(was, 'the upper arm has its joint')
505
505
  const button = async text =>
@@ -514,7 +514,7 @@ describe('a motion is changed where it is (2026-09-25)', () => {
514
514
  await click(await button('figure.button.change-motion'))
515
515
  const max = await page().evaluateHandle(() => {
516
516
  const deep = (root = document) => [...root.querySelectorAll('input[type=number]')].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
517
- const inputs = deep().filter(i => i.closest('div[pair]') && i.getRootNode().host?.tagName === 'FIGURE-V3-LINKS')
517
+ const inputs = deep().filter(i => i.closest('div[pair]') && i.getRootNode().host?.tagName === 'FIGURE-LINKS')
518
518
  return inputs[inputs.length - 1] ?? null
519
519
  })
520
520
  assert.ok(await max.evaluate(i => !!i), 'the form is open with the range')
@@ -620,9 +620,9 @@ describe('the modeller made easier to find and to keep (2026-09-26 baseline)', (
620
620
  await select.select(control)
621
621
  await sleep(500)
622
622
  assert.equal((await state()).revision, start.revision + 1, 'one step')
623
- const drawn = () => read(() => document.querySelector('figure-v3-editor').session.read().geometry.find(g => g.id === 'upper-arm')?.visible)
623
+ const drawn = () => read(() => document.querySelector('figure-editor').session.read().geometry.find(g => g.id === 'upper-arm')?.visible)
624
624
  const { value, atLeast } = await read(control => {
625
- const asset = JSON.parse(document.querySelector('figure-v3-editor').session.source())
625
+ const asset = JSON.parse(document.querySelector('figure-editor').session.source())
626
626
  const node = asset.document.model.nodes.find(n => n.op === 'at-least@1' && n.args[0] === control)
627
627
  return { value: asset.stateDefaults[control], atLeast: asset.document.model.constants.find(c => c.id === node.args[1]).value }
628
628
  }, control)
@@ -670,7 +670,7 @@ describe('a part follows the figure size in one step (follow-size, 2026-09-26)',
670
670
  })
671
671
  assert.ok(notice?.includes('figure.text.size-declared-to-follow'), `a line says so: ${notice}`)
672
672
  const grown = await read(() => {
673
- const editor = document.querySelector('figure-v3-editor')
673
+ const editor = document.querySelector('figure-editor')
674
674
  editor.session.apply([{ kind: 'set-design', input: 'size.x', unit: 'mm', value: 800 }])
675
675
  return editor.session.read().geometry.find(g => g.id === 'box').shape.dimensions
676
676
  })
@@ -694,7 +694,7 @@ describe('the recipe catalog: a template chosen, filled in, put onto the chosen
694
694
  const r = e.getBoundingClientRect()
695
695
  return { x: r.left + r.width / 2, y: r.top + r.height / 2, disabled: !!e.disabled, text: e.textContent.trim() }
696
696
  }, selector)
697
- const parts = () => read(() => document.querySelector('figure-v3-editor').session.read().geometry.map(g => g.id))
697
+ const parts = () => read(() => document.querySelector('figure-editor').session.read().geometry.map(g => g.id))
698
698
  const type = async (selector, text) => {
699
699
  const field = await page().evaluateHandle(selector => {
700
700
  const deep = (root = document) => [...root.querySelectorAll(selector)].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
@@ -712,7 +712,7 @@ describe('the recipe catalog: a template chosen, filled in, put onto the chosen
712
712
  await sleep(2500)
713
713
  await click(await read(() => probe.row('room')))
714
714
  await click(await at('button[recipe-catalog]'))
715
- assert.ok(await at('figure-v3-recipe-catalog'), 'the catalog is open')
715
+ assert.ok(await at('figure-recipe-catalog'), 'the catalog is open')
716
716
  await click(await at('button[card="hinged-door"]'))
717
717
  /* A search that takes the chosen card away tells the first card it leaves (:3300, 2026-09-26). */
718
718
  await type('input[search]', 'sliding')
@@ -734,13 +734,13 @@ describe('the recipe catalog: a template chosen, filled in, put onto the chosen
734
734
  await type('input[param="share"]', '50')
735
735
  const start = await state()
736
736
  await click(await at('button[insert]'))
737
- assert.equal(await read(() => document.querySelector('figure-v3-editor').recipeError), '', 'not refused')
737
+ assert.equal(await read(() => document.querySelector('figure-editor').recipeError), '', 'not refused')
738
738
  assert.equal((await state()).revision, start.revision + 1, 'one step')
739
739
  const ids = await parts()
740
740
  for (const part of ['frame', 'left', 'right', 'lamp']) assert.ok(ids.includes(`sliding-door-1-${part}`), `${part} is in`)
741
741
  const frame = await body('sliding-door-1-frame')
742
742
  assert.equal(Math.round(frame.dimensions.right - frame.dimensions.left), 2000, "half the room's 4000 mm")
743
- assert.equal(await at('figure-v3-recipe-catalog'), null, 'the catalog closes')
743
+ assert.equal(await at('figure-recipe-catalog'), null, 'the catalog closes')
744
744
  })
745
745
 
746
746
  test('a door the room cannot take is refused in the catalog, with the step it stopped at, and nothing is added', async () => {
@@ -10,7 +10,7 @@ const HTML = `<!DOCTYPE html>
10
10
  <html><head><meta charset="utf-8"><style>
11
11
  html, body { margin: 0; height: 100%; }
12
12
  body { display: flex; font-family: sans-serif; }
13
- figure-v3-editor { flex: 1; min-width: 0; min-height: 0; }
13
+ figure-editor { flex: 1; min-width: 0; min-height: 0; }
14
14
  </style></head>
15
15
  <body><script type="module" src="/page.js"></script></body></html>`
16
16
 
@@ -1,14 +1,14 @@
1
1
  /*
2
2
  * The page that draws recipe template thumbnails (ADR-0095 amended): a model -- the host and the template's parts -- drawn
3
- * the way the modeller draws a figure's card, by the same code (`captureV3Thumbnail`).
3
+ * the way the modeller draws a figure's card, by the same code (`captureFigureThumbnail`).
4
4
  */
5
- import { captureV3Thumbnail } from 'figure-ui-client/modeller/v3-thumbnail.js'
6
- import { drawableV3Parts } from 'figure-ui-client/modeller/v3-renderer.js'
7
- import { compileV3Asset } from '@hatiolab/figure-model'
5
+ import { captureFigureThumbnail } from 'figure-ui-client/modeller/thumbnail.js'
6
+ import { drawableFigureParts } from 'figure-ui-client/modeller/renderer.js'
7
+ import { compileFigureAsset } from '@hatiolab/figure-model'
8
8
 
9
9
  window.harness = { ready: true }
10
10
  window.shoot = async (text, state = {}) => {
11
11
  const asset = JSON.parse(text)
12
- const evaluation = compileV3Asset(asset).evaluate({ ...asset.stateDefaults, ...state })
13
- return await captureV3Thumbnail(drawableV3Parts(evaluation.geometry), evaluation.palette ?? asset.palette ?? {})
12
+ const evaluation = compileFigureAsset(asset).evaluate({ ...asset.stateDefaults, ...state })
13
+ return await captureFigureThumbnail(drawableFigureParts(evaluation.geometry), evaluation.palette ?? asset.palette ?? {})
14
14
  }
@@ -8,25 +8,25 @@ import assert from 'node:assert/strict'
8
8
  import { readFileSync } from 'node:fs'
9
9
  import { fileURLToPath } from 'node:url'
10
10
 
11
- import { addV3Box, applyV3Authoring, createV3Asset, editV3Part } from '@hatiolab/figure-model'
12
- import type { V3Asset } from '@hatiolab/figure-model'
11
+ import { addFigureBox, applyFigureAuthoring, createFigureAsset, editFigurePart } from '@hatiolab/figure-model'
12
+ import type { FigureAsset } from '@hatiolab/figure-model'
13
13
 
14
- import { clipsOf, poseAt, trackEdit, undrivenOf, withKey, withoutKey } from '../client/modeller/v3-clips.ts'
14
+ import { clipsOf, poseAt, trackEdit, undrivenOf, withKey, withoutKey } from '../client/modeller/clips.ts'
15
15
  import { moving } from './joints.ts'
16
16
 
17
17
  const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
18
18
 
19
- const machine = (): V3Asset => {
20
- let asset = createV3Asset('Machine')
21
- asset = addV3Box(asset, { id: 'block', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 } })
19
+ const machine = (): FigureAsset => {
20
+ let asset = createFigureAsset('Machine')
21
+ asset = addFigureBox(asset, { id: 'block', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 } })
22
22
  asset = moving(asset, 'block', { kind: 'slide', axis: 'x', id: 'shift', min: 0, max: 60, start: 0 })
23
- asset = addV3Box(asset, { id: 'arm', size: { x: 40, y: 200, z: 40 }, position: { x: 0, y: 200, z: 0 } })
23
+ asset = addFigureBox(asset, { id: 'arm', size: { x: 40, y: 200, z: 40 }, position: { x: 0, y: 200, z: 0 } })
24
24
  return moving(asset, 'arm', { kind: 'turn', axis: 'y', id: 'spin', min: -180, max: 180, start: 0 })
25
25
  }
26
26
 
27
27
  test('클립마다 길이와 트랙을, 트랙마다 키를 초로 읽는다', () => {
28
- let asset = editV3Part(machine(), { kind: 'set-state-input', input: 'shift', label: '밀기' })
29
- asset = editV3Part(asset, { kind: 'set-driver', state: 'shift', clip: 'cycle', duration: 4, keys: [{ seconds: 0, value: 0 }, { seconds: 2, value: 60 }, { seconds: 4, value: 0 }] })
28
+ let asset = editFigurePart(machine(), { kind: 'set-state-input', input: 'shift', label: '밀기' })
29
+ asset = editFigurePart(asset, { kind: 'set-driver', state: 'shift', clip: 'cycle', duration: 4, keys: [{ seconds: 0, value: 0 }, { seconds: 2, value: 60 }, { seconds: 4, value: 0 }] })
30
30
  assert.deepEqual(undrivenOf(asset), ['spin'], '재생하지 않는 조작값만 새 트랙이 된다')
31
31
  const [clip] = clipsOf(asset)
32
32
  assert.equal(clip!.name, 'cycle')
@@ -35,19 +35,19 @@ test('클립마다 길이와 트랙을, 트랙마다 키를 초로 읽는다', (
35
35
  })
36
36
 
37
37
  test('막대를 옮기면 그 순간의 자세를 한 묶음으로 — 드라이버와 같은 값', () => {
38
- const asset = editV3Part(machine(), { kind: 'set-driver', state: 'shift', clip: 'cycle', duration: 4, keys: [{ seconds: 0, value: 0 }, { seconds: 4, value: 60 }] })
38
+ const asset = editFigurePart(machine(), { kind: 'set-driver', state: 'shift', clip: 'cycle', duration: 4, keys: [{ seconds: 0, value: 0 }, { seconds: 4, value: 60 }] })
39
39
  assert.deepEqual(poseAt(asset, 'cycle', 1), [{ kind: 'set-state', input: 'shift', unit: 'mm', value: 15 }])
40
40
  assert.deepEqual(poseAt(asset, 'nothing', 1), [])
41
41
  })
42
42
 
43
43
  test('키를 그 순간에 더하고 빼며, 쓰면 figure-model 이 받는다 — 양 끝은 남는다', () => {
44
- let asset = editV3Part(machine(), { kind: 'set-driver', state: 'shift', clip: 'cycle', duration: 4, keys: [{ seconds: 0, value: 0 }, { seconds: 4, value: 60 }] })
44
+ let asset = editFigurePart(machine(), { kind: 'set-driver', state: 'shift', clip: 'cycle', duration: 4, keys: [{ seconds: 0, value: 0 }, { seconds: 4, value: 60 }] })
45
45
  const [clip] = clipsOf(asset)
46
46
  const track = clip!.tracks[0]!
47
47
  const keys = withKey(track.keys, { seconds: 2, value: 30 })
48
48
  assert.deepEqual(keys.map(k => k.seconds), [0, 2, 4])
49
49
  assert.deepEqual(withKey(keys, { seconds: 2, value: 45 }).map(k => k.value), [0, 45, 60], '같은 순간이면 바꾼다')
50
- asset = editV3Part(asset, trackEdit(clip!, track, { keys }))
50
+ asset = editFigurePart(asset, trackEdit(clip!, track, { keys }))
51
51
  assert.equal(clipsOf(asset)[0]!.tracks[0]!.keys.length, 3)
52
52
  assert.equal(withoutKey(track.keys, 1), null, '두 개뿐이면 뺄 수 없다')
53
53
  assert.equal(withoutKey(keys, 0), null, '처음 키(0초)는 뺄 수 없다')
@@ -55,13 +55,13 @@ test('키를 그 순간에 더하고 빼며, 쓰면 figure-model 이 받는다
55
55
  })
56
56
 
57
57
  test('움직임 탭에 서고, 막대는 입력 한 묶음 · 나머지는 부품 편집으로 편집기에 닿는다', () => {
58
- const editor = read('../client/modeller/v3-editor.ts')
59
- const start = editor.indexOf('<figure-v3-clips')
60
- const block = editor.slice(start, editor.indexOf('</figure-v3-clips>', start))
58
+ const editor = read('../client/modeller/editor.ts')
59
+ const start = editor.indexOf('<figure-clips')
60
+ const block = editor.slice(start, editor.indexOf('</figure-clips>', start))
61
61
  assert.ok(start >= 0)
62
- assert.ok(block.includes('@v3-states=${(e: CustomEvent) => this.run(() => this.session!.apply(e.detail.changes))}'))
63
- assert.ok(block.includes('@v3-part-edit=${(e: CustomEvent) => this.run(() => this.session!.edit(e.detail.edit))}'))
64
- const clips = read('../client/modeller/figure-v3-clips.ts')
62
+ assert.ok(block.includes('@states=${(e: CustomEvent) => this.run(() => this.session!.apply(e.detail.changes))}'))
63
+ assert.ok(block.includes('@part-edit=${(e: CustomEvent) => this.run(() => this.session!.edit(e.detail.edit))}'))
64
+ const clips = read('../client/modeller/figure-clips.ts')
65
65
  assert.ok(clips.includes("@change=${(e: Event) => this.scrub(clip, number(e))}"), '막대는 놓을 때 한 번')
66
66
  const keys = [...clips.matchAll(/'(figure\.[a-z.-]*[a-z])'/g)].map(m => m[1])
67
67
  for (const lang of ['ko', 'en', 'ja', 'zh', 'ms']) {
@@ -9,25 +9,25 @@ 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, editV3Part } from '@hatiolab/figure-model'
13
- import type { V3Asset } from '@hatiolab/figure-model'
12
+ import { addFigureBox, applyFigureAuthoring, createFigureAsset, editFigurePart } from '@hatiolab/figure-model'
13
+ import type { FigureAsset } from '@hatiolab/figure-model'
14
14
 
15
- import { controlsOf, poseAsDefault, poseDiffers } from '../client/modeller/v3-controls.ts'
15
+ import { controlsOf, poseAsDefault, poseDiffers } from '../client/modeller/controls.ts'
16
16
  import { moving } from './joints.ts'
17
17
 
18
18
  const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
19
19
 
20
- const slider = (): V3Asset => {
21
- let asset = createV3Asset('Slider')
22
- asset = addV3Box(asset, { id: 'block', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 } })
20
+ const slider = (): FigureAsset => {
21
+ let asset = createFigureAsset('Slider')
22
+ asset = addFigureBox(asset, { id: 'block', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 } })
23
23
  asset = moving(asset, 'block', { kind: 'slide', axis: 'x', id: 'shift', min: 0, max: 60, start: 20 })
24
- asset = addV3Box(asset, { id: 'arm', size: { x: 40, y: 200, z: 40 }, position: { x: 0, y: 200, z: 0 } })
24
+ asset = addFigureBox(asset, { id: 'arm', size: { x: 40, y: 200, z: 40 }, position: { x: 0, y: 200, z: 0 } })
25
25
  return moving(asset, 'arm', { kind: 'turn', axis: 'y', id: 'spin', min: -90, max: 90, start: 0 })
26
26
  }
27
27
 
28
28
  test('조작값마다 이름 · 단위 · 범위 · 지금 값 · 시작 값 · 이동 시간을 읽는다', () => {
29
29
  let asset = slider()
30
- asset = editV3Part(asset, { kind: 'set-state-input', input: 'shift', label: '밀기', sweep: 2 })
30
+ asset = editFigurePart(asset, { kind: 'set-state-input', input: 'shift', label: '밀기', sweep: 2 })
31
31
  const [shift, spin] = controlsOf(asset, { shift: 45 })
32
32
  assert.deepEqual(
33
33
  { ...shift, step: undefined },
@@ -45,21 +45,21 @@ test('지금 자세를 기본으로 — 보고 있는 자세가 시작 자세와
45
45
  assert.equal(poseDiffers(controlsOf(asset)), false)
46
46
  const looking = controlsOf(asset, { shift: 50, spin: 30 })
47
47
  assert.equal(poseDiffers(looking), true)
48
- const kept = editV3Part(asset, poseAsDefault(looking))
48
+ const kept = editFigurePart(asset, poseAsDefault(looking))
49
49
  assert.deepEqual([kept.stateDefaults['shift'], kept.stateDefaults['spin']], [50, 30])
50
50
  assert.equal(poseDiffers(controlsOf(kept, { shift: 50, spin: 30 })), false)
51
51
  })
52
52
 
53
53
  test('편집기에 닿는다 — 값 바꾸기는 입력으로, 이름 · 범위 · 시간 · 기본 자세는 부품 편집으로', () => {
54
- const editor = read('../client/modeller/v3-editor.ts')
55
- const start = editor.indexOf('<figure-v3-controls')
56
- const block = editor.slice(start, editor.indexOf('</figure-v3-controls>', start))
54
+ const editor = read('../client/modeller/editor.ts')
55
+ const start = editor.indexOf('<figure-controls')
56
+ const block = editor.slice(start, editor.indexOf('</figure-controls>', start))
57
57
  assert.ok(start >= 0)
58
- assert.ok(block.includes("@v3-state=${(e: CustomEvent) => this.run(() => this.session!.apply([{ kind: 'set-state'"))
59
- assert.ok(block.includes('@v3-part-edit=${(e: CustomEvent) => this.run(() => this.session!.edit(e.detail.edit))}'))
58
+ assert.ok(block.includes("@state=${(e: CustomEvent) => this.run(() => this.session!.apply([{ kind: 'set-state'"))
59
+ assert.ok(block.includes('@part-edit=${(e: CustomEvent) => this.run(() => this.session!.edit(e.detail.edit))}'))
60
60
  assert.ok(editor.includes("const design = view.fields.filter(i => i.role !== 'state')"), '조작값은 설계 값 칸에 두 번 나오지 않는다')
61
61
 
62
- const controls = read('../client/modeller/figure-v3-controls.ts')
62
+ const controls = read('../client/modeller/figure-controls.ts')
63
63
  assert.ok(controls.includes("@change=${(e: Event) => this.try(c, number(e))}"), '슬라이더는 놓을 때 한 번 — 끄는 동안 기록이 쌓이지 않는다')
64
64
  const keys = [...controls.matchAll(/'(figure\.[a-z.-]*[a-z])'/g)].map(m => m[1])
65
65
  for (const lang of ['ko', 'en', 'ja', 'zh', 'ms']) {