@things-factory/figure-ui 10.1.65 → 10.1.67

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 (176) hide show
  1. package/client/graphql/index.ts +0 -45
  2. package/client/modeller/figure-history-panel.ts +6 -145
  3. package/client/modeller/figure-v3-capability.ts +2 -2
  4. package/client/modeller/figure-v3-clips.ts +2 -2
  5. package/client/modeller/figure-v3-controls.ts +2 -2
  6. package/client/modeller/figure-v3-first-save.ts +1 -1
  7. package/client/modeller/figure-v3-links.ts +6 -6
  8. package/client/modeller/figure-v3-material.ts +11 -11
  9. package/client/modeller/figure-v3-parts.ts +7 -10
  10. package/client/modeller/figure-v3-preview.ts +3 -3
  11. package/client/modeller/figure-v3-properties.ts +5 -6
  12. package/client/modeller/figure-v3-verdict.ts +9 -10
  13. package/client/modeller/figure-view.ts +2 -3
  14. package/client/modeller/modeller-route.ts +2 -3
  15. package/client/modeller/scene-budget.ts +0 -14
  16. package/client/modeller/side-panel-styles.ts +5 -5
  17. package/client/modeller/v3-align.ts +16 -6
  18. package/client/modeller/v3-canvas.ts +12 -12
  19. package/client/modeller/v3-clips.ts +1 -1
  20. package/client/modeller/v3-controls.ts +1 -1
  21. package/client/modeller/v3-editor.ts +21 -21
  22. package/client/modeller/v3-fields.ts +1 -1
  23. package/client/modeller/v3-figure-facts.ts +1 -1
  24. package/client/modeller/v3-first-save.ts +3 -4
  25. package/client/modeller/v3-gizmo.ts +5 -5
  26. package/client/modeller/v3-help.ts +1 -1
  27. package/client/modeller/v3-joint-fan.ts +4 -5
  28. package/client/modeller/v3-keys.ts +1 -1
  29. package/client/modeller/v3-links.ts +9 -12
  30. package/client/modeller/v3-maturity.ts +22 -26
  31. package/client/modeller/v3-refusals.ts +17 -0
  32. package/client/modeller/v3-renderer.ts +1 -1
  33. package/client/modeller/v3-stage-model.ts +2 -2
  34. package/client/modeller/v3-thumbnail.ts +1 -3
  35. package/client/pages/figure-v3-modeller-page.ts +8 -11
  36. package/client/types.ts +2 -31
  37. package/dist-client/graphql/index.js +0 -45
  38. package/dist-client/graphql/index.js.map +1 -1
  39. package/dist-client/modeller/figure-history-panel.d.ts +4 -37
  40. package/dist-client/modeller/figure-history-panel.js +8 -154
  41. package/dist-client/modeller/figure-history-panel.js.map +1 -1
  42. package/dist-client/modeller/figure-v3-capability.d.ts +1 -1
  43. package/dist-client/modeller/figure-v3-capability.js +1 -1
  44. package/dist-client/modeller/figure-v3-capability.js.map +1 -1
  45. package/dist-client/modeller/figure-v3-clips.d.ts +2 -2
  46. package/dist-client/modeller/figure-v3-clips.js +2 -2
  47. package/dist-client/modeller/figure-v3-clips.js.map +1 -1
  48. package/dist-client/modeller/figure-v3-controls.d.ts +1 -1
  49. package/dist-client/modeller/figure-v3-controls.js +2 -2
  50. package/dist-client/modeller/figure-v3-controls.js.map +1 -1
  51. package/dist-client/modeller/figure-v3-first-save.d.ts +1 -1
  52. package/dist-client/modeller/figure-v3-first-save.js +1 -1
  53. package/dist-client/modeller/figure-v3-first-save.js.map +1 -1
  54. package/dist-client/modeller/figure-v3-links.d.ts +5 -5
  55. package/dist-client/modeller/figure-v3-links.js +6 -6
  56. package/dist-client/modeller/figure-v3-links.js.map +1 -1
  57. package/dist-client/modeller/figure-v3-material.d.ts +6 -6
  58. package/dist-client/modeller/figure-v3-material.js +11 -11
  59. package/dist-client/modeller/figure-v3-material.js.map +1 -1
  60. package/dist-client/modeller/figure-v3-parts.d.ts +6 -8
  61. package/dist-client/modeller/figure-v3-parts.js +1 -1
  62. package/dist-client/modeller/figure-v3-parts.js.map +1 -1
  63. package/dist-client/modeller/figure-v3-preview.js.map +1 -1
  64. package/dist-client/modeller/figure-v3-properties.d.ts +4 -5
  65. package/dist-client/modeller/figure-v3-properties.js +5 -6
  66. package/dist-client/modeller/figure-v3-properties.js.map +1 -1
  67. package/dist-client/modeller/figure-v3-verdict.d.ts +2 -2
  68. package/dist-client/modeller/figure-v3-verdict.js +9 -10
  69. package/dist-client/modeller/figure-v3-verdict.js.map +1 -1
  70. package/dist-client/modeller/figure-view.d.ts +2 -3
  71. package/dist-client/modeller/figure-view.js.map +1 -1
  72. package/dist-client/modeller/modeller-route.d.ts +2 -3
  73. package/dist-client/modeller/modeller-route.js +2 -3
  74. package/dist-client/modeller/modeller-route.js.map +1 -1
  75. package/dist-client/modeller/scene-budget.d.ts +0 -11
  76. package/dist-client/modeller/scene-budget.js +0 -12
  77. package/dist-client/modeller/scene-budget.js.map +1 -1
  78. package/dist-client/modeller/side-panel-styles.d.ts +2 -2
  79. package/dist-client/modeller/side-panel-styles.js +5 -5
  80. package/dist-client/modeller/side-panel-styles.js.map +1 -1
  81. package/dist-client/modeller/v3-align.d.ts +9 -5
  82. package/dist-client/modeller/v3-align.js +13 -3
  83. package/dist-client/modeller/v3-align.js.map +1 -1
  84. package/dist-client/modeller/v3-canvas.d.ts +4 -4
  85. package/dist-client/modeller/v3-canvas.js +9 -9
  86. package/dist-client/modeller/v3-canvas.js.map +1 -1
  87. package/dist-client/modeller/v3-clips.js.map +1 -1
  88. package/dist-client/modeller/v3-controls.js.map +1 -1
  89. package/dist-client/modeller/v3-editor.d.ts +13 -13
  90. package/dist-client/modeller/v3-editor.js +19 -18
  91. package/dist-client/modeller/v3-editor.js.map +1 -1
  92. package/dist-client/modeller/v3-fields.js.map +1 -1
  93. package/dist-client/modeller/v3-figure-facts.d.ts +1 -1
  94. package/dist-client/modeller/v3-figure-facts.js +1 -1
  95. package/dist-client/modeller/v3-figure-facts.js.map +1 -1
  96. package/dist-client/modeller/v3-first-save.js +3 -4
  97. package/dist-client/modeller/v3-first-save.js.map +1 -1
  98. package/dist-client/modeller/v3-gizmo.d.ts +5 -5
  99. package/dist-client/modeller/v3-gizmo.js +1 -1
  100. package/dist-client/modeller/v3-gizmo.js.map +1 -1
  101. package/dist-client/modeller/v3-help.d.ts +1 -1
  102. package/dist-client/modeller/v3-help.js +1 -1
  103. package/dist-client/modeller/v3-help.js.map +1 -1
  104. package/dist-client/modeller/v3-joint-fan.js +4 -5
  105. package/dist-client/modeller/v3-joint-fan.js.map +1 -1
  106. package/dist-client/modeller/v3-keys.js.map +1 -1
  107. package/dist-client/modeller/v3-links.d.ts +5 -7
  108. package/dist-client/modeller/v3-links.js +6 -8
  109. package/dist-client/modeller/v3-links.js.map +1 -1
  110. package/dist-client/modeller/v3-maturity.d.ts +7 -7
  111. package/dist-client/modeller/v3-maturity.js +20 -24
  112. package/dist-client/modeller/v3-maturity.js.map +1 -1
  113. package/dist-client/modeller/v3-refusals.d.ts +6 -0
  114. package/dist-client/modeller/v3-refusals.js +18 -0
  115. package/dist-client/modeller/v3-refusals.js.map +1 -1
  116. package/dist-client/modeller/v3-renderer.js +1 -1
  117. package/dist-client/modeller/v3-renderer.js.map +1 -1
  118. package/dist-client/modeller/v3-stage-model.js.map +1 -1
  119. package/dist-client/modeller/v3-thumbnail.js +1 -3
  120. package/dist-client/modeller/v3-thumbnail.js.map +1 -1
  121. package/dist-client/pages/figure-v3-modeller-page.d.ts +4 -5
  122. package/dist-client/pages/figure-v3-modeller-page.js +8 -11
  123. package/dist-client/pages/figure-v3-modeller-page.js.map +1 -1
  124. package/dist-client/route.d.ts +1 -1
  125. package/dist-client/tsconfig.fui.tsbuildinfo +1 -1
  126. package/dist-client/tsconfig.tsbuildinfo +1 -1
  127. package/dist-client/types.d.ts +2 -29
  128. package/dist-client/types.js +3 -0
  129. package/dist-client/types.js.map +1 -1
  130. package/docs/v3-product-cutover.md +14 -24
  131. package/package.json +5 -5
  132. package/test/browser/modeller-page.js +6 -7
  133. package/test/browser/modeller.check.mjs +9 -16
  134. package/test/fixtures/joint-column.json +1138 -0
  135. package/test/fixtures/joint-roller.json +872 -0
  136. package/test/fixtures/robot-arm-6dof.json +2776 -0
  137. package/test/i18n-prefix-guard.test.ts +0 -31
  138. package/test/modeller-route.test.ts +2 -3
  139. package/test/v3-align.test.ts +9 -4
  140. package/test/v3-clips.test.ts +1 -1
  141. package/test/v3-controls.test.ts +1 -1
  142. package/test/v3-fields.test.ts +4 -4
  143. package/test/v3-first-save.test.ts +1 -1
  144. package/test/v3-gizmo.test.ts +5 -5
  145. package/test/v3-joint-fan.test.ts +12 -42
  146. package/test/v3-keys.test.ts +1 -1
  147. package/test/v3-layout.test.ts +4 -5
  148. package/test/v3-links.test.ts +3 -3
  149. package/test/v3-material.test.ts +3 -3
  150. package/test/v3-maturity.test.ts +8 -9
  151. package/test/v3-parts-wiring.test.ts +3 -3
  152. package/test/v3-preview.test.ts +1 -1
  153. package/test/v3-product-write.test.ts +1 -1
  154. package/test/v3-properties.test.ts +3 -3
  155. package/test/v3-refusals.test.ts +12 -0
  156. package/test/v3-release-panel.test.ts +5 -5
  157. package/test/v3-scene-canvas.test.ts +2 -2
  158. package/test/v3-theme.test.ts +2 -2
  159. package/test/v3-verdict.test.ts +2 -2
  160. package/test/v3-view.test.ts +2 -2
  161. package/translations/en.json +2 -143
  162. package/translations/ja.json +2 -143
  163. package/translations/ko.json +2 -143
  164. package/translations/ms.json +2 -143
  165. package/translations/zh.json +2 -143
  166. package/client/modeller/figure-source.ts +0 -437
  167. package/client/modeller/part-edits.ts +0 -312
  168. package/dist-client/modeller/figure-source.d.ts +0 -238
  169. package/dist-client/modeller/figure-source.js +0 -193
  170. package/dist-client/modeller/figure-source.js.map +0 -1
  171. package/dist-client/modeller/part-edits.d.ts +0 -89
  172. package/dist-client/modeller/part-edits.js +0 -270
  173. package/dist-client/modeller/part-edits.js.map +0 -1
  174. package/test/expect.ts +0 -57
  175. package/test/figure-source.test.ts +0 -511
  176. package/test/part-edits.test.ts +0 -350
