@things-factory/figure-ui 10.1.74 → 10.1.76

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 (244) 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} +137 -126
  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-refusals.ts → refusals.ts} +62 -13
  37. package/client/modeller/{v3-renderer.ts → renderer.ts} +16 -16
  38. package/client/modeller/{v3-stage-model.ts → stage-model.ts} +15 -15
  39. package/client/modeller/{v3-thumbnail.ts → thumbnail.ts} +8 -8
  40. package/client/pages/figure-list-page.ts +4 -4
  41. package/client/pages/{figure-v3-modeller-page.ts → figure-modeller-page.ts} +37 -37
  42. package/client/route.ts +1 -5
  43. package/dist-client/figure-assistant.js +3 -3
  44. package/dist-client/figure-assistant.js.map +1 -1
  45. package/dist-client/graphql/store.d.ts +3 -0
  46. package/dist-client/graphql/store.js +39 -0
  47. package/dist-client/graphql/store.js.map +1 -0
  48. package/dist-client/modeller/ai-proposal.d.ts +51 -0
  49. package/dist-client/modeller/ai-proposal.js +64 -0
  50. package/dist-client/modeller/ai-proposal.js.map +1 -0
  51. package/dist-client/modeller/align.d.ts +30 -0
  52. package/dist-client/modeller/align.js +75 -0
  53. package/dist-client/modeller/align.js.map +1 -0
  54. package/dist-client/modeller/canvas.d.ts +72 -0
  55. package/dist-client/modeller/canvas.js +379 -0
  56. package/dist-client/modeller/canvas.js.map +1 -0
  57. package/dist-client/modeller/clips.d.ts +39 -0
  58. package/dist-client/modeller/clips.js +58 -0
  59. package/dist-client/modeller/clips.js.map +1 -0
  60. package/dist-client/modeller/controls.d.ts +28 -0
  61. package/dist-client/modeller/controls.js +28 -0
  62. package/dist-client/modeller/controls.js.map +1 -0
  63. package/dist-client/modeller/editor.d.ts +203 -0
  64. package/dist-client/modeller/editor.js +1526 -0
  65. package/dist-client/modeller/editor.js.map +1 -0
  66. package/dist-client/modeller/fields.d.ts +45 -0
  67. package/dist-client/modeller/fields.js +83 -0
  68. package/dist-client/modeller/fields.js.map +1 -0
  69. package/dist-client/modeller/figure-capability-panel.d.ts +22 -0
  70. package/dist-client/modeller/figure-capability-panel.js +141 -0
  71. package/dist-client/modeller/figure-capability-panel.js.map +1 -0
  72. package/dist-client/modeller/figure-clips.d.ts +31 -0
  73. package/dist-client/modeller/figure-clips.js +268 -0
  74. package/dist-client/modeller/figure-clips.js.map +1 -0
  75. package/dist-client/modeller/figure-controls.d.ts +36 -0
  76. package/dist-client/modeller/figure-controls.js +226 -0
  77. package/dist-client/modeller/figure-controls.js.map +1 -0
  78. package/dist-client/modeller/figure-facts.d.ts +35 -0
  79. package/dist-client/modeller/figure-facts.js +55 -0
  80. package/dist-client/modeller/figure-facts.js.map +1 -0
  81. package/dist-client/modeller/figure-first-save.d.ts +20 -0
  82. package/dist-client/modeller/figure-first-save.js +118 -0
  83. package/dist-client/modeller/figure-first-save.js.map +1 -0
  84. package/dist-client/modeller/figure-history-panel.d.ts +1 -1
  85. package/dist-client/modeller/figure-history-panel.js.map +1 -1
  86. package/dist-client/modeller/figure-links.d.ts +77 -0
  87. package/dist-client/modeller/figure-links.js +649 -0
  88. package/dist-client/modeller/figure-links.js.map +1 -0
  89. package/dist-client/modeller/figure-material-panel.d.ts +39 -0
  90. package/dist-client/modeller/figure-material-panel.js +323 -0
  91. package/dist-client/modeller/figure-material-panel.js.map +1 -0
  92. package/dist-client/modeller/figure-parts.d.ts +69 -0
  93. package/dist-client/modeller/figure-parts.js +559 -0
  94. package/dist-client/modeller/figure-parts.js.map +1 -0
  95. package/dist-client/modeller/figure-preview.d.ts +34 -0
  96. package/dist-client/modeller/figure-preview.js +346 -0
  97. package/dist-client/modeller/figure-preview.js.map +1 -0
  98. package/dist-client/modeller/figure-properties.d.ts +45 -0
  99. package/dist-client/modeller/figure-properties.js +239 -0
  100. package/dist-client/modeller/figure-properties.js.map +1 -0
  101. package/dist-client/modeller/figure-recipe-catalog.d.ts +38 -0
  102. package/dist-client/modeller/figure-recipe-catalog.js +399 -0
  103. package/dist-client/modeller/figure-recipe-catalog.js.map +1 -0
  104. package/dist-client/modeller/figure-verdict.d.ts +64 -0
  105. package/dist-client/modeller/figure-verdict.js +787 -0
  106. package/dist-client/modeller/figure-verdict.js.map +1 -0
  107. package/dist-client/modeller/first-save.d.ts +17 -0
  108. package/dist-client/modeller/first-save.js +26 -0
  109. package/dist-client/modeller/first-save.js.map +1 -0
  110. package/dist-client/modeller/gate-findings.d.ts +2 -2
  111. package/dist-client/modeller/gate-findings.js +2 -2
  112. package/dist-client/modeller/gate-findings.js.map +1 -1
  113. package/dist-client/modeller/gizmo.d.ts +72 -0
  114. package/dist-client/modeller/gizmo.js +111 -0
  115. package/dist-client/modeller/gizmo.js.map +1 -0
  116. package/dist-client/modeller/help.d.ts +18 -0
  117. package/dist-client/modeller/help.js +37 -0
  118. package/dist-client/modeller/help.js.map +1 -0
  119. package/dist-client/modeller/joint-drag.d.ts +19 -0
  120. package/dist-client/modeller/joint-drag.js +97 -0
  121. package/dist-client/modeller/joint-drag.js.map +1 -0
  122. package/dist-client/modeller/joint-fan.d.ts +9 -0
  123. package/dist-client/modeller/joint-fan.js +276 -0
  124. package/dist-client/modeller/joint-fan.js.map +1 -0
  125. package/dist-client/modeller/keys.d.ts +22 -0
  126. package/dist-client/modeller/keys.js +26 -0
  127. package/dist-client/modeller/keys.js.map +1 -0
  128. package/dist-client/modeller/links.d.ts +267 -0
  129. package/dist-client/modeller/links.js +480 -0
  130. package/dist-client/modeller/links.js.map +1 -0
  131. package/dist-client/modeller/live.d.ts +37 -0
  132. package/dist-client/modeller/live.js +68 -0
  133. package/dist-client/modeller/live.js.map +1 -0
  134. package/dist-client/modeller/maturity.d.ts +120 -0
  135. package/dist-client/modeller/maturity.js +313 -0
  136. package/dist-client/modeller/maturity.js.map +1 -0
  137. package/dist-client/modeller/modeller-route.d.ts +1 -1
  138. package/dist-client/modeller/modeller-route.js +1 -1
  139. package/dist-client/modeller/modeller-route.js.map +1 -1
  140. package/dist-client/modeller/occupancy.d.ts +18 -0
  141. package/dist-client/modeller/occupancy.js +24 -0
  142. package/dist-client/modeller/occupancy.js.map +1 -0
  143. package/dist-client/modeller/palette.d.ts +7 -0
  144. package/dist-client/modeller/palette.js +46 -0
  145. package/dist-client/modeller/palette.js.map +1 -0
  146. package/dist-client/modeller/preview-model.d.ts +47 -0
  147. package/dist-client/modeller/preview-model.js +68 -0
  148. package/dist-client/modeller/preview-model.js.map +1 -0
  149. package/dist-client/modeller/product-session.d.ts +510 -0
  150. package/dist-client/modeller/product-session.js +157 -0
  151. package/dist-client/modeller/product-session.js.map +1 -0
  152. package/dist-client/modeller/recipe-source.d.ts +8 -0
  153. package/dist-client/modeller/recipe-source.js +14 -0
  154. package/dist-client/modeller/recipe-source.js.map +1 -0
  155. package/dist-client/modeller/recipes.d.ts +37 -0
  156. package/dist-client/modeller/recipes.js +103 -0
  157. package/dist-client/modeller/recipes.js.map +1 -0
  158. package/dist-client/modeller/refusals.d.ts +68 -0
  159. package/dist-client/modeller/refusals.js +142 -0
  160. package/dist-client/modeller/refusals.js.map +1 -0
  161. package/dist-client/modeller/renderer.d.ts +40 -0
  162. package/dist-client/modeller/renderer.js +73 -0
  163. package/dist-client/modeller/renderer.js.map +1 -0
  164. package/dist-client/modeller/stage-model.d.ts +39 -0
  165. package/dist-client/modeller/stage-model.js +147 -0
  166. package/dist-client/modeller/stage-model.js.map +1 -0
  167. package/dist-client/modeller/thumbnail.d.ts +3 -0
  168. package/dist-client/modeller/thumbnail.js +75 -0
  169. package/dist-client/modeller/thumbnail.js.map +1 -0
  170. package/dist-client/modeller/v3-clips.d.ts +1 -1
  171. package/dist-client/modeller/v3-editor.d.ts +5 -0
  172. package/dist-client/modeller/v3-editor.js +21 -10
  173. package/dist-client/modeller/v3-editor.js.map +1 -1
  174. package/dist-client/modeller/v3-product-session.d.ts +31 -0
  175. package/dist-client/modeller/v3-refusals.d.ts +37 -5
  176. package/dist-client/modeller/v3-refusals.js +59 -16
  177. package/dist-client/modeller/v3-refusals.js.map +1 -1
  178. package/dist-client/pages/figure-list-page.d.ts +1 -1
  179. package/dist-client/pages/figure-list-page.js +4 -4
  180. package/dist-client/pages/figure-list-page.js.map +1 -1
  181. package/dist-client/pages/figure-modeller-page.d.ts +102 -0
  182. package/dist-client/pages/figure-modeller-page.js +548 -0
  183. package/dist-client/pages/figure-modeller-page.js.map +1 -0
  184. package/dist-client/route.d.ts +1 -1
  185. package/dist-client/route.js +1 -5
  186. package/dist-client/route.js.map +1 -1
  187. package/dist-client/tsconfig.tsbuildinfo +1 -1
  188. package/docs/product-cutover.md +25 -0
  189. package/package.json +5 -5
  190. package/test/{v3-ai-proposal.test.ts → ai-proposal.test.ts} +6 -6
  191. package/test/{v3-align.test.ts → align.test.ts} +8 -8
  192. package/test/browser/modeller-page.js +30 -30
  193. package/test/browser/modeller.check.mjs +17 -16
  194. package/test/browser/serve.mjs +1 -1
  195. package/test/browser/thumbnail-page.js +6 -6
  196. package/test/{v3-clips.test.ts → clips.test.ts} +18 -18
  197. package/test/{v3-controls.test.ts → controls.test.ts} +15 -15
  198. package/test/{v3-fields.test.ts → fields.test.ts} +13 -13
  199. package/test/figure-assistant-surface.test.ts +3 -3
  200. package/test/{v3-first-save.test.ts → first-save.test.ts} +10 -10
  201. package/test/fixtures/joint-column.json +2 -2
  202. package/test/fixtures/joint-roller.json +2 -2
  203. package/test/fixtures/robot-arm-6dof.json +2 -2
  204. package/test/{v3-gizmo.test.ts → gizmo.test.ts} +33 -33
  205. package/test/{v3-joint-drag.test.ts → joint-drag.test.ts} +17 -17
  206. package/test/{v3-joint-fan.test.ts → joint-fan.test.ts} +18 -18
  207. package/test/joints.ts +6 -6
  208. package/test/{v3-keys.test.ts → keys.test.ts} +2 -2
  209. package/test/{v3-layout.test.ts → layout.test.ts} +8 -8
  210. package/test/{v3-links.test.ts → links.test.ts} +201 -201
  211. package/test/{v3-live.test.ts → live.test.ts} +18 -18
  212. package/test/{v3-material.test.ts → material.test.ts} +12 -12
  213. package/test/{v3-maturity.test.ts → maturity.test.ts} +74 -74
  214. package/test/modeller-route.test.ts +5 -6
  215. package/test/{v3-palette-icons.test.ts → palette-icons.test.ts} +1 -1
  216. package/test/{v3-parts-wiring.test.ts → parts-wiring.test.ts} +11 -7
  217. package/test/{v3-preview.test.ts → preview.test.ts} +12 -12
  218. package/test/{v3-product-session.test.ts → product-session.test.ts} +17 -17
  219. package/test/{v3-product-write.test.ts → product-write.test.ts} +20 -20
  220. package/test/{v3-properties.test.ts → properties.test.ts} +24 -24
  221. package/test/{v3-recipes.test.ts → recipes.test.ts} +10 -10
  222. package/test/refusals.test.ts +166 -0
  223. package/test/{v3-release-panel.test.ts → release-panel.test.ts} +21 -21
  224. package/test/renderer.test.ts +24 -0
  225. package/test/resolve-ts-siblings.mjs +1 -1
  226. package/test/{v3-scene-canvas.test.ts → scene-canvas.test.ts} +6 -6
  227. package/test/{v3-stage-model.test.ts → stage-model.test.ts} +5 -5
  228. package/test/{v3-theme.test.ts → theme.test.ts} +4 -4
  229. package/test/{v3-thumbnail.test.ts → thumbnail.test.ts} +4 -4
  230. package/test/{v3-verdict.test.ts → verdict.test.ts} +17 -17
  231. package/test/{v3-view.test.ts → view.test.ts} +2 -2
  232. package/things-factory.config.js +2 -3
  233. package/translations/en.json +15 -7
  234. package/translations/ja.json +15 -7
  235. package/translations/ko.json +15 -7
  236. package/translations/ms.json +15 -7
  237. package/translations/zh.json +15 -7
  238. package/docs/v3-product-cutover.md +0 -25
  239. package/test/v3-refusals.test.ts +0 -136
  240. package/test/v3-renderer.test.ts +0 -24
  241. /package/client/modeller/{v3-fields.ts → fields.ts} +0 -0
  242. /package/client/modeller/{v3-first-save.ts → first-save.ts} +0 -0
  243. /package/client/modeller/{v3-help.ts → help.ts} +0 -0
  244. /package/client/modeller/{v3-keys.ts → keys.ts} +0 -0
@@ -1,5 +1,5 @@
1
1
  /*
2
- * **V3 부품이 무엇에 묶여 있나 — 크기 따르기 · 붙이기 · 움직임** (2026-09-24).
2
+ * **부품이 무엇에 묶여 있나 — 크기 따르기 · 붙이기 · 움직임** (2026-09-24).
3
3
  *
4
4
  * 세 칸이 보여 주는 것은 figure-model 명령이 남긴 그래프를 거꾸로 읽은 것이다. 명령을 실제로 적용한 뒤 읽어서,
5
5
  * 화면이 명령과 같은 말을 하는지 잰다.
@@ -7,15 +7,15 @@
7
7
  import { test } from 'node:test'
8
8
  import assert from 'node:assert/strict'
9
9
 
10
- import { addV3Box, addV3Primitive, applyV3Authoring, createV3Asset } from '@hatiolab/figure-model'
11
- import type { V3Asset } from '@hatiolab/figure-model'
10
+ import { addFigureBox, addFigurePrimitive, applyFigureAuthoring, createFigureAsset } from '@hatiolab/figure-model'
11
+ import type { FigureAsset } from '@hatiolab/figure-model'
12
12
 
13
- import { acceptsOf, canFasten, canFollow, dimensionNow, instanceRangeOf, proportionToFollow, rangeDiffers, rangeWith, turnsAboutAJoint, valueNow, fastenedTo, followableAxes, followOf, hasInstanceSize, positionIsShare, holdsThings, motionOf, placeFacesOf, placeIdFor, placesOn, positionsWorkedOut } from '../client/modeller/v3-links.ts'
13
+ import { acceptsOf, canFasten, canFollow, dimensionNow, instanceRangeOf, proportionToFollow, rangeDiffers, rangeWith, turnsAboutAJoint, valueNow, fastenedTo, followableAxes, followOf, hasInstanceSize, positionIsShare, holdsThings, motionOf, placeFacesOf, placeIdFor, placesOn, positionsWorkedOut } from '../client/modeller/links.ts'
14
14
 
15
- const pair = (): V3Asset => {
16
- let asset = createV3Asset('Pair')
17
- asset = addV3Box(asset, { id: 'plate', size: { x: 600, y: 100, z: 400 }, position: { x: 0, y: 50, z: 0 } })
18
- asset = addV3Box(asset, { id: 'block', size: { x: 200, y: 60, z: 120 }, position: { x: 0, y: 130, z: 0 } })
15
+ const pair = (): FigureAsset => {
16
+ let asset = createFigureAsset('Pair')
17
+ asset = addFigureBox(asset, { id: 'plate', size: { x: 600, y: 100, z: 400 }, position: { x: 0, y: 50, z: 0 } })
18
+ asset = addFigureBox(asset, { id: 'block', size: { x: 200, y: 60, z: 120 }, position: { x: 0, y: 130, z: 0 } })
19
19
  const model = asset.document.model
20
20
  for (const axis of ['x', 'y', 'z']) {
21
21
  model.inputs.push({ id: `size.${axis}`, unit: 'mm', min: 100, max: 20000, role: 'design' } as never)
@@ -27,23 +27,23 @@ const pair = (): V3Asset => {
27
27
  test('크기 — 제 치수 · 인스턴스 크기 · 다른 부품의 치수를 가려 읽는다', () => {
28
28
  const asset = pair()
29
29
  assert.deepEqual(followOf(asset, 'plate', 'x'), { of: 'own' })
30
- const byInstance = applyV3Authoring(asset, { kind: 'link-dimension', part: 'plate', axis: 'x', source: { of: 'instance', axis: 'x' } })
30
+ const byInstance = applyFigureAuthoring(asset, { kind: 'link-dimension', part: 'plate', axis: 'x', source: { of: 'instance', axis: 'x' } })
31
31
  assert.deepEqual(followOf(byInstance, 'plate', 'x'), { of: 'instance', axis: 'x', times: 1 })
32
- const byPart = applyV3Authoring(byInstance, { kind: 'link-dimension', part: 'block', axis: 'x', source: { of: 'part', part: 'plate', axis: 'x' }, times: 0.5 })
32
+ const byPart = applyFigureAuthoring(byInstance, { kind: 'link-dimension', part: 'block', axis: 'x', source: { of: 'part', part: 'plate', axis: 'x' }, times: 0.5 })
33
33
  assert.deepEqual(followOf(byPart, 'block', 'x'), { of: 'part', part: 'plate', axis: 'x', times: 0.5 }, '배율이 끼어도 원천을 찾고, 배율도 읽는다')
34
- const back = applyV3Authoring(byPart, { kind: 'unlink-dimension', part: 'block', axis: 'x', mm: 300 })
34
+ const back = applyFigureAuthoring(byPart, { kind: 'unlink-dimension', part: 'block', axis: 'x', mm: 300 })
35
35
  assert.deepEqual(followOf(back, 'block', 'x'), { of: 'own' }, '떼면 다시 제 치수')
36
36
  assert.equal(hasInstanceSize(asset, 'y'), true)
37
- assert.equal(hasInstanceSize(createV3Asset('Bare'), 'y'), false)
37
+ assert.equal(hasInstanceSize(createFigureAsset('Bare'), 'y'), false)
38
38
  })
39
39
 
40
40
  test('붙이기 — 누구에게 붙었나, 붙일 수 있는 모양인가', () => {
41
41
  const asset = pair()
42
42
  assert.equal(fastenedTo(asset, 'block'), null)
43
- const fastened = applyV3Authoring(asset, { kind: 'attach', part: 'block', face: 'bottom', to: { part: 'plate', face: 'top' } })
43
+ const fastened = applyFigureAuthoring(asset, { kind: 'attach', part: 'block', face: 'bottom', to: { part: 'plate', face: 'top' } })
44
44
  assert.equal(fastenedTo(fastened, 'block'), 'plate')
45
- assert.equal(fastenedTo(applyV3Authoring(fastened, { kind: 'detach', part: 'block' }), 'block'), null)
46
- const withDrum = addV3Primitive(asset, { id: 'drum', kind: 'cylinder', dimensions: { radius: 50, length: 100 }, position: { x: 0, y: 50, z: 0 } })
45
+ assert.equal(fastenedTo(applyFigureAuthoring(fastened, { kind: 'detach', part: 'block' }), 'block'), null)
46
+ const withDrum = addFigurePrimitive(asset, { id: 'drum', kind: 'cylinder', dimensions: { radius: 50, length: 100 }, position: { x: 0, y: 50, z: 0 } })
47
47
  assert.equal(canFasten(withDrum, 'block'), true)
48
48
  assert.equal(canFasten(withDrum, 'drum'), true, '원기둥도 면으로 붙인다(figure-model 0.1.47)')
49
49
  assert.equal(canFollow(withDrum, 'drum'), true, '원기둥은 반지름(X)과 길이(Y)를 따를 수 있다')
@@ -62,13 +62,13 @@ test('움직임 — 미끄러짐 · 돌기, 축, 무엇이 모느냐를 읽는
62
62
  const turning = moving(asset, 'block', { kind: 'turn', axis: 'y', id: 'swing', min: -90, max: 90, start: 0 })
63
63
  assert.deepEqual([motionOf(turning, 'block')!.kind, motionOf(turning, 'block')!.axis], ['turn', 'y'])
64
64
  const joint = sliding.joints!.find(j => j.body1.part === 'block')!
65
- const still = applyV3Authoring(sliding, { kind: 'set-joint', joint: { id: joint.id, type: 'fixed', body0: joint.body0, body1: joint.body1 } })
65
+ const still = applyFigureAuthoring(sliding, { kind: 'set-joint', joint: { id: joint.id, type: 'fixed', body0: joint.body0, body1: joint.body1 } })
66
66
  assert.equal(motionOf(still, 'block'), undefined, 'a fixed joint does not move')
67
67
  })
68
68
 
69
69
  import { readFileSync } from 'node:fs'
70
70
  import { fileURLToPath } from 'node:url'
71
- import { choiceOf } from '../client/modeller/v3-links.ts'
71
+ import { choiceOf } from '../client/modeller/links.ts'
72
72
  import { moving } from './joints.ts'
73
73
 
74
74
  const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
@@ -81,21 +81,21 @@ test('고르개의 값은 지금 따르는 것을 가리킨다', () => {
81
81
  })
82
82
 
83
83
  test('편집기는 세 칸의 명령을 세션의 이력 하나로 돌리고, 만날 수 있는 거절을 사람의 말로 한다', () => {
84
- const editor = read('../client/modeller/v3-editor.ts')
85
- assert.match(editor, /@v3-command=\$\{\(e: CustomEvent\) => this\.linkCommand\(e\.detail\.action\)\}/)
84
+ const editor = read('../client/modeller/editor.ts')
85
+ assert.match(editor, /@command=\$\{\(e: CustomEvent\) => this\.linkCommand\(e\.detail\.action\)\}/)
86
86
  assert.match(editor, /this\.session!\.command\(action\)/)
87
87
  assert.match(editor, /code === 'DIMENSION_CYCLE'[\s\S]*?figure\.text\.follow-would-loop/)
88
88
  assert.match(editor, /code === 'ATTACH_CYCLE'[\s\S]*?figure\.text\.fasten-would-loop/)
89
89
  })
90
90
 
91
91
  test('떼면 지금 크기로 고정한다 — 따르던 값을 잃지 않는다', () => {
92
- const links = read('../client/modeller/figure-v3-links.ts')
92
+ const links = read('../client/modeller/figure-links.ts')
93
93
  assert.ok(links.includes("this.command({ kind: 'unlink-dimension', part: this.partId, axis, mm: Math.max(1, Math.round(current)) })"))
94
94
  assert.ok(links.includes('const current = dimensionNow(asset, this.partId, axis) ?? this.dims?.[AXES.indexOf(axis)] ?? 100'))
95
95
  })
96
96
 
97
97
  test('세 칸의 말은 다섯 말 모두에 있다', () => {
98
- const links = read('../client/modeller/figure-v3-links.ts')
98
+ const links = read('../client/modeller/figure-links.ts')
99
99
  const keys = [...links.matchAll(/'(figure\.[a-z.-]*[a-z])'/g)].map(m => m[1])
100
100
  for (const lang of ['ko', 'en', 'ja', 'zh', 'ms']) {
101
101
  const table = JSON.parse(read(`../translations/${lang}.json`))
@@ -106,25 +106,25 @@ test('세 칸의 말은 다섯 말 모두에 있다', () => {
106
106
 
107
107
  test('도형 크기를 따라 움직이는 자리를 가려 읽고, 여기에 고정하면 사라진다 — 드라이룸 램프의 길', () => {
108
108
  let asset = pair()
109
- asset = addV3Box(asset, { id: 'lamp', size: { x: 40, y: 20, z: 40 }, position: { x: 0, y: 10, z: 0 } })
109
+ asset = addFigureBox(asset, { id: 'lamp', size: { x: 40, y: 20, z: 40 }, position: { x: 0, y: 10, z: 0 } })
110
110
  const model = asset.document.model
111
111
  model.nodes.push({ id: 'lamp.lift', op: 'add@1', args: ['size.y', 'lamp.position.y'], outputs: { value: 'lamp.lift.value' } } as never)
112
112
  model.nodes.find(n => n.id === 'lamp.pose')!.args[1] = 'lamp.lift.value'
113
113
  assert.deepEqual(positionsWorkedOut(asset, 'lamp'), ['y'])
114
114
  assert.deepEqual(positionsWorkedOut(asset, 'plate'), [], '제 숫자로 놓인 부품에는 없다')
115
- const fixed = applyV3Authoring(asset, { kind: 'fix-position', part: 'lamp', axis: 'y' })
115
+ const fixed = applyFigureAuthoring(asset, { kind: 'fix-position', part: 'lamp', axis: 'y' })
116
116
  assert.deepEqual(positionsWorkedOut(fixed, 'lamp'), [])
117
117
  assert.equal(fixed.designInputs['lamp.position.y'], 110, '지금 자리 그대로 — 100 + 10')
118
118
  })
119
119
 
120
120
  test('자리 — 부품의 면에 둔 집는 자리·놓는 자리를 그 부품에서 읽는다', () => {
121
121
  let asset = pair()
122
- asset = addV3Primitive(asset, { id: 'drum', kind: 'cylinder', dimensions: { radius: 100, length: 300 }, position: { x: 0, y: 400, z: 0 } } as never)
122
+ asset = addFigurePrimitive(asset, { id: 'drum', kind: 'cylinder', dimensions: { radius: 100, length: 300 }, position: { x: 0, y: 400, z: 0 } } as never)
123
123
  assert.deepEqual(placeFacesOf(asset, 'block'), ['left', 'right', 'bottom', 'top', 'back', 'front'])
124
124
  assert.deepEqual(placeFacesOf(asset, 'drum'), [], '둥근 몸에는 자리를 권하지 않는다 — 명령이 거절할 것이다')
125
125
 
126
126
  assert.equal(placeIdFor(asset, 'block', 'grip'), 'block-grip')
127
- asset = applyV3Authoring(asset, { kind: 'add-binding', id: placeIdFor(asset, 'block', 'grip'), label: '집는 자리', role: 'grip', part: 'block', face: 'top', accepts: acceptsOf(' BOX, TOTE,, BOX '), capacity: 2 })
127
+ asset = applyFigureAuthoring(asset, { kind: 'add-binding', id: placeIdFor(asset, 'block', 'grip'), label: '집는 자리', role: 'grip', part: 'block', face: 'top', accepts: acceptsOf(' BOX, TOTE,, BOX '), capacity: 2 })
128
128
  assert.equal(placeIdFor(asset, 'block', 'grip'), 'block-grip-2', '같은 부품에 둘째 자리는 다른 id')
129
129
  assert.deepEqual(
130
130
  placesOn(asset, 'block').map(p => [p.id, p.role, p.face, p.accepts, p.capacity]),
@@ -141,18 +141,18 @@ test('담는 능력이 없는 도형은 자리가 있어도 보드가 아무것
141
141
  asset.document.capabilities.names = ['FlowNode']
142
142
  assert.equal(holdsThings(asset), true, 'FlowNode 는 Capacity 를 품는다')
143
143
 
144
- const links = read('../client/modeller/figure-v3-links.ts')
144
+ const links = read('../client/modeller/figure-links.ts')
145
145
  assert.match(links, /holdsThings\(asset\)/)
146
146
  assert.match(links, /kind: 'set-capabilities', names/)
147
- const editor = read('../client/modeller/v3-editor.ts')
147
+ const editor = read('../client/modeller/editor.ts')
148
148
  /* A slice and `includes`, not a lazy regex over the whole editor: that hangs when it does not match. */