@@ -28,8 +28,6 @@ import { readFileSync, readdirSync } from 'node:fs'
28
28
  import { fileURLToPath } from 'node:url'
29
29
  import { join } from 'node:path'
30
30
 
31
- import { CHANNEL_PATHS, INTERPOLATIONS } from '@hatiolab/figure-model'
32
-
33
31
  const HERE = fileURLToPath(new URL('.', import.meta.url))
34
32
  const TRANSLATIONS = join(HERE, '../translations')
35
33
  const CLIENT = join(HERE, '../client')
@@ -122,35 +120,6 @@ test('★ 한국어를 defaultValue 로 들고 있지 않다 — 번역 파일
122
120
  assert.deepEqual(hard, [], `한국어 defaultValue:\n ${hard.join('\n ')}`)
123
121
  })
124
122
 
125
- /*
126
- 애니메이션 탭도 열거 값을 키에 이어 붙인다. 사다리와 **같은 모양의 함정**이라 같은 못을 박는다.
127
-
128
- 경로는 이름(`label`)과 단위 설명(`text.…-unit`)으로 갈린다. 한쪽만 넣으면 화면에 키가
129
- 그대로 뜨고, 넓은 앞머리가 위의 미사용 검사를 덮어 통과시킨다.
130
-
131
- There is no drive enumeration to check. Whether a pose runs on time or on a value is which list it is
132
- written in: animations run on time, parameters on a value (ADR-0051). The old `drive` field is gone.
133
-
134
- 열거 값을 여기 다시 적지 않고 형식에서 읽는다. 적어 두면 형식에 값을 하나 더할 때
135
- 검사가 조용히 통과한다.
136
- */
137
- test('★ 열거 값마다 짝이 다 있다 — 경로 · 보간', () => {
138
- const missing: string[] = []
139
- const want = (key: string) => {
140
- if (!(key in KO)) missing.push(key)
141
- }
142
-
143
- for (const path of CHANNEL_PATHS) {
144
- want(`figure.label.path-${path}`)
145
- want(`figure.text.path-${path}-unit`)
146
- }
147
- for (const one of INTERPOLATIONS) {
148
- want(`figure.label.interpolation-${one}`)
149
- }
150
-
151
- assert.deepEqual(missing, [], `이어 붙여 부르는 키가 번역에 없다:\n ${missing.join('\n ')}`)
152
- })
153
-
154
123
  /*
155
124
  대체 언어(`fallbackLng`)는 **en** 이다 — 클라이언트(`@operato/i18n` 의 config)와 서버
156
125
  (`i18n-base` 의 미들웨어) 양쪽 다.
@@ -1,6 +1,5 @@
1
1
  /*
2
- * One modeller since ADR-0087 step 5 (2026-09-24, user decision: V2 is not kept). The catalogue, a new figure and
3
- * an old address all open it.
2
+ * One modeller. The catalogue, a new figure and an old address all open it.
4
3
  */
5
4
  import { test } from 'node:test'
6
5
  import assert from 'node:assert/strict'