149
- const start = editor.indexOf('<figure-v3-links')
150
- const block = editor.slice(start, editor.indexOf('</figure-v3-links>', start))
151
- assert.ok(start >= 0 && block.includes('@v3-part-edit=${(e: CustomEvent) => this.run(() => this.session!.edit(e.detail.edit))}'), '자리 절의 능력 선언이 편집기에 닿는다')
149
+ const start = editor.indexOf('<figure-links')
150
+ const block = editor.slice(start, editor.indexOf('</figure-links>', start))
151
+ assert.ok(start >= 0 && block.includes('@part-edit=${(e: CustomEvent) => this.run(() => this.session!.edit(e.detail.edit))}'), '자리 절의 능력 선언이 편집기에 닿는다')
152
152
  })
153
153
 
154
154
  test('받는 종류가 없으면 자리를 만들지 않는다 — 형식에 「아무거나」가 없다', () => {
155
- const links = read('../client/modeller/figure-v3-links.ts')
155
+ const links = read('../client/modeller/figure-links.ts')
156
156
  assert.match(links, /\?disabled=\$\{this\.locked \|\| !accepts\.length \|\| !capacityOk\}/)
157
157
  assert.deepEqual(acceptsOf(' , ,'), [])
158
158
  })
@@ -160,9 +160,9 @@ test('받는 종류가 없으면 자리를 만들지 않는다 — 형식에 「
160
160
  /* ── Proportional rules (user 2026-09-24: ROBOT_ARM at twice the size looked broken) ───────────── */
161
161
 
162
162
  const sizedPair = () => {
163
- let asset = createV3Asset('Probe')
164
- asset = addV3Box(asset, { id: 'base', size: { x: 200, y: 100, z: 200 }, position: { x: 0, y: 50, z: 0 } })
165
- asset = addV3Primitive(asset, { id: 'flange', kind: 'cylinder', dimensions: { radius: 20, length: 10 }, position: { x: 0, y: 95, z: 0 } })
163
+ let asset = createFigureAsset('Probe')
164
+ asset = addFigureBox(asset, { id: 'base', size: { x: 200, y: 100, z: 200 }, position: { x: 0, y: 50, z: 0 } })
165
+ asset = addFigurePrimitive(asset, { id: 'flange', kind: 'cylinder', dimensions: { radius: 20, length: 10 }, position: { x: 0, y: 95, z: 0 } })
166
166
  asset.document.model.inputs.push({ id: 'size.y', unit: 'mm', min: 1, max: 20000, role: 'design' } as never)
167
167
  asset.designInputs['size.y'] = 100
168
168
  return asset
@@ -172,7 +172,7 @@ test('비례 — 인스턴스 크기의 몫을 따르는 치수는 비례로 읽
172
172
  const asset = sizedPair()
173
173
  assert.equal(dimensionNow(asset, 'flange', 'y'), 10, '원기둥의 Y 는 길이')
174
174
  assert.equal(dimensionNow(asset, 'flange', 'x'), 20, '원기둥의 X 는 반지름')
175
- const share = applyV3Authoring(asset, { kind: 'link-dimension', part: 'flange', axis: 'y', source: { of: 'instance', axis: 'y' }, times: 0.1 })
175
+ const share = applyFigureAuthoring(asset, { kind: 'link-dimension', part: 'flange', axis: 'y', source: { of: 'instance', axis: 'y' }, times: 0.1 })
176
176
  assert.deepEqual(followOf(share, 'flange', 'y'), { of: 'instance', axis: 'y', times: 0.1 })
177
177
 
178
178
  /* A span: the size less what the others take -- a sum, not a share (ROBOT_ARM's tool flange). */
@@ -183,7 +183,7 @@ test('비례 — 인스턴스 크기의 몫을 따르는 치수는 비례로 읽
183
183
  m.nodes.push({ id: 'flange.dimension.y', op: 'mul@1', args: ['flange.free.value', m.constants.find(c => c.value === 1)?.id ?? 'k.one'], outputs: { value: 'flange.dimension.y.value' } })
184
184
  if (!m.constants.some(c => c.value === 1)) m.constants.push({ id: 'k.one', unit: 'ratio', value: 1 })
185
185
  /* Read as what it is since the plus is read (2026-09-25): the size less 90 -- following the size, not a share of it. */
186
- assert.deepEqual(followOf(fill as V3Asset, 'flange', 'y'), { of: 'instance', axis: 'y', times: 1, plus: -90 })
186
+ assert.deepEqual(followOf(fill as FigureAsset, 'flange', 'y'), { of: 'instance', axis: 'y', times: 1, plus: -90 })
187
187
 
188
188
  /* Before any link the shape reads the measure directly; a worked-out one there is not 「고정」 (ROBOT_ARM on :3300). */
189
189
  const direct = structuredClone(asset)
@@ -191,23 +191,23 @@ test('비례 — 인스턴스 크기의 몫을 따르는 치수는 비례로 읽
191
191
  d.constants.push({ id: 'k.gaps', unit: 'mm', value: -90 })
192
192
  d.nodes.push({ id: 'flange.free', op: 'add@1', args: ['size.y', 'k.gaps'], outputs: { value: 'flange.free.value' } })
193
193
  d.nodes.find(n => n.id === 'flange.shape')!.args[1] = 'flange.free.value'
194
- assert.deepEqual(followOf(direct as V3Asset, 'flange', 'y'), { of: 'expression' })
194
+ assert.deepEqual(followOf(direct as FigureAsset, 'flange', 'y'), { of: 'expression' })
195
195
  assert.deepEqual(followOf(asset, 'flange', 'y'), { of: 'own' }, '제 숫자는 고정')
196
196
  })
197
197
 
198
198
  test('자리를 크기에 비례로 두면 그렇게 읽고, 다시 권하지 않는다', () => {
199
199
  let asset = sizedPair()
200
- asset = applyV3Authoring(asset, { kind: 'scale-position', part: 'flange', axis: 'y' })
200
+ asset = applyFigureAuthoring(asset, { kind: 'scale-position', part: 'flange', axis: 'y' })
201
201
  assert.equal(positionIsShare(asset, 'flange', 'y'), true)
202
202
  assert.equal(positionIsShare(asset, 'base', 'y'), false)
203
- const links = read('../client/modeller/figure-v3-links.ts')
203
+ const links = read('../client/modeller/figure-links.ts')
204
204
  assert.ok(links.includes("hasInstanceSize(asset, axis) && !positionIsShare(asset, this.partId, axis)"))
205
205
  assert.ok(links.includes("this.command({ kind: 'scale-position', part: this.partId, axis })"))
206
206
  assert.ok(links.includes("const times = Math.round((current / size) * 1e6) / 1e6"), '비례는 지금 치수의 몫')
207
207
  })
208
208
 
209
209
  test('놓일 수 있는 크기 — 남은 만큼 채우던 부품을 비례로 바꾸면 다시 재어 넓힌다', () => {
210
- assert.equal(instanceRangeOf(createV3Asset('Probe')), undefined, '크기를 선언하지 않은 도형에는 범위가 없다')
210
+ assert.equal(instanceRangeOf(createFigureAsset('Probe')), undefined, '크기를 선언하지 않은 도형에는 범위가 없다')
211
211
  let asset = sizedPair()
212
212
  for (const axis of ['x', 'z'] as const) {
213
213
  asset.document.model.inputs.push({ id: `size.${axis}`, unit: 'mm', min: 1, max: 20000, role: 'design' } as never)
@@ -220,9 +220,9 @@ test('놓일 수 있는 크기 — 남은 만큼 채우던 부품을 비례로
220
220
  assert.deepEqual(range.declared.y, { min: 90, max: 1000 })
221
221
  assert.deepEqual(range.measured.y, { min: 1, max: 1000 }, '기본 범위: 1 mm 에서 열 배')
222
222
  assert.equal(rangeDiffers(range.declared, range.measured), true)
223
- asset = applyV3Authoring(asset, { kind: 'declare-instance-size', range: range.measured, replace: true })
223
+ asset = applyFigureAuthoring(asset, { kind: 'declare-instance-size', range: range.measured, replace: true })
224
224
  assert.equal(rangeDiffers(instanceRangeOf(asset)!.declared, instanceRangeOf(asset)!.measured), false, '다시 재면 같다')
225
- const editor = read('../client/modeller/v3-editor.ts')
225
+ const editor = read('../client/modeller/editor.ts')
226
226
  assert.ok(editor.includes("this.linkCommand({ kind: 'declare-instance-size', range: range.measured, replace: true })"))
227
227
  })
228
228
 
@@ -237,30 +237,30 @@ test('놓일 수 있는 크기 — 저작자가 끝을 적어 좁힌다(갠트
237
237
  const next = rangeWith(declared, 'x', 'min', 150)!
238
238
  assert.deepEqual(next.x, { min: 150, max: 20000 })
239
239
  assert.deepEqual(declared.x, { min: 1, max: 20000 }, '선언된 범위는 그대로')
240
- asset = applyV3Authoring(asset, { kind: 'declare-instance-size', range: next, replace: true })
240
+ asset = applyFigureAuthoring(asset, { kind: 'declare-instance-size', range: next, replace: true })
241
241
  assert.deepEqual(instanceRangeOf(asset)!.declared.x, { min: 150, max: 20000 })
242
- assert.throws(() => applyV3Authoring(asset, { kind: 'declare-instance-size', range: rangeWith(next, 'x', 'min', 300)!, replace: true }), (e: { code?: string }) => e.code === 'INSTANCE_SIZE_RANGE', '저작한 크기(200)를 벗어나면 거절')
243
- const editor = read('../client/modeller/v3-editor.ts')
242
+ assert.throws(() => applyFigureAuthoring(asset, { kind: 'declare-instance-size', range: rangeWith(next, 'x', 'min', 300)!, replace: true }), (e: { code?: string }) => e.code === 'INSTANCE_SIZE_RANGE', '저작한 크기(200)를 벗어나면 거절')
243
+ const editor = read('../client/modeller/editor.ts')
244
244
  assert.ok(editor.includes("const next = rangeWith(range.declared, axis, bound, (e.target as HTMLInputElement).valueAsNumber)"))
245
245
  assert.ok(editor.includes("if (next) this.linkCommand({ kind: 'declare-instance-size', range: next, replace: true })"))
246
246
  })
247
247
 
248
248
  test('한 비율로 커지는 도형 — 자기 숫자인 길이를 세고, 한 번에 따라가게 하며, 무엇이 남는지 이름을 댄다(아키텍트 판정 09-25)', () => {
249
- let asset = createV3Asset('Probe')
250
- asset = addV3Box(asset, { id: 'base', size: { x: 400, y: 100, z: 400 }, position: { x: 0, y: 0, z: 0 } })
251
- asset = addV3Box(asset, { id: 'lamp', size: { x: 60, y: 60, z: 60 }, position: { x: 0, y: 0, z: 0 } })
252
- asset = applyV3Authoring(asset, { kind: 'attach', part: 'base', face: 'bottom', to: { plane: 'mounting-plane' } })
253
- asset = applyV3Authoring(asset, { kind: 'attach', part: 'lamp', face: 'bottom', to: { part: 'base', face: 'top' }, align: { x: { mm: 120 } } })
254
- asset = applyV3Authoring(asset, { kind: 'declare-instance-size', range: { x: { min: 40, max: 4000 }, y: { min: 16, max: 1600 }, z: { min: 40, max: 4000 } } })
249
+ let asset = createFigureAsset('Probe')
250
+ asset = addFigureBox(asset, { id: 'base', size: { x: 400, y: 100, z: 400 }, position: { x: 0, y: 0, z: 0 } })
251
+ asset = addFigureBox(asset, { id: 'lamp', size: { x: 60, y: 60, z: 60 }, position: { x: 0, y: 0, z: 0 } })
252
+ asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'base', face: 'bottom', to: { plane: 'mounting-plane' } })
253
+ asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'lamp', face: 'bottom', to: { part: 'base', face: 'top' }, align: { x: { mm: 120 } } })
254
+ asset = applyFigureAuthoring(asset, { kind: 'declare-instance-size', range: { x: { min: 40, max: 4000 }, y: { min: 16, max: 1600 }, z: { min: 40, max: 4000 } } })
255
255
  assert.equal(proportionToFollow(asset), undefined, '한 비율이 아니면 권하지 않는다')
256
- asset = applyV3Authoring(asset, { kind: 'scale-uniformly' })
256
+ asset = applyFigureAuthoring(asset, { kind: 'scale-uniformly' })
257
257
  assert.deepEqual(proportionToFollow(asset), { count: 3 + 3 + 1, kept: [] }, '치수 여섯과 자리 하나')
258
258
  const lamp = asset.joints!.find(j => j.id === 'lamp')!
259
- asset = applyV3Authoring(asset, { kind: 'set-joint', joint: { ...lamp, type: 'prismatic', axis: 'Y', lowerLimit: 0, upperLimit: 50, state: { id: 'rise' } } })
260
- asset = applyV3Authoring(asset, { kind: 'follow-size-in-proportion' })
259
+ asset = applyFigureAuthoring(asset, { kind: 'set-joint', joint: { ...lamp, type: 'prismatic', axis: 'Y', lowerLimit: 0, upperLimit: 50, state: { id: 'rise' } } })
260
+ asset = applyFigureAuthoring(asset, { kind: 'follow-size-in-proportion' })
261
261
  assert.deepEqual(proportionToFollow(asset), { count: 0, kept: ['lamp.motion'] }, '다 따르고, mm 로 움직이는 것만 남는다')
262
262
  assert.equal(Math.round(valueNow(asset, 'lamp.dimension.x.value')! * 1e6) / 1e6, 60, '따르게 해도 지금 크기는 그대로')
263
- const editor = read('../client/modeller/v3-editor.ts')
263
+ const editor = read('../client/modeller/editor.ts')
264
264
  assert.ok(editor.includes("this.linkCommand({ kind: 'follow-size-in-proportion' })"))
265
265
  assert.ok(editor.includes("?? valueNow(asset, layout?.args[4])"), '길이를 따르는 간격도 숫자로 보인다')
266
266
  })
@@ -270,7 +270,7 @@ test('관절이 있는 도형에는 한 비율로 키우기를 권하고, 선언
270
270
  assert.equal(turnsAboutAJoint(asset), false)
271
271
  asset = moving(asset, 'flange', { kind: 'turn', axis: 'y', id: 'flange.turn', min: 0, max: 90 })
272
272
  assert.equal(turnsAboutAJoint(asset), true)
273
- const editor = read('../client/modeller/v3-editor.ts')
273
+ const editor = read('../client/modeller/editor.ts')
274
274
  const rule = editor.slice(editor.indexOf(' private renderSizingRule('), editor.indexOf(' /** Choose a part,'))
275
275
  assert.ok(rule.includes("asset.sizing === 'uniform'") && rule.includes("'figure.label.sizing-uniform'"), '선언하면 규칙을 말한다')
276
276
  assert.ok(rule.includes("turnsAboutAJoint(asset) ? html`<p hint>${i18next.t('figure.text.sizing-uniform-proposed')}"), '관절이 있으면 권한다')
@@ -279,10 +279,10 @@ test('관절이 있는 도형에는 한 비율로 키우기를 권하고, 선언
279
279
 
280
280
 
281
281
  test('붙일 때 면 위 어디에 — 그 면을 가로지르는 두 축만, 바닥에는 모서리가 없다 (문을 틀 모서리에 달기, 09-25)', async () => {
282
- const { alignAxesOf, alignOf } = await import('../client/modeller/v3-links.ts')
283
- let two = createV3Asset('Two')
284
- two = addV3Box(two, { id: 'jamb', size: { x: 100, y: 2100, z: 150 }, position: { x: 0, y: 1050, z: 0 } })
285
- two = addV3Box(two, { id: 'leaf', size: { x: 900, y: 2000, z: 40 }, position: { x: 600, y: 1000, z: 0 } })
282
+ const { alignAxesOf, alignOf } = await import('../client/modeller/links.ts')
283
+ let two = createFigureAsset('Two')
284
+ two = addFigureBox(two, { id: 'jamb', size: { x: 100, y: 2100, z: 150 }, position: { x: 0, y: 1050, z: 0 } })
285
+ two = addFigureBox(two, { id: 'leaf', size: { x: 900, y: 2000, z: 40 }, position: { x: 600, y: 1000, z: 0 } })
286
286
  const right = { part: 'jamb', face: 'right' }
287
287
  assert.deepEqual(alignAxesOf(two, 'leaf', right), [{ axis: 'y', edges: true }, { axis: 'z', edges: true }])
288
288
  assert.deepEqual(alignAxesOf(two, 'leaf', 'plane'), [{ axis: 'x', edges: false }, { axis: 'z', edges: false }])
@@ -293,63 +293,63 @@ test('붙일 때 면 위 어디에 — 그 면을 가로지르는 두 축만,
293
293
  assert.equal(alignOf(two, 'leaf', { part: 'jamb', face: 'top' }, {}, {}), undefined)
294
294
 
295
295
  /* What the form sends is what hangs the part on the edge: the door's pivot is on the jamb's front edge line. */
296
- let asset = createV3Asset('Door')
297
- asset = addV3Box(asset, { id: 'jamb', size: { x: 100, y: 2100, z: 150 }, position: { x: 0, y: 1050, z: 0 } })
298
- asset = addV3Box(asset, { id: 'leaf', size: { x: 900, y: 2000, z: 40 }, position: { x: 600, y: 1000, z: 0 } })
299
- asset = applyV3Authoring(asset, { kind: 'attach', part: 'jamb', face: 'bottom', to: { plane: 'mounting-plane' } })
300
- asset = applyV3Authoring(asset, { kind: 'attach', part: 'leaf', face: 'left', to: { part: 'jamb', face: 'right' }, align: alignOf(asset, 'leaf', { part: 'jamb', face: 'right' }, { y: 'min', z: 'max' }, {}) })
301
- const { compileV3Asset } = await import('@hatiolab/figure-model')
302
- const leaf = compileV3Asset(asset).evaluate().geometry.find(g => g.id === 'leaf')!
296
+ let asset = createFigureAsset('Door')
297
+ asset = addFigureBox(asset, { id: 'jamb', size: { x: 100, y: 2100, z: 150 }, position: { x: 0, y: 1050, z: 0 } })
298
+ asset = addFigureBox(asset, { id: 'leaf', size: { x: 900, y: 2000, z: 40 }, position: { x: 600, y: 1000, z: 0 } })
299
+ asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'jamb', face: 'bottom', to: { plane: 'mounting-plane' } })
300
+ asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'leaf', face: 'left', to: { part: 'jamb', face: 'right' }, align: alignOf(asset, 'leaf', { part: 'jamb', face: 'right' }, { y: 'min', z: 'max' }, {}) })
301
+ const { compileFigureAsset } = await import('@hatiolab/figure-model')
302
+ const leaf = compileFigureAsset(asset).evaluate().geometry.find(g => g.id === 'leaf')!
303
303
  assert.deepEqual(leaf.pose.t.map(Math.round), [500, 1000, 55], '문짝 아래가 문틀 아래에, 앞이 문틀 앞에')
304
304
  })
305
305
 