@@ -16,7 +15,7 @@ test('every figure and a new one open the one modeller', () => {
16
15
  assert.equal(modellerPathOf(), 'figure-modeller')
17
16
  })
18
17
 
19
- test('the route and the shell table send both addresses to the V3 page, and the V2 page is gone', () => {
18
+ test('the route and the shell table send both addresses to the V3 page, and no `figure-modeller-page` is registered', () => {
20
19
  const route = read('../client/route.ts')
21
20
  assert.doesNotMatch(route, /figure-modeller-page/)
22
21
  const config = read('../things-factory.config.js')
@@ -1,7 +1,7 @@
1
1
  /*
2
- * **V3 부품 맞추기 — 가운데 · 바닥 · 대칭 · 눈금** (V3 동등표 ③, 2026-09-24).
2
+ * **부품 맞추기 — 가운데 · 바닥 · 대칭 · 눈금** (2026-09-24).
3
3
  *
4
- * V2 인스펙터의 「맞추기」 여섯 단추. V3 의 틀은 바닥이 y 0, X · Z 가 0 에 가운데다. 각 도구가 부품의 **실제로 차지하는
4
+ * 「맞추기」 여섯 단추. V3 의 틀은 바닥이 y 0, X · Z 가 0 에 가운데다. 각 도구가 부품의 **실제로 차지하는
5
5
  * 상자**(돌린 부품이면 돈 부피)를 기준으로 맞추는지, 그 결과를 잰다.
6
6
  */
7
7
  import { test } from 'node:test'
@@ -11,7 +11,7 @@ import { fileURLToPath } from 'node:url'
11
11
 
12
12
  import { addV3Box, createV3Asset, editV3Part } from '@hatiolab/figure-model'
13
13
 
14
- import { alignChanges, boxOf, canMirror, gridStepOf, mirrorChanges } from '../client/modeller/v3-align.ts'
14
+ import { alignChanges, boxOf, canMirror, gridStepOf, mirrorChanges, niceStep } from '../client/modeller/v3-align.ts'
15
15
  import { gizmoReachOf } from '../client/modeller/v3-gizmo.ts'
16
16
 
17
17
  const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
@@ -41,7 +41,7 @@ test('바닥은 부품의 가장 낮은 점을 y 0 에 둔다 — 돌린 부품
41
41
  assert.ok(Math.abs(landed.centre.y - landed.extent.y / 2) <= 1, `45° 돈 부품의 모서리가 바닥에 닿는다 (${landed.centre.y - landed.extent.y / 2})`)
42
42
  })
43
43
 
44
- test('눈금은 V2 와 같은 1 · 2 · 5 간격으로 자리와 크기를 맞추고, 크기를 0 으로 만들지 않는다', () => {
44
+ test('눈금은 1 · 2 · 5 간격으로 자리와 크기를 맞추고, 크기를 0 으로 만들지 않는다', () => {
45
45
  const asset = probe({ x: 133, y: 47, z: -71 })
46
46
  const step = gridStepOf(asset)
47
47
  assert.equal(step, 50, '가장 긴 폭 200 을 다섯 칸으로 — 40 이 아니라 50')
@@ -62,3 +62,8 @@ test('붙인 부품처럼 옮길 수 없는 것은 맞추기도 없다', () => {
62
62
  assert.deepEqual(alignChanges(probe(), 'body', { translate: false, rotate: false, scale: false }, 'floor'), [])
63
63
  assert.match(read('../client/modeller/v3-editor.ts'), /if \(!this\.selected \|\| !reach\.translate\) return null/)
64
64
  })
65
+
66
+ test('the grid step reads well -- 27 or 33 would make coordinates unreadable -- and is never 0', () => {
67
+ assert.deepEqual([3000, 1000, 137, 4].map(niceStep), [500, 200, 20, 1])
68
+ assert.ok(niceStep(0) > 0 && niceStep(-100) > 0)
69
+ })
@@ -1,5 +1,5 @@
1
1
  /*
2
- * **클립 — V2 의 애니메이션** (V3 동등표 ⑥, 2026-09-24).
2
+ * **클립 — 애니메이션** (2026-09-24).
3
3
  *
4
4
  * 조작값을 시간에 따라 움직이는 자리다. 읽는 값은 figure-model 편집을 실제로 적용한 자산에서 재고, 배선은 소스에서 본다.
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*
2
- * **조작값 — V2 의 파라미터와 자세 도구** (V3 동등표 ⑥, 2026-09-24).
2
+ * **조작값과 자세 도구** (2026-09-24).
3
3
  *
4
4
  * 보드가 값을 바꿔 도형을 움직이는 자리다. 화면이 읽는 값을 명령을 실제로 적용한 자산에서 재고, 화면의 배선은
5
5
  * 소스에서 확인한다.
@@ -1,5 +1,5 @@
1
1
  /*
2
- * **값 칸의 묶음 — V2 인스펙터처럼** (사용자 2026-09-24: 「적절한 그루핑이 없어서 가시성이 좋지 않다. 크기는 왜
2
+ * **값 칸의 묶음** (사용자 2026-09-24: 「적절한 그루핑이 없어서 가시성이 좋지 않다. 크기는 왜
3
3
  * 한줄씩 차지하는가」). 위치 · 회전 · 크기는 X · Y · Z 세 칸 한 줄, 형상의 나머지 치수는 제 제목 아래 한 줄씩.
4
4
  */
5
5
  import { test } from 'node:test'
@@ -38,7 +38,7 @@ test('오른쪽 칸은 한 벌의 규격 — 요소마다 제 제목 · 칸 ·
38
38
  }
39
39
  })
40
40
 
41
- test('긴 설명은 제목 옆 「?」 로 연다 — 늘 펼쳐 두지 않는다 (V2)', () => {
41
+ test('긴 설명은 제목 옆 「?」 로 연다 — 늘 펼쳐 두지 않는다', () => {
42
42
  for (const [file, key] of [
43
43
  ['figure-v3-links', 'figure.text.motion-explained'],
44
44
  ['figure-v3-links', 'figure.text.place-explained'],
@@ -81,7 +81,7 @@ test('편집기는 저작한 부품 단위로 고르고 보인다 — 되풀이
81
81
  assert.ok(editor.includes('.parts=${this.partRows(view.geometry)}'), '목록은 저작한 부품 한 줄씩')
82
82
  assert.ok(editor.includes("this.session!.command({ kind: 'set-repeat', part: this.selected, pitch })"), 'pitch 는 명령으로 정한다')
83
83
  const parts = read('../client/modeller/figure-v3-parts.ts')
84
- assert.ok(parts.includes('?disabled=${this.locked || full}'), '등급 한도에 이르면 팔레트가 잠긴다(V2)')
84
+ assert.ok(parts.includes('?disabled=${this.locked || full}'), '등급 한도에 이르면 팔레트가 잠긴다')
85
85
  })
86
86
 
87
87
  test('pitch 는 한 곳에서 하나의 값이고, 늘 set-repeat 로 바꿔 최대 길이가 따라온다', () => {
@@ -98,7 +98,7 @@ test('pitch 는 한 곳에서 하나의 값이고, 늘 set-repeat 로 바꿔 최
98
98
  }
99
99
  })
100
100
 
101
- test('기즈모로 끈 변환 부품은 규칙을 지킨 채 옮겨지고, 관절·붙이기가 정하는 부품은 까닭을 말한다 — 튕겨 돌아가지 않는다', () => {
101
+ test('기즈모로 끈, 규칙이 자리를 정하는 부품은 규칙을 지킨 채 옮겨지고, 관절·붙이기가 정하는 부품은 까닭을 말한다 — 튕겨 돌아가지 않는다', () => {
102
102
  const editor = readFileSync(fileURLToPath(new URL('../client/modeller/v3-editor.ts', import.meta.url)), 'utf8')
103
103
  const moved = editor.slice(editor.indexOf(' private transformed('), editor.indexOf(' /** 맞추기'))
104
104
  assert.ok(moved.includes("kind: 'nudge-part',"), '사용자 2026-09-24: 「기즈모 컨트롤도 전혀 작동이 안되는군」')
@@ -1,5 +1,5 @@
1
1
  /*
2
- * **처음 저장은 타입과 이름을 묻는다** — V2 의 저장 창. V3 로 옮기며 빠져서 새 도형이 기계 이름으로 저장됐다(2026-09-25).
2
+ * **처음 저장은 타입과 이름을 묻는다** — 묻지 않을 때 새 도형이 기계 이름으로 저장됐다(2026-09-25).
3
3
  */
4
4
  import { test } from 'node:test'
5
5
  import assert from 'node:assert/strict'
@@ -1,8 +1,8 @@
1
1
  /*
2
- * **V3 부품을 끌어서 옮기고 · 돌리고 · 키운다** (V3 동등표 ③, 2026-09-24).
2
+ * **부품을 끌어서 옮기고 · 돌리고 · 키운다** (2026-09-24).
3
3
  *
4
- * V2 는 부품을 고르면 기즈모가 붙고 W · E · R 로 옮기기 · 돌리기 · 크기가 갈린다. 끈 결과는 숫자 칸으로 되쓰인다.
5
- * V3 도 같은 일을 한다 — 되쓸 숫자가 부품 자신의 것일 때만. 붙인 부품처럼 되쓸 수 없는 것은 기즈모가 내지 않는다.
4
+ * 부품을 고르면 기즈모가 붙고 W · E · R 로 옮기기 · 돌리기 · 크기가 갈린다. 끈 결과는 숫자 칸으로 되쓰인다 —
5
+ * 되쓸 숫자가 부품 자신의 것일 때만. 붙인 부품처럼 되쓸 수 없는 것은 기즈모가 내지 않는다.
6
6
  */
7
7
  import { test } from 'node:test'
8
8
  import assert from 'node:assert/strict'
@@ -87,7 +87,7 @@ test('끈 것은 세션의 한 걸음이다 — 한 번의 되돌리기로 돌
87
87
  assert.ok(moved.includes("kind: 'nudge-part',"), 'a nudge is one command, one step')
88
88
  })
89
89
 
90
- test('캔버스 — 기즈모 · W E R · 스냅은 씬의 편집 모드가 한다, V2 처럼', () => {
90
+ test('캔버스 — 기즈모 · W E R · 스냅은 씬의 편집 모드가 한다', () => {
91
91
  const canvas = read('../client/modeller/v3-canvas.ts')
92
92
  assert.match(canvas, /mode: 1\n/)
93
93
  assert.doesNotMatch(canvas, /TransformControls|OrbitControls/)
@@ -119,7 +119,7 @@ test('끄는 동안은 모아 두고 놓을 때 한 번 적는다 — 크기 기
119
119
  })
120
120
 
121
121
 
122
- test('원기둥 · 구도 크기 기즈모가 먹는다 — 단면은 원으로(V2 의 keepRound)', async () => {
122
+ test('원기둥 · 구도 크기 기즈모가 먹는다 — 단면은 원으로', async () => {
123
123
  const { addV3Primitive, createV3Asset } = await import('@hatiolab/figure-model')
124
124
  const { paletteRecipe } = await import('../client/modeller/v3-palette.ts')
125
125
  const { gizmoChanges, gizmoReachOf } = await import('../client/modeller/v3-gizmo.ts')
@@ -1,6 +1,6 @@
1
1
  /*
2
2
  * **고른 부품이 어디서 얼마나 도는지** (사용자 2026-09-24: 「로봇암은 편집기에서 원래는 부품을 선택했을 때, 가변 각도가
3
- * 표현이 되었었는데」). V2 는 관절의 가동 범위를 부채꼴로 저작면에 그렸다. 부채꼴의 두 끝이 제어값의 최솟값과 최댓값에서
3
+ * 표현이 되었었는데」). 관절의 가동 범위를 부채꼴로 저작면에 그린다. 부채꼴의 두 끝이 제어값의 최솟값과 최댓값에서
4
4
  * 부품이 실제로 가는 쪽을 가리키는지를 — 평가해서 — 본다.
5
5
  */
6
6
  import { test } from 'node:test'
@@ -8,7 +8,7 @@ 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, compileV3Asset, convertV2ToV3, createV3Asset } from '@hatiolab/figure-model'
11
+ import { addV3Box, applyV3Authoring, compileV3Asset, createV3Asset } from '@hatiolab/figure-model'
12
12
  import type { V3Asset } from '@hatiolab/figure-model'
13
13
 
14
14
  import { JOINT_FAN, jointFanOf } from '../client/modeller/v3-joint-fan.ts'
@@ -87,31 +87,16 @@ test('the editor draws it for the selected part, with the pose in hand; the canv
87
87
  assert.ok(canvas.includes('{ ...fan, capturable: false, locked: true }'))
88
88
  })
89
89
 
90
- /* ── Joints the converter wrote (user 2026-09-24: 「로봇암 axis와 관절의 각도 표현을 다시 검증해보라」) ───────── */
90
+ /* ── Joints on a chain (user 2026-09-24: 「로봇암 axis와 관절의 각도 표현을 다시 검증해보라」) ───────── */
91
91
 
92
- /** A V2 column: a link on a hinge at the root, a tip on a wrist inside it -- a first joint and a nested one. */
92
+ /**
93
+ * A column: a link on a hinge at the root, a tip on a wrist inside it -- a first joint and a nested one. `housing` rides
94
+ * the hinge centred on the wrist's axis (as ROBOT_ARM's elbow-housing is the elbow's); `badge` rides the hinge and holds
95
+ * no pivot.
96
+ */
97
+ const fixture = (name: string): V3Asset => JSON.parse(readFileSync(fileURLToPath(new URL(`./fixtures/${name}.json`, import.meta.url)), 'utf8'))
93
98
  function column(): V3Asset {
94
- const source = {
95
- version: 2,
96
- type: 'COLUMN',
97
- base: { x: 200, y: 700, z: 200 },
98
- parts: [
99
- { name: 'base', primitive: 'cube', transform: { position: { x: 0, y: 50, z: 0 }, size: { x: 200, y: 100, z: 200 } }, material: { token: 'palette.dark' } },
100
- { name: 'link', primitive: 'cube', transform: { position: { x: 0, y: 300, z: 0 }, size: { x: 80, y: 400, z: 80 } }, material: { token: 'palette.primary' }, parent: 'base' },
101
- { name: 'tip', primitive: 'cube', transform: { position: { x: 0, y: 600, z: 0 }, size: { x: 60, y: 200, z: 60 } }, material: { token: 'palette.primary' }, parent: 'link' },
102
- /* Rides the hinge, centred on the wrist's axis -- the wrist's housing, as ROBOT_ARM's elbow-housing is the elbow's. */
103
- { name: 'housing', primitive: 'cube', transform: { position: { x: 0, y: 500, z: 0 }, size: { x: 100, y: 100, z: 100 } }, material: { token: 'palette.dark' }, parent: 'link' },
104
- /* Rides the hinge and holds no pivot. */
105
- { name: 'badge', primitive: 'cube', transform: { position: { x: 60, y: 300, z: 0 }, size: { x: 10, y: 40, z: 40 } }, material: { token: 'palette.dark' }, parent: 'link' }
106
- ],
107
- joints: [
108
- { name: 'hinge', child: 'link', type: 'revolute', origin: { x: 0, y: 100, z: 0 }, axis: { x: 1, y: 0, z: 0 }, limits: { min: -90, max: 60 } },
109
- { name: 'wrist', child: 'tip', type: 'revolute', origin: { x: 0, y: 500, z: 0 }, axis: { x: 0, y: 0, z: 1 }, limits: { min: -120, max: 120 } }
110
- ]
111
- }
112
- const result = convertV2ToV3(source, { palette: { 'palette.dark': '#333333', 'palette.primary': '#3366cc' } } as never)
113
- assert.equal(result.status, 'converted', JSON.stringify((result as { refusals?: unknown }).refusals))
114
- return (result as unknown as { asset: V3Asset }).asset
99
+ return fixture('joint-column')
115
100
  }
116
101
 
117
102
  const sub = (a: number[], b: number[]) => a.map((v, i) => v - b[i]!)
@@ -195,23 +180,8 @@ test('one joint, one fan: the housing and the part it turns show the same fan, s
195
180
 
196
181
  test('an axis running through a part does not make it that joint’s: a roll along a link shows on the roller, not the link', () => {
197
182
  /* ROBOT_ARM's forearm showed the wrist roll, whose axis runs its length (2026-09-24 audit). */
198
- const source = {
199
- version: 2,
200
- type: 'ROLLER',
201
- base: { x: 200, y: 800, z: 200 },
202
- parts: [
203
- { name: 'base', primitive: 'cube', transform: { position: { x: 0, y: 50, z: 0 }, size: { x: 200, y: 100, z: 200 } }, material: { token: 'palette.dark' } },
204
- { name: 'link', primitive: 'cube', transform: { position: { x: 0, y: 300, z: 0 }, size: { x: 80, y: 400, z: 80 } }, material: { token: 'palette.primary' }, parent: 'base' },
205
- { name: 'roller', primitive: 'cube', transform: { position: { x: 30, y: 600, z: 0 }, size: { x: 120, y: 200, z: 60 } }, material: { token: 'palette.primary' }, parent: 'link' }
206
- ],
207
- joints: [
208
- { name: 'hinge', child: 'link', type: 'revolute', origin: { x: 0, y: 100, z: 0 }, axis: { x: 1, y: 0, z: 0 }, limits: { min: -90, max: 60 } },
209
- { name: 'twist', child: 'roller', type: 'revolute', origin: { x: 0, y: 500, z: 0 }, axis: { x: 0, y: 1, z: 0 }, limits: { min: -170, max: 170 } }
210
- ]
211
- }
212
- const result = convertV2ToV3(source, { palette: { 'palette.dark': '#333333', 'palette.primary': '#3366cc' } } as never)
213
- assert.equal(result.status, 'converted')
214
- const asset = (result as unknown as { asset: V3Asset }).asset
183
+ /* A link on a hinge and a roller on a twist whose axis runs along the link. */
184
+ const asset = fixture('joint-roller')
215
185
  const turns = (part: string) => (jointFanOf(asset, part) as unknown as { turns?: string } | null)?.turns ?? null
216
186
  assert.equal(turns('link'), 'hinge', 'the twist axis runs through the link, and the link does not twist')
217
187
  assert.equal(turns('roller'), 'twist')
@@ -1,5 +1,5 @@
1
1
  /*
2
- * **모델러의 키** — V2 의 Ctrl+Z · Ctrl+Y 에 더해 복제 · 지우기 · 도형 전체로. 칸에 치는 키는 칸의 것이다.
2
+ * **모델러의 키** — Ctrl+Z · Ctrl+Y 와 복제 · 지우기 · 도형 전체로. 칸에 치는 키는 칸의 것이다.
3
3
  */
4
4
  import { test } from 'node:test'
5
5
  import assert from 'node:assert/strict'
@@ -1,8 +1,7 @@
1
1
  /*
2
- * **V3 모델러의 구성 — V2 의 세 칸과 오른쪽 탭, 그보다 낫게** (사용자 2026-09-24: 「v2 모델러의 구성과 레이아웃이
3
- * 좋았다. 그 보다도 더 좋아야지.」).
2
+ * **모델러의 구성 — 세 칸과 오른쪽 탭** (사용자 2026-09-24).
4
3
  *
5
- * V2 는 부품 목록 · 캔버스 · 오른쪽 칸(아이콘 탭 여섯)이었다. V3 는 부품 속성이 캔버스 아래 좁은 칸에 끼어 있었다.
4
+ * 부품 목록 · 캔버스 · 오른쪽 칸(아이콘 탭 여섯). 전에는 부품 속성이 캔버스 아래 좁은 칸에 끼어 있었다.
6
5
  * 이 시험은 소스에서 그 구성을 확인한다.
7
6
  */
8
7
  import { test } from 'node:test'
@@ -24,7 +23,7 @@ test('세 칸 — 부품 목록 · 캔버스 · 오른쪽 칸, 캔버스 아래
24
23
  assert.doesNotMatch(read('../client/pages/figure-v3-modeller-page.ts'), /<aside/)
25
24
  })
26
25
 
27
- test('오른쪽 칸의 탭 — V2 의 여섯: 속성 · 능력 · 움직임 · 판정 · 환경 · 이력, 판정의 개수는 탭에 남는다', () => {
26
+ test('오른쪽 칸의 탭 여섯: 속성 · 능력 · 움직임 · 판정 · 환경 · 이력, 판정의 개수는 탭에 남는다', () => {
28
27
  const tabs = [...renderBody.matchAll(/this\.renderTab\('([a-z]+)'/g)].map(m => m[1])
29
28
  assert.deepEqual(tabs, ['properties', 'capability', 'motion', 'verdict', 'view', 'history'])
30
29
  for (const tab of tabs) assert.ok(renderBody.includes(`?off=\${tab !== '${tab}'}`), `${tab} 는 감추되 떼지 않는다`)
@@ -66,7 +65,7 @@ test('띠 — 칸 접기 · 되돌리기 · 전체 보기 · 저장 상태 ·
66
65
  }
67
66
  })
68
67
 
69
- test('탭 아이콘은 오른쪽 칸 맨 위 — 타입 칸은 V2 처럼 속성 탭 안의 첫 칸', () => {
68
+ test('탭 아이콘은 오른쪽 칸 맨 위 — 타입 칸은 속성 탭 안의 첫 칸', () => {
70
69
  const aside = renderBody.slice(renderBody.indexOf('<aside slot="end">'), renderBody.indexOf('</aside>'))
71
70
  assert.match(aside, /^<aside slot="end">\s*<div tabs>/, '탭이 첫째')
72
71
  const properties = aside.slice(aside.indexOf("?off=${tab !== 'properties'}"), aside.indexOf("?off=${tab !== 'capability'}"))
@@ -1,5 +1,5 @@
1
1
  /*
2
- * **V3 부품이 무엇에 묶여 있나 — 크기 따르기 · 붙이기 · 움직임** (V3 동등표 ④ ⑤ ⑥, 2026-09-24).
2
+ * **V3 부품이 무엇에 묶여 있나 — 크기 따르기 · 붙이기 · 움직임** (2026-09-24).
3
3
  *
4
4
  * 세 칸이 보여 주는 것은 figure-model 명령이 남긴 그래프를 거꾸로 읽은 것이다. 명령을 실제로 적용한 뒤 읽어서,
5
5
  * 화면이 명령과 같은 말을 하는지 잰다.
@@ -157,7 +157,7 @@ test('받는 종류가 없으면 자리를 만들지 않는다 — 형식에 「
157
157
  assert.deepEqual(acceptsOf(' , ,'), [])
158
158
  })
159
159
 
160
- /* ── V2's proportional rules (user 2026-09-24: ROBOT_ARM at twice the size looked broken) ───────────── */
160
+ /* ── Proportional rules (user 2026-09-24: ROBOT_ARM at twice the size looked broken) ───────────── */
161
161
 
162
162
  const sizedPair = () => {
163
163
  let asset = createV3Asset('Probe')
@@ -175,7 +175,7 @@ test('비례 — 인스턴스 크기의 몫을 따르는 치수는 비례로 읽
175
175
  const share = applyV3Authoring(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
- /* V2's span: the size less what the others take -- a sum, not a share (ROBOT_ARM's tool flange). */
178
+ /* A span: the size less what the others take -- a sum, not a share (ROBOT_ARM's tool flange). */
179
179
  const fill = structuredClone(asset)
180
180
  const m = fill.document.model as unknown as { nodes: { id: string; op: string; args: string[]; outputs: Record<string, string> }[]; constants: { id: string; unit: string; value: number }[] }
181
181
  m.constants.push({ id: 'k.gaps', unit: 'mm', value: -90 })
@@ -1,7 +1,7 @@
1
1
  /*
2
- * **V3 부품의 재질을 V2 처럼 고친다** (V3 동등표 ③, 2026-09-24).
2
+ * **부품의 재질을 고친다** (2026-09-24).
3
3
  *
4
- * V3 편집기는 색 역할을 날것의 이름(`palette.dark`)으로 고르는 칸 하나였다. 재질 칸이 내는 편집이 figure-model 에서
4
+ * 전의 편집기는 색 역할을 날것의 이름(`palette.dark`)으로 고르는 칸 하나였다. 재질 칸이 내는 편집이 figure-model 에서
5
5
  * 정말로 받아들여지고, 그려지는 것을 바꾸는지를 잰다.
6
6
  */
7
7
  import { test } from 'node:test'
@@ -67,7 +67,7 @@ test('자산에 없는 색 역할은 그 색을 함께 보낸다 — 새 도형
67
67
  assert.match(material, /\.\.\.\(Object\.keys\(palette\)\.length \? \{ palette \} : \{\}\)/)
68
68
  })
69
69
 
70
- test('분할 수는 V2 처럼 — 흔한 넷은 다각형 윤곽과 삼각형 수로, 나머지는 접어 둔다', () => {
70
+ test('분할 수 — 흔한 넷은 다각형 윤곽과 삼각형 수로, 나머지는 접어 둔다', () => {
71
71
  const source = readFileSync(fileURLToPath(new URL('../client/modeller/figure-v3-material.ts', import.meta.url)), 'utf8')
72
72
  assert.ok(source.includes('SEGMENT_PRESETS.slice(0, 4).map(') && source.includes('${silhouette(preset)}'))
73
73
  assert.ok(source.includes('<details free') && source.includes('SEGMENT_PRESETS.slice(4)'))
@@ -1,9 +1,8 @@
1
1
  /*
2
- * **V3 성숙도 탭은 V2 리포트를 V3 수로 다시 보인다** (사용자 2026-09-24: 「분석부분도 많이 부족하고, 왜 이렇게 너프되었는지」).
2
+ * **성숙도 탭** (사용자 2026-09-24: 「분석부분도 많이 부족하고, 왜 이렇게 너프되었는지」).
3
3
  *
4
- * V2 리포트(figure-report.ts)는 개수 · 등급과 눈금 · 등급을 정한 한도 · 여섯 축 radar · 한도 막대 · 그리는 비용 · 재사용
5
- * 추이를 보였다. 여기서는 그 수들이 V3 자산에서 V2 의 셈 그대로 나오는지, V3 렌더러가 실제로 나눠 쓰는 것을 세는지,
6
- * 화면이 그 순서와 말로 그리는지를 본다.
4
+ * 탭은 개수 · 등급과 눈금 · 등급을 정한 한도 · 여섯 축 radar · 한도 막대 · 그리는 비용 · 재사용 추이를 보인다. 여기서는
5
+ * 그 수들이 V3 자산에서 바르게 나오는지, V3 렌더러가 실제로 나눠 쓰는 것을 세는지, 화면이 그 순서와 말로 그리는지를 본다.
7
6
  */
8
7
  import { test } from 'node:test'
9
8
  import assert from 'node:assert/strict'
@@ -49,7 +48,7 @@ test('부품이 없으면 리포트가 없다 — 0 으로 그리지 않는다',
49
48
  assert.equal(v3ReportOf(createV3Asset('Empty')), undefined)
50
49
  })
51
50
 
52
- test('등급은 V2 의 눈금이다 — 한도의 절반 A · 4분의 3 B · 한도 C · 넘치면 D', () => {
51
+ test('등급의 눈금 — 한도의 절반 A · 4분의 3 B · 한도 C · 넘치면 D', () => {
53
52
  assert.deepEqual(GRADE_SCALE, [
54
53
  ['A', 0.5],
55
54
  ['B', 0.75],
@@ -104,7 +103,7 @@ test('투명 재질은 서로 다른 재질로 센다 — 둘이면 한도 1 을
104
103
  assert.equal(v3ReportOf(same)!.score.budgets.find(b => b.key === 'transparent')!.used, 1, '같은 재질은 하나로 나눠 쓴다')
105
104
  })
106
105
 
107
- test('상태를 따라 움직이는 부품을 센다 — V2 의 「따로 움직이는 부품」', () => {
106
+ test('상태를 따라 움직이는 부품을 센다 — 「따로 움직이는 부품」', () => {
108
107
  let asset = createV3Asset('Probe')
109
108
  asset = addV3Box(asset, cube('base', 0))
110
109
  asset = addV3Box(asset, cube('arm', 200))
@@ -138,7 +137,7 @@ test('서로 다른 형상은 things-scene 이 정점 버퍼를 나눠 쓰는
138
137
  assert.equal(report.axes.find(axis => axis.key === 'reuse')!.ratio, 1 - 4 / 6)
139
138
  })
140
139
 
141
- test('여섯 축 — V2 의 순서, 잴 수 없는 둘은 비율 없이 까닭을 단다', () => {
140
+ test('여섯 축 — 정해진 순서, 잴 수 없는 둘은 비율 없이 까닭을 단다', () => {
142
141
  let asset = withPalette('palette.accent')
143
142
  asset = addV3Box(asset, cube('a', 50))
144
143
  asset = addV3Box(asset, cube('b', 150, 'palette.accent'))
@@ -161,7 +160,7 @@ test('여섯 축 — V2 의 순서, 잴 수 없는 둘은 비율 없이 까닭
161
160
  assert.equal(byKey.readable!.reading, '2')
162
161
  })
163
162
 
164
- test('재질이 한도를 넘으면 일관성이 한 칸 내려간다 — V2 의 셋 중 하나', () => {
163
+ test('재질이 한도를 넘으면 일관성이 한 칸 내려간다 — 셋 중 하나', () => {
165
164
  let asset = withPalette('t1', 't2', 't3')
166
165
  asset = addV3Box(asset, cube('a', 0))
167
166
  asset = addV3Box(asset, cube('b', 200, 't1'))
@@ -205,7 +204,7 @@ test('추이 — 1 에서 1만까지 1·2·5, 삼각형과 draw call 은 오르
205
204
  assert.deepEqual(layout.xTicks.map(t => t.label), ['1', '10', '100', '1k', '10k'])
206
205
  })
207
206
 
208
- test('화면은 V2 리포트의 차례로 그린다 — 개수 · 축 · 한도 · 그리는 비용 · 추이 · 발행 판정', () => {
207
+ test('화면은 리포트의 차례로 그린다 — 개수 · 축 · 한도 · 그리는 비용 · 추이 · 발행 판정', () => {
209
208
  const order = ['this.renderVerdict(report)', 'this.renderAxes(report)', 'this.renderBudgets(report)', 'this.renderMetrics(report)', 'this.renderCurve()', 'this.renderGate()']
210
209
  // From the full report, past the early return that shows only guidance and the release verdict.
211
210
  const at = order.map(call => verdict.indexOf(call, verdict.indexOf('${this.renderVerdict(report)}')))
@@ -1,7 +1,7 @@
1
1
  /*
2
- * **V3 편집기의 부품 목록은 V2 의 목록이다** (V3 동등표 ③, 2026-09-24).
2
+ * **편집기의 부품 목록** (2026-09-24).
3
3
  *
4
- * `figure-v3-parts` 는 V2 목록과 같은 자리 · 같은 조작 · 같은 사건 이름으로 지어졌지만(fdc15e162e) 편집기 어디에도
4
+ * `figure-v3-parts` 는 지어졌지만(fdc15e162e) 편집기 어디에도
5
5
  * 걸리지 않았다. 편집기는 맨 단추 네 개와 이름 단추 목록이었다. 패널이 내는 사건마다 받는 자리가 있어야 한다 —
6
6
  * 없으면 눌러도 아무 일이 없다.
7
7
  */
@@ -53,7 +53,7 @@ test('좁은 칸에서도 고른 줄의 이름과 조작이 다 보인다 —
53
53
  assert.match(narrow, /li\[selected\] div\[ops\] \{\s*flex-basis: 100%;/)
54
54
  })
55
55
 
56
- test('둥근 상자는 제 이름으로 부른다 — 「사각 단면」은 V2 의 다른 도형이다', () => {
56
+ test('둥근 상자는 제 이름으로 부른다 — 「사각 단면」이 아니다', () => {
57
57
  assert.match(panel, /kind: 'rounded-box', icon: 'crop_square', label: 'figure\.label\.primitive-rounded-box'/)
58
58
  for (const lang of ['ko', 'en', 'ja', 'zh', 'ms'])
59
59
  assert.ok(JSON.parse(read(`../translations/${lang}.json`))['figure.label.primitive-rounded-box'], lang)
@@ -1,5 +1,5 @@
1
1
  /*
2
- * **미리보기 — V2 의 그것** (사용자 2026-09-24: 「미리보기도 v2 가 낫다」). 저작 중인 도형을 도면처럼 등록하고,
2
+ * **미리보기** (사용자 2026-09-24). 저작 중인 도형을 도면처럼 등록하고,
3
3
  * 기준 크기와 한 · 두 · 세 축을 2배로 늘린 일곱을 나란히 FigureInstance 로 세운다.
4
4
  */
5
5
  import { test } from 'node:test'
@@ -9,7 +9,7 @@ test('V3 saves preserve the graph and clear unmeasured previous scores', () => {
9
9
  const saved = prepareV3Write(source)
10
10
  assert.equal(saved.source, source)
11
11
  assert.equal(saved.name, '빈 자산')
12
- /* 점수는 V2 재사용의 잣대다 — V3 를 잰 적이 없으므로 비운다. */
12
+ /* 점수는 저장할 때 재지 않으므로 비운다. */
13
13
  assert.equal(saved.score, null)
14
14
  /* 예산은 잰다. 빈 자산은 0 이고, 그것은 「안 쟀다」가 아니라 「그릴 것이 없다」는 뜻이다. */
15
15
  assert.equal(saved.triangles, 0)
@@ -1,7 +1,7 @@
1
1
  /*
2
- * **V3 화면에서 도형의 속성을 고친다** (V3 동등표 ②, 2026-09-24).
2
+ * **도형의 속성을 고친다** (2026-09-24).
3
3
  *
4
- * V2 인스펙터는 이름 · 배치 기준 · 차지하는 공간 · 분류 · 태그를 맨 위에 둔다. V3 화면에는 타입 칸 하나뿐이었다.
4
+ * 속성 탭은 이름 · 배치 기준 · 차지하는 공간 · 분류 · 태그를 맨 위에 둔다. 전에는 타입 칸 하나뿐이었다.
5
5
  * 두 축을 본다: 탭이 내는 명령이 figure-model 에서 **정말로** 그 일을 하는가, 그리고 화면이 명령과 행 쓰기를 잇는가.
6
6
  */
7
7
  import { test } from 'node:test'
@@ -53,7 +53,7 @@ test('이름을 바꾸면 자산의 이름이 바뀐다 — 저장하면 목록
53
53
  assert.equal(facts(applyV3Authoring(probe(), { kind: 'rename', name: 'Roller conveyor' })).name, 'Roller conveyor')
54
54
  })
55
55
 
56
- test('태그 줄은 V2 와 같은 규칙으로 목록이 된다', () => {
56
+ test('태그 줄은 쉼표로 나뉘어 목록이 된다', () => {
57
57
  assert.deepEqual(tagsOf('#반출, 컨베이어,, 반출 '), ['반출', '컨베이어'])
58
58
  assert.deepEqual(tagsOf(''), [], '빈 줄은 전부 지우는 것이다')
59
59
  })
@@ -122,3 +122,15 @@ test('배치를 바꿀 수 없는 까닭 — 평면에 붙은 부품과 새 평
122
122
  const props = readFileSync(fileURLToPath(new URL('../client/modeller/figure-v3-properties.ts', import.meta.url)), 'utf8')
123
123
  assert.ok(props.includes('select.value = current'), '거절되면 목록이 원래 배치로 돌아온다')
124
124
  })
125
+
126
+ test('범위 밖 값은 받을 수 있는 범위를 숫자로 말한다 — 커널의 영어가 아니다 (Figure 레인 :3300, 2026-09-26)', async () => {
127
+ const { inputRangeRefusalOf } = await import('../client/modeller/v3-refusals.ts')
128
+ const range = (message: string) => Object.assign(new Error(message), { code: 'INPUT_RANGE' })
129
+ const size = inputRangeRefusalOf(range('INPUT_RANGE: size.x: 6021100 outside [300,1200]'), say)!
130
+ assert.ok(size.includes('X') && size.includes('300') && size.includes('1,200') && size.includes('6,021,100'), size)
131
+ assert.ok(!/outside|INPUT_RANGE/.test(size))
132
+ const other = inputRangeRefusalOf(range('INPUT_RANGE: lift.stroke: -5 outside [0,1]'), say)!
133
+ assert.ok(other.includes('0') && other.includes('1') && other.includes('-5'), other)
134
+ assert.equal(inputRangeRefusalOf(range('INPUT_RANGE: steps: at the length it is authored (100 mm) a pitch under 1 mm makes more than 50 copies'), say), null)
135
+ assert.equal(inputRangeRefusalOf(Object.assign(new Error('x'), { code: 'EDIT_TARGET' }), say), null)
136
+ })
@@ -53,10 +53,10 @@ test('선언한 공간을 넘은 부품도 같은 한 번으로 풀리고, 배
53
53
 
54
54
  const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
55
55
 
56
- test('V3 화면이 발행 패널을 V3 로 걸고, 패널이 내는 셋을 받는다', () => {
56
+ test('모델러가 발행 패널을 걸고, 패널이 내는 셋을 받는다', () => {
57
57
  const page = read('../client/pages/figure-v3-modeller-page.ts')
58
58
 
59
- assert.ok(/<figure-history-panel[\s\S]*?format="v3"/.test(page), '패널을 V3 형식으로 건다')
59
+ assert.ok(/<figure-history-panel/.test(page), '패널을 건다')
60
60
  for (const event of ['fit-base-to-parts', 'figure-released', 'figure-reverted']) {
61
61
  assert.ok(page.includes(`@${event}=`), `패널이 내는 ${event} 를 받는 자리가 없다 — 눌러도 아무 일이 없다`)
62
62
  }
@@ -65,9 +65,9 @@ test('V3 화면이 발행 패널을 V3 로 걸고, 패널이 내는 셋을 받
65
65
  assert.ok(/:host \{[^}]*overflow: hidden/.test(page), '페이지가 주어진 폭 안에 든다')
66
66
  })
67
67
 
68
- test('패널은 받는 화면이 있는 조치만 그린다 — V3 에 「바닥에 앉히기」는 없다', () => {
68
+ test('패널은 받는 화면이 있는 조치만 그린다 — 「바닥에 앉히기」는 없다', () => {
69
69
  const panel = read('../client/modeller/figure-history-panel.ts')
70
70
 
71
- assert.ok(/'parts-off-placement-face' && this\.format === 'v2'/.test(panel), '앉히기는 V2 에서만')
72
- assert.ok(/code === 'part-outside-occupancy' \|\| code === 'occupancy-undeclared'/.test(panel), 'V3 의 두 판정에 맞추기를 낸다')
71
+ assert.ok(!/settle-parts-on-floor|apply-sizing-fix|restore-released-footprint/.test(panel), '받는 화면이 없는 조치를 내지 않는다')
72
+ assert.ok(/code === 'part-outside-occupancy' \|\| code === 'occupancy-undeclared'/.test(panel), '두 판정에 맞추기를 낸다')
73
73
  })
@@ -1,5 +1,5 @@
1
1
  /*
2
- * **편집 캔버스는 V2 처럼 things-scene 편집 모드 위에 선다** (사용자 2026-09-24: 「v2 방식으로 하면 안되냐」).
2
+ * **편집 캔버스는 things-scene 편집 모드 위에 선다** (사용자 2026-09-24).
3
3
  *
4
4
  * 부품마다 `figure-v3-part` 컴포넌트로 판에 세우면 고르기 · 주황 테두리 · 기즈모와 스냅 · 박스 선택 · 카메라(감속 ·
5
5
  * 커서 쪽 확대 · 더블클릭 맞추기 · 시점 1~6) · 스튜디오 환경 · 좌표 바탕 · 조명과 그림자가 씬에서 온다. 여기서는 그
@@ -37,7 +37,7 @@ test('things-scene 의 판을 편집 모드로 세우고, 보기 설정은 판
37
37
  assert.match(canvas, /type: 'model-layer'/)
38
38
  assert.match(canvas, /mode: 1\n/, '편집 모드 — 시점 단추 · 기즈모 · 도움말 · 단축키')
39
39
  assert.ok(canvas.includes('...sceneLook(this.view ?? DEFAULT_VIEW)'))
40
- assert.ok(canvas.includes('pixelRatio: Math.min(3, (window.devicePixelRatio || 1) * 2)'), 'V2 의 두 배 표본')
40
+ assert.ok(canvas.includes('pixelRatio: Math.min(3, (window.devicePixelRatio || 1) * 2)'), '두 배 표본')
41
41
  assert.doesNotMatch(canvas, /new THREE\.|WebGLRenderer|OrbitControls|TransformControls/, '제 장면을 짜지 않는다')
42
42
  })
43
43
 
@@ -1,5 +1,5 @@
1
1
  /*
2
- * **V3 모델러는 테마를 따른다** (V3 동등표 ⑦, 2026-09-24).
2
+ * **모델러는 테마를 따른다** (2026-09-24).
3
3
  *
4
4
  * V3 캔버스와 편집기는 색을 박아 두었다. 어두운 테마에서 가운데만 밝은 판이 떴고, 경계선과 눌린 단추 · 오류 칸도
5
5
  * 밝은 색 그대로였다. 색은 셸의 토큰에서 온다(UX 결정 — 바탕은 surface-container, 눌림은 secondary-container,
@@ -24,7 +24,7 @@ test('편집기에 박힌 색이 없다 — 토큰의 대체값만 남는다', (
24
24
  assert.deepEqual(fixed(editor), [], 'v3-editor.ts')
25
25
  })
26
26
 
27
- test('캔버스 바탕은 V2 의 작업대 회색 — 판의 하늘색이라 환경 탭에서 바꾼다', () => {
27
+ test('캔버스 바탕은 작업대 회색 — 판의 하늘색이라 환경 탭에서 바꾼다', () => {
28
28
  assert.ok(canvas.includes('background-color: ${unsafeCSS(WORKBENCH)}'), '씬이 서기 전 · 2D 쪽도 같은 색')
29
29
  assert.ok(canvas.includes("sky: 'color'") && canvas.includes('...sceneLook(this.view ?? DEFAULT_VIEW)'), '3D 바탕은 보기 설정의 skyColor')
30
30
  assert.match(read('../client/modeller/figure-view.ts'), /skyColor: WORKBENCH/)
@@ -1,7 +1,7 @@
1
1
  /*
2
- * **V3 화면이 편집하는 동안 판정한다** (V3 동등표 ①, 2026-09-24).
2
+ * **편집하는 동안 판정한다** (2026-09-24).
3
3
  *
4
- * V2 모델러는 고치는 동안 「한 화면에 몇 개」와 「무엇이 발행을 막나」를 보여 준다. V3 화면은 둘 다 없어서 막힌
4
+ * 모델러는 고치는 동안 「한 화면에 몇 개」와 「무엇이 발행을 막나」를 보여 준다. 전에는 둘 다 없어서 막힌
5
5
  * 까닭을 저장한 뒤 발행 패널에서야 알았다. 판정 탭이 편집 중인 원본으로 둘을 답한다.
6
6
  *
7
7
  * 세 축을 본다: 개수가 목록 카드와 같은 셈인가, 판정이 발행 패널과 같은 문 · 같은 문장인가, 화면이 탭과 조치를
@@ -1,5 +1,5 @@
1
1
  /*
2
- * **보기 — 작업대의 설정** (V3 동등표 ⑦, V2 의 「환경」 탭). 도형에 저장되지 않고, 보는 사람의 브라우저에 남는다.
2
+ * **보기 — 작업대의 설정** (「환경」 탭). 도형에 저장되지 않고, 보는 사람의 브라우저에 남는다.
3
3
  */
4
4
  import { test } from 'node:test'
5
5
  import assert from 'node:assert/strict'
@@ -9,7 +9,7 @@ import { fileURLToPath } from 'node:url'
9
9
 
10
10
  const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
11
11
 
12
- test('환경 탭은 V2 의 보기 패널 그대로 — 설정은 판의 상태 이름이고, 도형이 아니라 브라우저에 둔다', () => {
12
+ test('환경 탭 — 설정은 판의 상태 이름이고, 도형이 아니라 브라우저에 둔다', () => {
13
13
  const editor = read('../client/modeller/v3-editor.ts')
14
14
  assert.ok(editor.includes('<figure-settings .view=${this.view} @update-view=${(e: CustomEvent) => this.setView(e.detail.view)}></figure-settings>'))
15
15
  assert.ok(editor.includes("localStorage.setItem('figure-v3-view'"))