306
306
  test('붙여도 제자리 — 폼은 지금 서 있는 자리를 가운데서 거리로 채운다 (문짝이 문틀 앞 100 mm 에서 가운데로 옮겨졌다, 09-26)', async () => {
307
- const { alignOf, placeKeptOf } = await import('../client/modeller/v3-links.ts')
308
- const { compileV3Asset } = await import('@hatiolab/figure-model')
309
- let asset = createV3Asset('Door')
310
- asset = addV3Box(asset, { id: 'frame', size: { x: 1600, y: 100, z: 150 }, position: { x: 300, y: 50, z: 0 } })
311
- asset = addV3Box(asset, { id: 'leaf', size: { x: 1400, y: 2200, z: 40 }, position: { x: 250, y: 1100, z: 100 } })
312
- const where = (a: V3Asset) => compileV3Asset(a).evaluate().geometry.find(g => g.id === 'leaf')!.pose.t.map(v => Math.round(v))
307
+ const { alignOf, placeKeptOf } = await import('../client/modeller/links.ts')
308
+ const { compileFigureAsset } = await import('@hatiolab/figure-model')
309
+ let asset = createFigureAsset('Door')
310
+ asset = addFigureBox(asset, { id: 'frame', size: { x: 1600, y: 100, z: 150 }, position: { x: 300, y: 50, z: 0 } })
311
+ asset = addFigureBox(asset, { id: 'leaf', size: { x: 1400, y: 2200, z: 40 }, position: { x: 250, y: 1100, z: 100 } })
312
+ const where = (a: FigureAsset) => compileFigureAsset(a).evaluate().geometry.find(g => g.id === 'leaf')!.pose.t.map(v => Math.round(v))
313
313
  for (const target of ['plane', { part: 'frame', face: 'top' }] as const) {
314
314
  const kept = placeKeptOf(asset, 'leaf', target)
315
315
  const align = alignOf(asset, 'leaf', target, kept.align, kept.alignMm)
316
316
  const to = target === 'plane' ? { plane: 'mounting-plane' as const } : target
317
- const fastened = applyV3Authoring(asset, { kind: 'attach', part: 'leaf', face: 'bottom', to, ...(align ? { align } : {}) } as never)
317
+ const fastened = applyFigureAuthoring(asset, { kind: 'attach', part: 'leaf', face: 'bottom', to, ...(align ? { align } : {}) } as never)
318
318
  const [before, after] = [where(asset), where(fastened)]
319
319
  assert.deepEqual([after[0], after[2]], [before[0], before[2]], `${JSON.stringify(target)}: 앞뒤·좌우는 그대로`)
320
320
  }
321
321
  assert.deepEqual(placeKeptOf(asset, 'leaf', { part: 'frame', face: 'top' }), { align: { x: 'mm', z: 'mm' }, alignMm: { x: -50, z: 100 } })
322
- const centred = addV3Box(createV3Asset('C'), { id: 'a', size: { x: 10, y: 10, z: 10 }, position: { x: 0, y: 5, z: 0 } })
322
+ const centred = addFigureBox(createFigureAsset('C'), { id: 'a', size: { x: 10, y: 10, z: 10 }, position: { x: 0, y: 5, z: 0 } })
323
323
  assert.deepEqual(placeKeptOf(centred, 'a', 'plane'), { align: {}, alignMm: {} }, '가운데 있던 것은 가운데로')
324
324
 
325
325
  /* The form fills it in whenever the aim changes, and does not offer a part that hangs from this one. */
326
- const { hangsFrom } = await import('../client/modeller/v3-links.ts')
327
- let row = addV3Box(createV3Asset('Row'), { id: 'plate', size: { x: 1000, y: 50, z: 400 }, position: { x: 0, y: 700, z: 0 } })
328
- row = addV3Box(row, { id: 'roller', size: { x: 60, y: 60, z: 400 }, position: { x: 0, y: 760, z: 0 } })
329
- row = addV3Box(row, { id: 'cap', size: { x: 20, y: 20, z: 20 }, position: { x: 0, y: 800, z: 0 } })
330
- row = applyV3Authoring(row, { kind: 'attach', part: 'roller', face: 'bottom', to: { part: 'plate', face: 'top' } })
331
- row = applyV3Authoring(row, { kind: 'attach', part: 'cap', face: 'bottom', to: { part: 'roller', face: 'top' } })
326
+ const { hangsFrom } = await import('../client/modeller/links.ts')
327
+ let row = addFigureBox(createFigureAsset('Row'), { id: 'plate', size: { x: 1000, y: 50, z: 400 }, position: { x: 0, y: 700, z: 0 } })
328
+ row = addFigureBox(row, { id: 'roller', size: { x: 60, y: 60, z: 400 }, position: { x: 0, y: 760, z: 0 } })
329
+ row = addFigureBox(row, { id: 'cap', size: { x: 20, y: 20, z: 20 }, position: { x: 0, y: 800, z: 0 } })
330
+ row = applyFigureAuthoring(row, { kind: 'attach', part: 'roller', face: 'bottom', to: { part: 'plate', face: 'top' } })
331
+ row = applyFigureAuthoring(row, { kind: 'attach', part: 'cap', face: 'bottom', to: { part: 'roller', face: 'top' } })
332
332
  assert.deepEqual([hangsFrom(row, 'roller', 'plate'), hangsFrom(row, 'cap', 'plate'), hangsFrom(row, 'plate', 'roller')], [true, true, false])
333
- const form = read('../client/modeller/figure-v3-links.ts')
333
+ const form = read('../client/modeller/figure-links.ts')
334
334
  assert.equal(form.match(/this\.aimAt\(asset, \{ \.\.\.f, (toPlane|target|targetFace):/g)?.length, 3, '바닥 · 상대 부품 · 상대 면을 바꿀 때마다')
335
335
  assert.ok(form.includes('!hangsFrom(asset, one, this.partId)'))
336
336
  })
337
337
 
338
338
  test('따라 움직이는 관절 — 같은 방식으로 움직이고 제 조작값이 있는 관절만 고르고, 기록에는 배율과 더하기만 (집게 두 손가락, 09-25)', async () => {
339
- const { leadersFor, motionJoint } = await import('../client/modeller/v3-links.ts')
340
- const { compileV3Asset } = await import('@hatiolab/figure-model')
341
- let asset = createV3Asset('Gripper')
342
- asset = addV3Box(asset, { id: 'palm', size: { x: 240, y: 70, z: 160 }, position: { x: 0, y: 35, z: 0 } })
343
- for (const id of ['left', 'right']) asset = addV3Box(asset, { id, size: { x: 30, y: 140, z: 100 }, position: { x: 0, y: 200, z: 0 } })
344
- asset = addV3Box(asset, { id: 'wrist', size: { x: 60, y: 60, z: 60 }, position: { x: 0, y: 400, z: 0 } })
345
- asset = applyV3Authoring(asset, { kind: 'attach', part: 'palm', face: 'bottom', to: { plane: 'mounting-plane' } })
346
- asset = applyV3Authoring(asset, { kind: 'attach', part: 'left', face: 'bottom', to: { part: 'palm', face: 'top' }, align: { x: 'min' } })
347
- asset = applyV3Authoring(asset, { kind: 'attach', part: 'right', face: 'bottom', to: { part: 'palm', face: 'top' }, align: { x: 'max' } })
348
- asset = applyV3Authoring(asset, { kind: 'attach', part: 'wrist', face: 'top', to: { part: 'palm', face: 'bottom' } })
339
+ const { leadersFor, motionJoint } = await import('../client/modeller/links.ts')
340
+ const { compileFigureAsset } = await import('@hatiolab/figure-model')
341
+ let asset = createFigureAsset('Gripper')
342
+ asset = addFigureBox(asset, { id: 'palm', size: { x: 240, y: 70, z: 160 }, position: { x: 0, y: 35, z: 0 } })
343
+ for (const id of ['left', 'right']) asset = addFigureBox(asset, { id, size: { x: 30, y: 140, z: 100 }, position: { x: 0, y: 200, z: 0 } })
344
+ asset = addFigureBox(asset, { id: 'wrist', size: { x: 60, y: 60, z: 60 }, position: { x: 0, y: 400, z: 0 } })
345
+ asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'palm', face: 'bottom', to: { plane: 'mounting-plane' } })
346
+ asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'left', face: 'bottom', to: { part: 'palm', face: 'top' }, align: { x: 'min' } })
347
+ asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'right', face: 'bottom', to: { part: 'palm', face: 'top' }, align: { x: 'max' } })
348
+ asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'wrist', face: 'top', to: { part: 'palm', face: 'bottom' } })
349
349
  const form = { kind: 'slide' as const, axis: 'x' as const, id: 'grip', min: 0, max: 60, follow: '', multiplier: 1, offset: 0 }
350
350
  const jointOf = (part: string) => asset.joints!.find(j => j.body1.part === part)!
351
- asset = applyV3Authoring(asset, { kind: 'set-joint', joint: motionJoint(jointOf('left'), form, 'left') })
352
- asset = applyV3Authoring(asset, { kind: 'set-joint', joint: motionJoint(jointOf('wrist'), { ...form, kind: 'turn', axis: 'y', id: 'twist', min: -90, max: 90 }, 'wrist') })
351
+ asset = applyFigureAuthoring(asset, { kind: 'set-joint', joint: motionJoint(jointOf('left'), form, 'left') })
352
+ asset = applyFigureAuthoring(asset, { kind: 'set-joint', joint: motionJoint(jointOf('wrist'), { ...form, kind: 'turn', axis: 'y', id: 'twist', min: -90, max: 90 }, 'wrist') })
353
353
 
354
354
  assert.deepEqual(leadersFor(asset, 'right', 'slide').map(j => j.id), ['left'], '미끄러지는 관절은 미끄러지는 관절을 따른다')
355
355
  assert.deepEqual(leadersFor(asset, 'right', 'turn').map(j => j.id), ['wrist'])
@@ -359,28 +359,28 @@ test('따라 움직이는 관절 — 같은 방식으로 움직이고 제 조작
359
359
  assert.deepEqual(follower.mimic, { joint: 'left', multiplier: -1 })
360
360
  assert.equal(follower.state, undefined, '따라가는 관절은 제 조작값이 없다')
361
361
  assert.equal(follower.lowerLimit, undefined)
362
- asset = applyV3Authoring(asset, { kind: 'set-joint', joint: follower })
362
+ asset = applyFigureAuthoring(asset, { kind: 'set-joint', joint: follower })
363
363
  assert.deepEqual(leadersFor(asset, 'wrist', 'slide').map(j => j.id), ['left'], '따라가는 관절은 앞장서지 않는다')
364
364
 
365
- const x = (id: string, grip: number) => Math.round(compileV3Asset(asset).evaluate({ grip }).geometry.find(g => g.id === id)!.pose.t[0]!)
365
+ const x = (id: string, grip: number) => Math.round(compileFigureAsset(asset).evaluate({ grip }).geometry.find(g => g.id === id)!.pose.t[0]!)
366
366
  assert.equal(x('left', 40) - x('left', 0), 40)
367
367
  assert.equal(x('right', 40) - x('right', 0), -40, '오른손은 거울로 따라 닫힌다')
368
- const { motionOf } = await import('../client/modeller/v3-links.ts')
368
+ const { motionOf } = await import('../client/modeller/links.ts')
369
369
  assert.deepEqual(motionOf(asset, 'right')?.follows, { joint: 'left', multiplier: -1 }, '화면은 무엇을 따르는지 읽는다')
370
370
  })
371
371
 
372
372
  test('면은 도형이 정한다 — 문틀(portal)은 바깥 여섯에 개구부 셋, 공은 없음, 원뿔대는 위아래뿐 (2026-09-25)', async () => {
373
- const { facesOf, faceAxisOf, canFasten, alignAxesOf } = await import('../client/modeller/v3-links.ts')
374
- let asset = createV3Asset('Doorway')
375
- asset = addV3Primitive(asset, {
373
+ const { facesOf, faceAxisOf, canFasten, alignAxesOf } = await import('../client/modeller/links.ts')
374
+ let asset = createFigureAsset('Doorway')
375
+ asset = addFigurePrimitive(asset, {
376
376
  id: 'frame',
377
377
  kind: 'portal',
378
378
  dimensions: { width: 1100, height: 2200, depth: 150, jamb: 100, head: 100 },
379
379
  position: { x: 0, y: 0, z: 0 }
380
380
  })
381
- asset = addV3Primitive(asset, { id: 'ball', kind: 'sphere', dimensions: { radiusX: 50, radiusY: 50, radiusZ: 50 }, position: { x: 0, y: 0, z: 0 } })
382
- asset = addV3Primitive(asset, { id: 'cone', kind: 'frustum', dimensions: { radiusTop: 20, radiusBottom: 60, height: 100 }, position: { x: 0, y: 0, z: 0 } })
383
- asset = addV3Box(asset, { id: 'leaf', size: { x: 890, y: 2095, z: 40 }, position: { x: 0, y: 0, z: 0 } })
381
+ asset = addFigurePrimitive(asset, { id: 'ball', kind: 'sphere', dimensions: { radiusX: 50, radiusY: 50, radiusZ: 50 }, position: { x: 0, y: 0, z: 0 } })
382
+ asset = addFigurePrimitive(asset, { id: 'cone', kind: 'frustum', dimensions: { radiusTop: 20, radiusBottom: 60, height: 100 }, position: { x: 0, y: 0, z: 0 } })
383
+ asset = addFigureBox(asset, { id: 'leaf', size: { x: 890, y: 2095, z: 40 }, position: { x: 0, y: 0, z: 0 } })
384
384
  assert.deepEqual(facesOf(asset, 'frame'), ['left', 'right', 'bottom', 'top', 'back', 'front', 'opening-left', 'opening-right', 'opening-top'])
385
385
  assert.equal(faceAxisOf(asset, 'frame', 'opening-top'), 'y')
386
386
  assert.deepEqual(facesOf(asset, 'ball'), [])
@@ -392,25 +392,25 @@ test('면은 도형이 정한다 — 문틀(portal)은 바깥 여섯에 개구
392
392
  })
393
393
 
394
394
  test('되풀이는 붙어 있고 움직이지 않고 아무것도 매달지 않은 부품에 — 붙인 면을 가로지르는 축으로 (빈틈 B, 2026-09-25)', async () => {
395
- const { rowAxesOf, rowOf } = await import('../client/modeller/v3-links.ts')
396
- let asset = createV3Asset('Conveyor')
397
- asset = addV3Box(asset, { id: 'frame', size: { x: 2000, y: 100, z: 600 }, position: { x: 0, y: 0, z: 0 } })
398
- asset = addV3Box(asset, { id: 'slat', size: { x: 60, y: 20, z: 560 }, position: { x: 0, y: 0, z: 0 } })
395
+ const { rowAxesOf, rowOf } = await import('../client/modeller/links.ts')
396
+ let asset = createFigureAsset('Conveyor')
397
+ asset = addFigureBox(asset, { id: 'frame', size: { x: 2000, y: 100, z: 600 }, position: { x: 0, y: 0, z: 0 } })
398
+ asset = addFigureBox(asset, { id: 'slat', size: { x: 60, y: 20, z: 560 }, position: { x: 0, y: 0, z: 0 } })
399
399
  assert.deepEqual(rowAxesOf(asset, 'slat'), [], '붙지 않은 부품은 되풀이를 권하지 않는다')
400
- asset = applyV3Authoring(asset, { kind: 'attach', part: 'frame', face: 'bottom', to: { plane: 'mounting-plane' } })
401
- asset = applyV3Authoring(asset, { kind: 'attach', part: 'slat', face: 'bottom', to: { part: 'frame', face: 'top' } })
400
+ asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'frame', face: 'bottom', to: { plane: 'mounting-plane' } })
401
+ asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'slat', face: 'bottom', to: { part: 'frame', face: 'top' } })
402
402
  assert.deepEqual(rowAxesOf(asset, 'frame'), [], '바닥에 선 부품은 채울 길이가 없다')
403
403
  assert.deepEqual(rowAxesOf(asset, 'slat'), ['x', 'z'], '윗면을 가로지르는 두 축')
404
- asset = applyV3Authoring(asset, { kind: 'make-repeat', part: 'slat', axis: 'x', pitch: 100 })
404
+ asset = applyFigureAuthoring(asset, { kind: 'make-repeat', part: 'slat', axis: 'x', pitch: 100 })
405
405
  assert.deepEqual(rowOf(asset, 'slat'), { axis: 'X', pitch: 100 })
406
406
  assert.deepEqual(rowAxesOf(asset, 'slat'), [], '이미 되풀이면 다시 권하지 않는다')
407
407
  })
408
408
 
409
409
  test('비튼 부품에는 모서리 맞춤을 내놓지도 보내지도 않는다 — 가운데서 거리만 (빈틈 F, 2026-09-25)', async () => {
410
- const { alignAxesOf, alignOf } = await import('../client/modeller/v3-links.ts')
411
- let asset = createV3Asset('Fan')
412
- asset = addV3Box(asset, { id: 'hub', size: { x: 120, y: 120, z: 80 }, position: { x: 0, y: 0, z: 0 } })
413
- asset = addV3Box(asset, { id: 'blade', size: { x: 300, y: 60, z: 8 }, position: { x: 0, y: 0, z: 0 } })
410
+ const { alignAxesOf, alignOf } = await import('../client/modeller/links.ts')
411
+ let asset = createFigureAsset('Fan')
412
+ asset = addFigureBox(asset, { id: 'hub', size: { x: 120, y: 120, z: 80 }, position: { x: 0, y: 0, z: 0 } })
413
+ asset = addFigureBox(asset, { id: 'blade', size: { x: 300, y: 60, z: 8 }, position: { x: 0, y: 0, z: 0 } })
414
414
  const front = { part: 'hub', face: 'front' }
415
415
  assert.deepEqual(alignAxesOf(asset, 'blade', front, true), [{ axis: 'x', edges: false }, { axis: 'y', edges: false }])
416
416
  assert.deepEqual(alignOf(asset, 'blade', front, { x: 'max', y: 'mm' }, { y: 50 }, true), { y: { mm: 50 } }, '끝 맞춤은 빠지고 거리만 간다')
@@ -418,58 +418,58 @@ test('비튼 부품에는 모서리 맞춤을 내놓지도 보내지도 않는
418
418
  })
419
419
 
420
420
  test('모델러에서 만든 도형은 부품에서 재어 도형 크기를 선언한다 — 그 뒤에야 부품이 도형 크기를 따를 수 있다 (사용자 09-25)', async () => {
421
- const { instanceRangeToDeclare, instanceRangeOf, hasInstanceSize } = await import('../client/modeller/v3-links.ts')
422
- const { compileV3Asset } = await import('@hatiolab/figure-model')
423
- let asset = createV3Asset('Conveyor')
421
+ const { instanceRangeToDeclare, instanceRangeOf, hasInstanceSize } = await import('../client/modeller/links.ts')
422
+ const { compileFigureAsset } = await import('@hatiolab/figure-model')
423
+ let asset = createFigureAsset('Conveyor')
424
424
  assert.equal(instanceRangeToDeclare(asset), undefined, '부품이 없으면 잴 것이 없다')
425
- asset = addV3Box(asset, { id: 'frame', size: { x: 2000, y: 100, z: 600 }, position: { x: 0, y: 0, z: 0 } })
426
- asset = applyV3Authoring(asset, { kind: 'attach', part: 'frame', face: 'bottom', to: { plane: 'mounting-plane' } })
425
+ asset = addFigureBox(asset, { id: 'frame', size: { x: 2000, y: 100, z: 600 }, position: { x: 0, y: 0, z: 0 } })
426
+ asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'frame', face: 'bottom', to: { plane: 'mounting-plane' } })
427
427
  assert.equal(hasInstanceSize(asset, 'x'), false, '새 도형에는 도형 크기가 없다')
428
428
  const toDeclare = instanceRangeToDeclare(asset)!
429
429
  assert.deepEqual(toDeclare.at, { x: 2000, y: 100, z: 600 }, '지금 부품의 크기가 선언할 크기')
430
- asset = applyV3Authoring(asset, { kind: 'declare-instance-size', range: toDeclare.range, at: toDeclare.at })
430
+ asset = applyFigureAuthoring(asset, { kind: 'declare-instance-size', range: toDeclare.range, at: toDeclare.at })
431
431
  assert.ok(instanceRangeOf(asset), '선언 뒤에는 놓일 수 있는 크기가 보인다')
432
432
  assert.equal(instanceRangeToDeclare(asset), undefined, '이미 있으면 다시 권하지 않는다')
433
433
  assert.equal(hasInstanceSize(asset, 'x'), true, '부품이 도형 크기를 따르는 선택이 열린다')
434
- asset = applyV3Authoring(asset, { kind: 'link-dimension', part: 'frame', axis: 'x', source: { of: 'instance', axis: 'x' } } as never)
435
- const at = (size: number) => (compileV3Asset({ ...asset, designInputs: { ...asset.designInputs, 'size.x': size } }).evaluate().geometry.find(g => g.id === 'frame')!.shape.dimensions as { width: number }).width
434
+ asset = applyFigureAuthoring(asset, { kind: 'link-dimension', part: 'frame', axis: 'x', source: { of: 'instance', axis: 'x' } } as never)
435
+ const at = (size: number) => (compileFigureAsset({ ...asset, designInputs: { ...asset.designInputs, 'size.x': size } }).evaluate().geometry.find(g => g.id === 'frame')!.shape.dimensions as { width: number }).width
436
436
  assert.equal(at(3000), 3000, '보드가 3000 으로 놓으면 틀도 3000')
437
437
  })
438
438
 
439
439
  test('따르는 치수의 더하기를 읽는다 — 문짝 폭 = 개구부 − 틈 (사이징, 2026-09-25)', async () => {
440
- const { followOf, choiceOf } = await import('../client/modeller/v3-links.ts')
441
- let asset = createV3Asset('Door')
442
- asset = addV3Box(asset, { id: 'head', size: { x: 1100, y: 100, z: 150 }, position: { x: 0, y: 0, z: 0 } })
443
- asset = addV3Box(asset, { id: 'leaf', size: { x: 890, y: 2095, z: 40 }, position: { x: 0, y: 0, z: 0 } })
444
- asset = applyV3Authoring(asset, { kind: 'link-dimension', part: 'leaf', axis: 'x', source: { of: 'part', part: 'head', axis: 'x' }, plus: -210 })
440
+ const { followOf, choiceOf } = await import('../client/modeller/links.ts')
441
+ let asset = createFigureAsset('Door')
442
+ asset = addFigureBox(asset, { id: 'head', size: { x: 1100, y: 100, z: 150 }, position: { x: 0, y: 0, z: 0 } })
443
+ asset = addFigureBox(asset, { id: 'leaf', size: { x: 890, y: 2095, z: 40 }, position: { x: 0, y: 0, z: 0 } })
444
+ asset = applyFigureAuthoring(asset, { kind: 'link-dimension', part: 'leaf', axis: 'x', source: { of: 'part', part: 'head', axis: 'x' }, plus: -210 })
445
445
  assert.deepEqual(followOf(asset, 'leaf', 'x'), { of: 'part', part: 'head', axis: 'x', plus: -210 })
446
446
  assert.equal(choiceOf(followOf(asset, 'leaf', 'x')), 'part:head:x', '선택은 그대로 — 더하기는 따로')
447
- const { compileV3Asset } = await import('@hatiolab/figure-model')
447
+ const { compileFigureAsset } = await import('@hatiolab/figure-model')
448
448
  const wide = { ...asset, designInputs: { ...asset.designInputs, 'head.size.x': 1500 } }
449
- assert.equal((compileV3Asset(wide).evaluate().geometry.find(g => g.id === 'leaf')!.shape.dimensions as { width: number }).width, 1290, '상인방 1500 이면 문짝 1290')
450
- asset = applyV3Authoring(asset, { kind: 'declare-instance-size', range: { x: { min: 500, max: 5000 }, y: { min: 500, max: 5000 }, z: { min: 50, max: 500 } }, at: { x: 1100, y: 2200, z: 150 } })
451
- asset = applyV3Authoring(asset, { kind: 'link-dimension', part: 'head', axis: 'x', source: { of: 'instance', axis: 'x' }, times: 0.5, plus: 20 })
449
+ assert.equal((compileFigureAsset(wide).evaluate().geometry.find(g => g.id === 'leaf')!.shape.dimensions as { width: number }).width, 1290, '상인방 1500 이면 문짝 1290')
450
+ asset = applyFigureAuthoring(asset, { kind: 'declare-instance-size', range: { x: { min: 500, max: 5000 }, y: { min: 500, max: 5000 }, z: { min: 50, max: 500 } }, at: { x: 1100, y: 2200, z: 150 } })
451
+ asset = applyFigureAuthoring(asset, { kind: 'link-dimension', part: 'head', axis: 'x', source: { of: 'instance', axis: 'x' }, times: 0.5, plus: 20 })
452
452
  assert.deepEqual(followOf(asset, 'head', 'x'), { of: 'instance', axis: 'x', times: 0.5, plus: 20 })
453
453
  })
454
454
 
455
455
  test('미끄러짐의 이동 거리가 부품 길이를 따른다 — 레일이 길어지면 브리지가 끝까지 간다 (사이징, 2026-09-25)', async () => {
456
- const { motionJoint, motionOf } = await import('../client/modeller/v3-links.ts')
457
- const { compileV3Asset } = await import('@hatiolab/figure-model')
458
- let asset = createV3Asset('Gantry')
459
- asset = addV3Box(asset, { id: 'rail', size: { x: 3000, y: 120, z: 120 }, position: { x: 0, y: 0, z: 0 } })
460
- asset = addV3Box(asset, { id: 'bridge', size: { x: 160, y: 160, z: 1300 }, position: { x: 0, y: 0, z: 0 } })
461
- asset = applyV3Authoring(asset, { kind: 'attach', part: 'rail', face: 'bottom', to: { plane: 'mounting-plane' } })
462
- asset = applyV3Authoring(asset, { kind: 'attach', part: 'bridge', face: 'bottom', to: { part: 'rail', face: 'top' }, align: { x: 'min' } })
456
+ const { motionJoint, motionOf } = await import('../client/modeller/links.ts')
457
+ const { compileFigureAsset } = await import('@hatiolab/figure-model')
458
+ let asset = createFigureAsset('Gantry')
459
+ asset = addFigureBox(asset, { id: 'rail', size: { x: 3000, y: 120, z: 120 }, position: { x: 0, y: 0, z: 0 } })
460
+ asset = addFigureBox(asset, { id: 'bridge', size: { x: 160, y: 160, z: 1300 }, position: { x: 0, y: 0, z: 0 } })
461
+ asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'rail', face: 'bottom', to: { plane: 'mounting-plane' } })
462
+ asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'bridge', face: 'bottom', to: { part: 'rail', face: 'top' }, align: { x: 'min' } })
463
463
  const joint = asset.joints!.find(j => j.body1.part === 'bridge')!
464
464
  const form = { kind: 'slide' as const, axis: 'x' as const, id: 'x', min: 0, max: 500, follow: '', multiplier: 1, offset: 0, travel: 'rail:x', travelPlus: -160 }
465
465
  const record = motionJoint(joint, form, 'bridge')
466
466
  assert.deepEqual(record.travel, { source: { of: 'part', part: 'rail', axis: 'x' }, plus: -160 })
467
467
  assert.equal(record.lowerLimit, undefined, '거리는 레일이 정한다 — mm 범위는 없다')
468
- asset = applyV3Authoring(asset, { kind: 'set-joint', joint: record })
468
+ asset = applyFigureAuthoring(asset, { kind: 'set-joint', joint: record })
469
469
  assert.deepEqual(motionOf(asset, 'bridge')?.travel, { part: 'rail', axis: 'x', plus: -160 }, '화면은 무엇을 따르는지 읽는다')
470
470
  const end = (railLength: number) => {
471
471
  const a = { ...asset, designInputs: { ...asset.designInputs, 'rail.size.x': railLength } }
472
- const g = (id: string) => compileV3Asset(a).evaluate({ x: 1 }).geometry.find(x => x.id === id)! as unknown as { pose: { t: number[] }; shape: { dimensions: { width: number } } }
472
+ const g = (id: string) => compileFigureAsset(a).evaluate({ x: 1 }).geometry.find(x => x.id === id)! as unknown as { pose: { t: number[] }; shape: { dimensions: { width: number } } }
473
473
  return Math.round(g('bridge').pose.t[0]! + 80 - (g('rail').pose.t[0]! + railLength / 2))
474
474
  }
475
475
  assert.equal(end(3000), 0, '끝까지(1) 가면 브리지 끝이 레일 끝')
@@ -477,47 +477,47 @@ test('미끄러짐의 이동 거리가 부품 길이를 따른다 — 레일이
477
477
  })
478
478
 
479
479
  test('면 위 거리를 도형 크기 비율로 — 도형이 커지면 그 자리도 비율로 옮긴다 (사이징, 2026-09-25)', async () => {
480
- const { alignOf } = await import('../client/modeller/v3-links.ts')
481
- const { compileV3Asset } = await import('@hatiolab/figure-model')
482
- let asset = createV3Asset('Panel')
483
- asset = addV3Box(asset, { id: 'panel', size: { x: 1000, y: 800, z: 40 }, position: { x: 0, y: 0, z: 0 } })
484
- asset = addV3Box(asset, { id: 'lamp', size: { x: 60, y: 60, z: 30 }, position: { x: 0, y: 0, z: 0 } })
485
- asset = applyV3Authoring(asset, { kind: 'attach', part: 'panel', face: 'bottom', to: { plane: 'mounting-plane' } })
480
+ const { alignOf } = await import('../client/modeller/links.ts')
481
+ const { compileFigureAsset } = await import('@hatiolab/figure-model')
482
+ let asset = createFigureAsset('Panel')
483
+ asset = addFigureBox(asset, { id: 'panel', size: { x: 1000, y: 800, z: 40 }, position: { x: 0, y: 0, z: 0 } })
484
+ asset = addFigureBox(asset, { id: 'lamp', size: { x: 60, y: 60, z: 30 }, position: { x: 0, y: 0, z: 0 } })
485
+ asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'panel', face: 'bottom', to: { plane: 'mounting-plane' } })
486
486
  const front = { part: 'panel', face: 'front' }
487
487
  assert.deepEqual(alignOf(asset, 'lamp', front, { x: 'share' }, { x: 300 }), undefined, '도형 크기가 없으면 비율을 보낼 수 없다')
488
- asset = applyV3Authoring(asset, { kind: 'declare-instance-size', range: { x: { min: 200, max: 5000 }, y: { min: 200, max: 5000 }, z: { min: 10, max: 500 } }, at: { x: 1000, y: 800, z: 40 } })
489
- asset = applyV3Authoring(asset, { kind: 'link-dimension', part: 'panel', axis: 'x', source: { of: 'instance', axis: 'x' } })
488
+ asset = applyFigureAuthoring(asset, { kind: 'declare-instance-size', range: { x: { min: 200, max: 5000 }, y: { min: 200, max: 5000 }, z: { min: 10, max: 500 } }, at: { x: 1000, y: 800, z: 40 } })
489
+ asset = applyFigureAuthoring(asset, { kind: 'link-dimension', part: 'panel', axis: 'x', source: { of: 'instance', axis: 'x' } })
490
490
  const align = alignOf(asset, 'lamp', front, { x: 'share' }, { x: 300 })!
491
491
  assert.deepEqual(align, { x: { mm: { from: { of: 'instance', axis: 'x' }, times: 0.3 } } })
492
- asset = applyV3Authoring(asset, { kind: 'attach', part: 'lamp', face: 'back', to: front, align })
493
- const x = (size: number) => Math.round(compileV3Asset({ ...asset, designInputs: { ...asset.designInputs, 'size.x': size } }).evaluate().geometry.find(g => g.id === 'lamp')!.pose.t[0]!)
492
+ asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'lamp', face: 'back', to: front, align })
493
+ const x = (size: number) => Math.round(compileFigureAsset({ ...asset, designInputs: { ...asset.designInputs, 'size.x': size } }).evaluate().geometry.find(g => g.id === 'lamp')!.pose.t[0]!)
494
494
  assert.equal(x(1000), 300)
495
495
  assert.equal(x(2000), 600, '도형이 두 배면 거리도 두 배')
496
496
  })
497
497
 
498
498
  test('선언할 크기는 부품이 쉬고 있을 때의 크기 — 문짝이 도는 자리까지 재지 않는다 (2026-09-25)', async () => {
499
- const { instanceRangeToDeclare } = await import('../client/modeller/v3-links.ts')
500
- let asset = createV3Asset('Door')
501
- asset = addV3Box(asset, { id: 'jamb', size: { x: 100, y: 2100, z: 150 }, position: { x: 0, y: 0, z: 0 } })
502
- asset = addV3Box(asset, { id: 'leaf', size: { x: 890, y: 2095, z: 40 }, position: { x: 0, y: 0, z: 0 } })
503
- asset = applyV3Authoring(asset, { kind: 'attach', part: 'jamb', face: 'bottom', to: { plane: 'mounting-plane' } })
504
- asset = applyV3Authoring(asset, { kind: 'attach', part: 'leaf', face: 'left', to: { part: 'jamb', face: 'right' }, align: { y: 'min', z: 'max' }, gap: 5 })
499
+ const { instanceRangeToDeclare } = await import('../client/modeller/links.ts')
500
+ let asset = createFigureAsset('Door')
501
+ asset = addFigureBox(asset, { id: 'jamb', size: { x: 100, y: 2100, z: 150 }, position: { x: 0, y: 0, z: 0 } })
502
+ asset = addFigureBox(asset, { id: 'leaf', size: { x: 890, y: 2095, z: 40 }, position: { x: 0, y: 0, z: 0 } })
503
+ asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'jamb', face: 'bottom', to: { plane: 'mounting-plane' } })
504
+ asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'leaf', face: 'left', to: { part: 'jamb', face: 'right' }, align: { y: 'min', z: 'max' }, gap: 5 })
505
505
  const j = asset.joints!.find(o => o.id === 'leaf')!
506
- asset = applyV3Authoring(asset, { kind: 'set-joint', joint: { ...j, type: 'revolute', axis: 'Y', lowerLimit: -90, upperLimit: 0, state: { id: 'open' } } })
506
+ asset = applyFigureAuthoring(asset, { kind: 'set-joint', joint: { ...j, type: 'revolute', axis: 'Y', lowerLimit: -90, upperLimit: 0, state: { id: 'open' } } })
507
507
  assert.deepEqual(instanceRangeToDeclare(asset)!.at, { x: 995, y: 2100, z: 150 })
508
508
  })
509
509
 
510
510
  test('바닥에 선 부품도 도형 크기를 선언한 도형이면 도형의 옆면에 맞출 수 있다 (2026-09-25)', async () => {
511
- const { alignAxesOf, alignOf } = await import('../client/modeller/v3-links.ts')
512
- let asset = addV3Box(createV3Asset('Gate'), { id: 'post', size: { x: 100, y: 2000, z: 150 }, position: { x: 0, y: 0, z: 0 } })
511
+ const { alignAxesOf, alignOf } = await import('../client/modeller/links.ts')
512
+ let asset = addFigureBox(createFigureAsset('Gate'), { id: 'post', size: { x: 100, y: 2000, z: 150 }, position: { x: 0, y: 0, z: 0 } })
513
513
  assert.deepEqual(alignAxesOf(asset, 'post', 'plane'), [{ axis: 'x', edges: false }, { axis: 'z', edges: false }], '크기가 없으면 옆면도 없다')
514
- asset = applyV3Authoring(asset, { kind: 'declare-instance-size', range: { x: { min: 200, max: 5000 }, y: { min: 500, max: 5000 }, z: { min: 50, max: 500 } }, at: { x: 1100, y: 2000, z: 150 } })
514
+ asset = applyFigureAuthoring(asset, { kind: 'declare-instance-size', range: { x: { min: 200, max: 5000 }, y: { min: 500, max: 5000 }, z: { min: 50, max: 500 } }, at: { x: 1100, y: 2000, z: 150 } })
515
515
  assert.deepEqual(alignAxesOf(asset, 'post', 'plane'), [{ axis: 'x', edges: true }, { axis: 'z', edges: true }])
516
516
  assert.deepEqual(alignOf(asset, 'post', 'plane', { x: 'min' }, {}), { x: 'min' })
517
517
  })
518
518
 
519
519
  test('움직임은 그 자리에서 고친다 — 폼에 지금 관절을 채우고, 그대로 보내면 같은 관절이다 (2026-09-25)', async () => {
520
- const { motionFormOf, motionJoint } = await import('../client/modeller/v3-links.ts')
520
+ const { motionFormOf, motionJoint } = await import('../client/modeller/links.ts')
521
521
  const base = { id: 'j', body0: { part: 'a', face: 'top' as const }, body1: { part: 'b', face: 'bottom' as const } }
522
522
  const joints = [
523
523
  { ...base, type: 'prismatic' as const, axis: 'Z' as const, lowerLimit: 0, upperLimit: 2500, state: { id: 'extend' } },
@@ -537,28 +537,28 @@ test('움직임은 그 자리에서 고친다 — 폼에 지금 관절을 채우
537
537
  })
538
538
 
539
539
  test('바닥에 선 부품도 무엇에 붙어 있는지 읽는다 — 그래야 옮겨 붙일 수 있다 (문설주, 2026-09-25)', async () => {
540
- const { heldBy } = await import('../client/modeller/v3-links.ts')
541
- let asset = createV3Asset('Door')
542
- asset = addV3Box(asset, { id: 'jamb', size: { x: 100, y: 2100, z: 150 }, position: { x: 0, y: 0, z: 0 } })
543
- asset = addV3Box(asset, { id: 'head', size: { x: 1100, y: 100, z: 150 }, position: { x: 0, y: 0, z: 0 } })
540
+ const { heldBy } = await import('../client/modeller/links.ts')
541
+ let asset = createFigureAsset('Door')
542
+ asset = addFigureBox(asset, { id: 'jamb', size: { x: 100, y: 2100, z: 150 }, position: { x: 0, y: 0, z: 0 } })
543
+ asset = addFigureBox(asset, { id: 'head', size: { x: 1100, y: 100, z: 150 }, position: { x: 0, y: 0, z: 0 } })
544
544
  assert.equal(heldBy(asset, 'jamb'), null)
545
- asset = applyV3Authoring(asset, { kind: 'attach', part: 'jamb', face: 'bottom', to: { plane: 'mounting-plane' } })
546
- asset = applyV3Authoring(asset, { kind: 'attach', part: 'head', face: 'bottom', to: { part: 'jamb', face: 'top' } })
545
+ asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'jamb', face: 'bottom', to: { plane: 'mounting-plane' } })
546
+ asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'head', face: 'bottom', to: { part: 'jamb', face: 'top' } })
547
547
  assert.deepEqual(heldBy(asset, 'jamb'), { plane: true }, '장착 평면')
548
548
  assert.deepEqual(heldBy(asset, 'head'), { part: 'jamb' })
549
549
  /* What the form then sends: a fastening that replaces the one there. */
550
- assert.doesNotThrow(() => applyV3Authoring(asset, { kind: 'attach', part: 'jamb', face: 'bottom', to: { plane: 'mounting-plane' }, align: { x: { mm: -500 } }, replace: !!heldBy(asset, 'jamb') }))
550
+ assert.doesNotThrow(() => applyFigureAuthoring(asset, { kind: 'attach', part: 'jamb', face: 'bottom', to: { plane: 'mounting-plane' }, align: { x: { mm: -500 } }, replace: !!heldBy(asset, 'jamb') }))
551
551
  })
552
552
 
553
553
  test('벽 배치 — 장착 평면은 Z = 0, 등으로 붙이고 가로질러 X · Y 로 맞춘다(사용자 · 아키텍트 09-25)', async () => {
554
- const { mountingPlaneOf, alignAxesOf } = await import('../client/modeller/v3-links.ts')
555
- let a = addV3Box(createV3Asset('Lamp'), { id: 'plate', size: { x: 200, y: 300, z: 20 }, position: { x: 0, y: 0, z: 0 } })
554
+ const { mountingPlaneOf, alignAxesOf } = await import('../client/modeller/links.ts')
555
+ let a = addFigureBox(createFigureAsset('Lamp'), { id: 'plate', size: { x: 200, y: 300, z: 20 }, position: { x: 0, y: 0, z: 0 } })
556
556
  assert.deepEqual(mountingPlaneOf(a), { normal: 'y', faces: ['bottom', 'top'] }, '배치 선언 전에는 바닥')
557
- a = applyV3Authoring(a, { kind: 'declare-occupancy', proposal: (await import('@hatiolab/figure-model')).proposeV3Occupancy(a), placement: 'wall' as never })
557
+ a = applyFigureAuthoring(a, { kind: 'declare-occupancy', proposal: (await import('@hatiolab/figure-model')).proposeFigureOccupancy(a), placement: 'wall' as never })
558
558
  assert.deepEqual(mountingPlaneOf(a), { normal: 'z', faces: ['back'] })
559
559
  assert.deepEqual(alignAxesOf(a, 'plate', 'plane').map(x => x.axis), ['x', 'y'])
560
- const form = read('../client/modeller/figure-v3-links.ts')
561
- assert.ok(form.includes('(!f.toPlane || plane.faces.includes(face as V3Face))'), '평면에 붙일 때는 평면이 받는 면만')
560
+ const form = read('../client/modeller/figure-links.ts')
561
+ assert.ok(form.includes('(!f.toPlane || plane.faces.includes(face as FigureFace))'), '평면에 붙일 때는 평면이 받는 면만')
562
562
  })
563
563
 
564
564
  test('부품 칸의 두 제목이 같은 말이 아니다 — 자리 숫자는 「위치와 크기」, 붙이기 칸은 「붙이기」 (09-26)', () => {
@@ -572,22 +572,22 @@ test('부품 칸의 두 제목이 같은 말이 아니다 — 자리 숫자는
572
572
  * 고르는 칸이 없었다(09-26 기준 측정).
573
573
  */
574
574
  test('보이는 때 — 명령이 남긴 것을 읽고, 폼은 그 명령을 낸다', async () => {
575
- const { shownWhenOf, visibilityControlsOf } = await import('../client/modeller/v3-links.ts')
576
- const { editV3Part } = await import('@hatiolab/figure-model')
577
- let asset = createV3Asset('Gate')
578
- asset = addV3Box(asset, { id: 'frame', size: { x: 1200, y: 60, z: 200 }, position: { x: 0, y: 30, z: 0 } })
579
- asset = addV3Box(asset, { id: 'door', size: { x: 1000, y: 2000, z: 40 }, position: { x: 0, y: 1060, z: 0 } })
580
- asset = addV3Box(asset, { id: 'lamp', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 2200, z: 0 } })
581
- asset = applyV3Authoring(asset, { kind: 'attach', part: 'frame', face: 'bottom', to: { plane: 'mounting-plane' } })
582
- asset = applyV3Authoring(asset, { kind: 'attach', part: 'door', face: 'bottom', to: { part: 'frame', face: 'top' } })
575
+ const { shownWhenOf, visibilityControlsOf } = await import('../client/modeller/links.ts')
576
+ const { editFigurePart } = await import('@hatiolab/figure-model')
577
+ let asset = createFigureAsset('Gate')
578
+ asset = addFigureBox(asset, { id: 'frame', size: { x: 1200, y: 60, z: 200 }, position: { x: 0, y: 30, z: 0 } })
579
+ asset = addFigureBox(asset, { id: 'door', size: { x: 1000, y: 2000, z: 40 }, position: { x: 0, y: 1060, z: 0 } })
580
+ asset = addFigureBox(asset, { id: 'lamp', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 2200, z: 0 } })
581
+ asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'frame', face: 'bottom', to: { plane: 'mounting-plane' } })
582
+ asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'door', face: 'bottom', to: { part: 'frame', face: 'top' } })
583
583
  assert.deepEqual(visibilityControlsOf(asset), [], '조작값이 없으면 고를 것도 없다')
584
584
  asset = moving(asset, 'door', { kind: 'slide', axis: 'x', id: 'gate-open', min: 0, max: 900 })
585
585
  assert.deepEqual(visibilityControlsOf(asset).map(c => [c.id, c.min, c.max, c.unit]), [['gate-open', 0, 900, 'mm']])
586
586
  assert.equal(shownWhenOf(asset, 'lamp'), null, '늘 보인다')
587
- const shown = editV3Part(asset, { kind: 'set-visibility', target: 'lamp', shownWhen: { input: 'gate-open', atLeast: 9 } })
587
+ const shown = editFigurePart(asset, { kind: 'set-visibility', target: 'lamp', shownWhen: { input: 'gate-open', atLeast: 9 } })
588
588
  assert.deepEqual(shownWhenOf(shown, 'lamp'), { input: 'gate-open', atLeast: 9 })
589
- assert.equal(shownWhenOf(editV3Part(shown, { kind: 'set-visibility', target: 'lamp', shownWhen: null }), 'lamp'), null)
590
- const form = read('../client/modeller/figure-v3-links.ts')
589
+ assert.equal(shownWhenOf(editFigurePart(shown, { kind: 'set-visibility', target: 'lamp', shownWhen: null }), 'lamp'), null)
590
+ const form = read('../client/modeller/figure-links.ts')
591
591
  const section = form.slice(form.indexOf(' private renderShown('), form.indexOf(' private renderMotion('))
592
592
  assert.ok(section.includes("kind: 'set-visibility'"), '폼은 그 명령을 낸다')
593
593
  assert.ok(form.includes('${this.renderShown(asset)}'), '움직임 칸에')
@@ -595,10 +595,10 @@ test('보이는 때 — 명령이 남긴 것을 읽고, 폼은 그 명령을 낸
595
595
 
596
596
  /* 문틀은 폭 · 높이 · 깊이로 — 상자처럼 크기를 따르고, 손잡이로 키운다 (figure-model portal-profile@2, 09-26). */
597
597
  test('팔레트의 문틀은 문 크기로 서고, 세 축 모두 크기를 따를 수 있고, 손잡이로 키울 수 있다', async () => {
598
- const { paletteRecipe } = await import('../client/modeller/v3-palette.ts')
599
- const { gizmoReachOf } = await import('../client/modeller/v3-gizmo.ts')
600
- const { addV3Primitive: add } = await import('@hatiolab/figure-model')
601
- const asset = add(createV3Asset('Frame'), paletteRecipe('portal', 'frame'))
598
+ const { paletteRecipe } = await import('../client/modeller/palette.ts')
599
+ const { gizmoReachOf } = await import('../client/modeller/gizmo.ts')
600
+ const { addFigurePrimitive: add } = await import('@hatiolab/figure-model')
601
+ const asset = add(createFigureAsset('Frame'), paletteRecipe('portal', 'frame'))
602
602
  assert.deepEqual(followableAxes(asset, 'frame'), ['x', 'y', 'z'])
603
603
  const reach = gizmoReachOf(asset as never, 'frame')
604
604
  assert.equal(reach.scale, true)
@@ -608,11 +608,11 @@ test('팔레트의 문틀은 문 크기로 서고, 세 축 모두 크기를 따
608
608
 
609
609
  /* 크기를 따라감 한 번 — 치수 칸의 단추 하나, 크기를 대신 정했으면 한 줄로 말한다 (ADR 판정 Q1, 09-26). */
610
610
  test('치수 칸 — 「도형 크기를 따라감」 단추는 follow-size 한 번, 다 따르면 사라진다; 크기를 대신 정하면 한 줄', () => {
611
- const form = read('../client/modeller/figure-v3-links.ts')
611
+ const form = read('../client/modeller/figure-links.ts')
612
612
  const dims = form.slice(form.indexOf(' private renderDimensions('), form.indexOf(' private renderDimensions(') + 1200)
613
613
  assert.ok(dims.includes("this.command({ kind: 'follow-size', part: this.partId })"))
614
614
  assert.ok(dims.includes("followOf(asset, this.partId, axis).of === 'instance'"), '다 따르면 단추가 없다')
615
- const editor = read('../client/modeller/v3-editor.ts')
615
+ const editor = read('../client/modeller/editor.ts')
616
616
  assert.ok(editor.includes("if (action.kind === 'follow-size' && !hadSize)"), '크기를 대신 정했을 때만')
617
617
  assert.ok(editor.includes('<div role="status" class="notice">'))
618
618
  const ko = JSON.parse(read('../translations/ko.json'))
@@ -621,10 +621,10 @@ test('치수 칸 — 「도형 크기를 따라감」 단추는 follow-size 한
621
621
 
622
622
  /* 통 — 속 빈 상자를 팔레트에서, 세 축 모두 크기를 따르고, 손잡이로 키우고, 안쪽 면에 붙인다 (G4, 09-26). */
623
623
  test('팔레트의 통은 통의 크기로 서고, 세 축 모두 크기를 따를 수 있고, 손잡이로 키울 수 있다', async () => {
624
- const { paletteRecipe } = await import('../client/modeller/v3-palette.ts')
625
- const { gizmoReachOf } = await import('../client/modeller/v3-gizmo.ts')
626
- const { addV3Primitive: add } = await import('@hatiolab/figure-model')
627
- const asset = add(createV3Asset('Tote'), paletteRecipe('hollow-box' as never, 'tote'))
624
+ const { paletteRecipe } = await import('../client/modeller/palette.ts')
625
+ const { gizmoReachOf } = await import('../client/modeller/gizmo.ts')
626
+ const { addFigurePrimitive: add } = await import('@hatiolab/figure-model')
627
+ const asset = add(createFigureAsset('Tote'), paletteRecipe('hollow-box' as never, 'tote'))
628
628
  assert.deepEqual(followableAxes(asset, 'tote'), ['x', 'y', 'z'])
629
629
  assert.deepEqual(gizmoReachOf(asset as never, 'tote').sizes, ['tote.dimension.width', 'tote.dimension.height', 'tote.dimension.depth'])
630
630
  const ko = JSON.parse(read('../translations/ko.json'))
@@ -635,13 +635,13 @@ test('팔레트의 통은 통의 크기로 서고, 세 축 모두 크기를 따
635
635
  * The fastening form opens on the part's own fastening (walk 1, 2026-09-26): a roller fastened by its centre was shown
636
636
  * as fastened by its bottom, and fastening again from there broke its turning in place.
637
637
  */
638
- import { fasteningOf } from '../client/modeller/v3-links.ts'
638
+ import { fasteningOf } from '../client/modeller/links.ts'
639
639
 
640
640
  test('붙이기 칸은 부품 제 붙임으로 열린다 — 가운데로 붙은 롤러는 가운데 · 상대 · 간격 그대로', () => {
641
- let asset = addV3Box(createV3Asset('Rollers'), { id: 'bed', size: { x: 2000, y: 400, z: 600 }, position: { x: 0, y: 200, z: 0 } })
642
- asset = applyV3Authoring(asset, { kind: 'attach', part: 'bed', face: 'bottom', to: { plane: 'mounting-plane' } })
643
- 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 } })
644
- asset = applyV3Authoring(asset, { kind: 'attach', part: 'roller', face: 'centre', to: { part: 'bed', face: 'top' }, gap: 40, align: { x: 'min' } })
641
+ let asset = addFigureBox(createFigureAsset('Rollers'), { id: 'bed', size: { x: 2000, y: 400, z: 600 }, position: { x: 0, y: 200, z: 0 } })
642
+ asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'bed', face: 'bottom', to: { plane: 'mounting-plane' } })
643
+ 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 } })
644
+ asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'roller', face: 'centre', to: { part: 'bed', face: 'top' }, gap: 40, align: { x: 'min' } })
645
645
  const roller = fasteningOf(asset, 'roller')!
646
646
  assert.equal(roller.face, 'centre')
647
647
  assert.equal(roller.target, 'bed')
@@ -650,8 +650,8 @@ test('붙이기 칸은 부품 제 붙임으로 열린다 — 가운데로 붙은
650
650
  assert.equal(roller.align.x, 'min', '기록의 정렬')
651
651
  const bed = fasteningOf(asset, 'bed')!
652
652
  assert.deepEqual([bed.face, bed.toPlane], ['bottom', true])
653
- assert.equal(fasteningOf(addV3Box(createV3Asset('Loose'), { id: 'b', size: { x: 1, y: 1, z: 1 }, position: { x: 0, y: 0, z: 0 } }), 'b'), null, '안 붙은 부품은 기본값으로')
654
- const panel = readFileSync(fileURLToPath(new URL('../client/modeller/figure-v3-links.ts', import.meta.url)), 'utf8')
653
+ assert.equal(fasteningOf(addFigureBox(createFigureAsset('Loose'), { id: 'b', size: { x: 1, y: 1, z: 1 }, position: { x: 0, y: 0, z: 0 } }), 'b'), null, '안 붙은 부품은 기본값으로')
654
+ const panel = readFileSync(fileURLToPath(new URL('../client/modeller/figure-links.ts', import.meta.url)), 'utf8')
655
655
  assert.ok(panel.includes('fasteningOf(this.asset, this.partId)'), '부품을 고르면 제 붙임으로 연다')
656
656
  assert.ok(panel.includes("...(f.toPlane ? [] : (['centre'] as const))"), '내 면에 가운데가 있다')
657
657
  })