@things-factory/figure-ui 10.1.147 → 10.1.150

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 (210) hide show
  1. package/client/bootstrap.ts +10 -2
  2. package/client/graphql/index.ts +20 -0
  3. package/client/modeller/canvas.ts +6 -2
  4. package/client/modeller/carrier-declaration.ts +63 -0
  5. package/client/modeller/editor.ts +62 -27
  6. package/client/modeller/figure-carrier.ts +117 -0
  7. package/client/modeller/figure-cavity-drawing.ts +3 -9
  8. package/client/modeller/figure-first-save.ts +4 -7
  9. package/client/modeller/figure-history-panel.ts +2 -3
  10. package/client/modeller/figure-links.ts +46 -60
  11. package/client/modeller/figure-material-panel.ts +4 -14
  12. package/client/modeller/figure-mesh-choice.ts +68 -0
  13. package/client/modeller/figure-parts.ts +23 -1
  14. package/client/modeller/figure-preview.ts +2 -2
  15. package/client/modeller/figure-properties.ts +5 -3
  16. package/client/modeller/figure-section-editor.ts +12 -33
  17. package/client/modeller/figure-section-panel.ts +6 -7
  18. package/client/modeller/figure-slot-layout-panel.ts +208 -0
  19. package/client/modeller/figure-verdict.ts +21 -31
  20. package/client/modeller/first-save.ts +23 -0
  21. package/client/modeller/links.ts +52 -1
  22. package/client/modeller/material-edit.ts +38 -0
  23. package/client/modeller/maturity.ts +28 -0
  24. package/client/modeller/mesh-import.ts +77 -0
  25. package/client/modeller/modeller-route.ts +9 -0
  26. package/client/modeller/mounting-part.ts +15 -0
  27. package/client/modeller/newest-answer.ts +21 -0
  28. package/client/modeller/preview-model.ts +5 -0
  29. package/client/modeller/refusals.ts +168 -82
  30. package/client/modeller/section-controls.ts +25 -1
  31. package/client/modeller/section-drawing.ts +43 -1
  32. package/client/modeller/slot-layout-host.ts +140 -0
  33. package/client/pages/figure-modeller-page.ts +6 -5
  34. package/client/viewparts/detail-route.ts +7 -0
  35. package/client/viewparts/figure-detail-dock.ts +1 -3
  36. package/dist-client/bootstrap.d.ts +1 -1
  37. package/dist-client/bootstrap.js +9 -2
  38. package/dist-client/bootstrap.js.map +1 -1
  39. package/dist-client/graphql/index.d.ts +5 -0
  40. package/dist-client/graphql/index.js +19 -0
  41. package/dist-client/graphql/index.js.map +1 -1
  42. package/dist-client/modeller/canvas.d.ts +2 -0
  43. package/dist-client/modeller/canvas.js +10 -2
  44. package/dist-client/modeller/canvas.js.map +1 -1
  45. package/dist-client/modeller/carrier-declaration.d.ts +31 -0
  46. package/dist-client/modeller/carrier-declaration.js +53 -0
  47. package/dist-client/modeller/carrier-declaration.js.map +1 -0
  48. package/dist-client/modeller/editor.d.ts +13 -0
  49. package/dist-client/modeller/editor.js +62 -27
  50. package/dist-client/modeller/editor.js.map +1 -1
  51. package/dist-client/modeller/figure-carrier.d.ts +26 -0
  52. package/dist-client/modeller/figure-carrier.js +123 -0
  53. package/dist-client/modeller/figure-carrier.js.map +1 -0
  54. package/dist-client/modeller/figure-cavity-drawing.js +3 -11
  55. package/dist-client/modeller/figure-cavity-drawing.js.map +1 -1
  56. package/dist-client/modeller/figure-first-save.d.ts +4 -0
  57. package/dist-client/modeller/figure-first-save.js +5 -8
  58. package/dist-client/modeller/figure-first-save.js.map +1 -1
  59. package/dist-client/modeller/figure-history-panel.js +2 -4
  60. package/dist-client/modeller/figure-history-panel.js.map +1 -1
  61. package/dist-client/modeller/figure-links.d.ts +4 -1
  62. package/dist-client/modeller/figure-links.js +47 -57
  63. package/dist-client/modeller/figure-links.js.map +1 -1
  64. package/dist-client/modeller/figure-material-panel.d.ts +0 -1
  65. package/dist-client/modeller/figure-material-panel.js +4 -14
  66. package/dist-client/modeller/figure-material-panel.js.map +1 -1
  67. package/dist-client/modeller/figure-mesh-choice.d.ts +16 -0
  68. package/dist-client/modeller/figure-mesh-choice.js +75 -0
  69. package/dist-client/modeller/figure-mesh-choice.js.map +1 -0
  70. package/dist-client/modeller/figure-parts.d.ts +1 -1
  71. package/dist-client/modeller/figure-parts.js +22 -0
  72. package/dist-client/modeller/figure-parts.js.map +1 -1
  73. package/dist-client/modeller/figure-preview.js +2 -2
  74. package/dist-client/modeller/figure-preview.js.map +1 -1
  75. package/dist-client/modeller/figure-properties.d.ts +2 -1
  76. package/dist-client/modeller/figure-properties.js +5 -3
  77. package/dist-client/modeller/figure-properties.js.map +1 -1
  78. package/dist-client/modeller/figure-section-editor.d.ts +2 -2
  79. package/dist-client/modeller/figure-section-editor.js +12 -33
  80. package/dist-client/modeller/figure-section-editor.js.map +1 -1
  81. package/dist-client/modeller/figure-section-panel.js +6 -7
  82. package/dist-client/modeller/figure-section-panel.js.map +1 -1
  83. package/dist-client/modeller/figure-slot-layout-panel.d.ts +36 -0
  84. package/dist-client/modeller/figure-slot-layout-panel.js +198 -0
  85. package/dist-client/modeller/figure-slot-layout-panel.js.map +1 -0
  86. package/dist-client/modeller/figure-verdict.d.ts +1 -1
  87. package/dist-client/modeller/figure-verdict.js +19 -37
  88. package/dist-client/modeller/figure-verdict.js.map +1 -1
  89. package/dist-client/modeller/first-save.d.ts +10 -0
  90. package/dist-client/modeller/first-save.js +16 -0
  91. package/dist-client/modeller/first-save.js.map +1 -1
  92. package/dist-client/modeller/links.d.ts +26 -1
  93. package/dist-client/modeller/links.js +43 -0
  94. package/dist-client/modeller/links.js.map +1 -1
  95. package/dist-client/modeller/material-edit.d.ts +11 -0
  96. package/dist-client/modeller/material-edit.js +28 -0
  97. package/dist-client/modeller/material-edit.js.map +1 -0
  98. package/dist-client/modeller/maturity.d.ts +15 -0
  99. package/dist-client/modeller/maturity.js +23 -0
  100. package/dist-client/modeller/maturity.js.map +1 -1
  101. package/dist-client/modeller/mesh-import.d.ts +47 -0
  102. package/dist-client/modeller/mesh-import.js +47 -0
  103. package/dist-client/modeller/mesh-import.js.map +1 -0
  104. package/dist-client/modeller/modeller-route.d.ts +5 -0
  105. package/dist-client/modeller/modeller-route.js +9 -0
  106. package/dist-client/modeller/modeller-route.js.map +1 -1
  107. package/dist-client/modeller/mounting-part.d.ts +14 -0
  108. package/dist-client/modeller/mounting-part.js +10 -0
  109. package/dist-client/modeller/mounting-part.js.map +1 -0
  110. package/dist-client/modeller/newest-answer.d.ts +12 -0
  111. package/dist-client/modeller/newest-answer.js +21 -0
  112. package/dist-client/modeller/newest-answer.js.map +1 -0
  113. package/dist-client/modeller/preview-model.d.ts +2 -0
  114. package/dist-client/modeller/preview-model.js +4 -0
  115. package/dist-client/modeller/preview-model.js.map +1 -1
  116. package/dist-client/modeller/refusals.d.ts +26 -5
  117. package/dist-client/modeller/refusals.js +184 -86
  118. package/dist-client/modeller/refusals.js.map +1 -1
  119. package/dist-client/modeller/section-controls.d.ts +17 -1
  120. package/dist-client/modeller/section-controls.js +18 -0
  121. package/dist-client/modeller/section-controls.js.map +1 -1
  122. package/dist-client/modeller/section-drawing.d.ts +24 -1
  123. package/dist-client/modeller/section-drawing.js +42 -0
  124. package/dist-client/modeller/section-drawing.js.map +1 -1
  125. package/dist-client/modeller/slot-layout-host.d.ts +81 -0
  126. package/dist-client/modeller/slot-layout-host.js +105 -0
  127. package/dist-client/modeller/slot-layout-host.js.map +1 -0
  128. package/dist-client/pages/figure-modeller-page.js +5 -4
  129. package/dist-client/pages/figure-modeller-page.js.map +1 -1
  130. package/dist-client/tsconfig.tsbuildinfo +1 -1
  131. package/dist-client/viewparts/detail-route.d.ts +5 -0
  132. package/dist-client/viewparts/detail-route.js +8 -0
  133. package/dist-client/viewparts/detail-route.js.map +1 -0
  134. package/dist-client/viewparts/figure-detail-dock.d.ts +0 -1
  135. package/dist-client/viewparts/figure-detail-dock.js +1 -3
  136. package/dist-client/viewparts/figure-detail-dock.js.map +1 -1
  137. package/package.json +9 -8
  138. package/test/align.test.ts +0 -9
  139. package/test/browser/assistant-stand-in.js +32 -0
  140. package/test/browser/build.check.mjs +77 -0
  141. package/test/browser/build.mjs +31 -6
  142. package/test/browser/editing-page.js +224 -0
  143. package/test/browser/editing.check.mjs +550 -0
  144. package/test/browser/links-page.js +113 -0
  145. package/test/browser/links.check.mjs +568 -0
  146. package/test/browser/modeller.check.mjs +21 -0
  147. package/test/browser/parts-page.js +120 -0
  148. package/test/browser/parts.check.mjs +328 -0
  149. package/test/browser/places-mesh-page.js +36 -0
  150. package/test/browser/places-mesh.check.mjs +239 -0
  151. package/test/browser/product-page.check.mjs +682 -0
  152. package/test/browser/product-page.js +276 -0
  153. package/test/browser/screen-page.js +112 -0
  154. package/test/browser/screen-product-page.js +62 -0
  155. package/test/browser/screen.check.mjs +1003 -0
  156. package/test/browser/section-page.js +256 -0
  157. package/test/browser/section.check.mjs +719 -0
  158. package/test/browser/serve.mjs +20 -12
  159. package/test/carrier-declaration.test.ts +69 -0
  160. package/test/cavity-drawing.test.ts +2 -18
  161. package/test/clip-ai-proposal.test.ts +1 -5
  162. package/test/clip-consent.test.ts +0 -5
  163. package/test/clips.test.ts +0 -7
  164. package/test/command-preview.test.ts +0 -68
  165. package/test/controls.test.ts +0 -9
  166. package/test/detail-dock.test.ts +6 -25
  167. package/test/face-mapping.test.ts +1 -20
  168. package/test/fasten-drawing.test.ts +0 -23
  169. package/test/fields.test.ts +6 -33
  170. package/test/figure-assistant-surface.test.ts +32 -38
  171. package/test/first-minute.test.ts +3 -55
  172. package/test/first-save-suggest.test.ts +11 -8
  173. package/test/first-save.test.ts +4 -26
  174. package/test/gizmo.test.ts +0 -25
  175. package/test/joint-fan.test.ts +0 -10
  176. package/test/keys.test.ts +0 -7
  177. package/test/layout.test.ts +4 -73
  178. package/test/links.test.ts +48 -67
  179. package/test/live.test.ts +0 -34
  180. package/test/material.test.ts +17 -26
  181. package/test/maturity.test.ts +13 -16
  182. package/test/mesh-import.test.ts +76 -0
  183. package/test/modeller-route.test.ts +3 -19
  184. package/test/package-download.test.ts +6 -7
  185. package/test/parts-wiring.test.ts +9 -51
  186. package/test/preview.test.ts +5 -46
  187. package/test/properties.test.ts +11 -26
  188. package/test/proposal-record.test.ts +2 -19
  189. package/test/recipes.test.ts +0 -15
  190. package/test/refusals.test.ts +257 -151
  191. package/test/release-entry.test.ts +1 -9
  192. package/test/release-panel.test.ts +1 -22
  193. package/test/resize-with-placement.test.ts +0 -32
  194. package/test/revert-choice.test.ts +1 -22
  195. package/test/revert-keeping.test.ts +1 -12
  196. package/test/save-landed.test.ts +16 -14
  197. package/test/scene-canvas.test.ts +2 -13
  198. package/test/section-editor.test.ts +30 -83
  199. package/test/section-panel.test.ts +22 -17
  200. package/test/slot-layout-host.test.ts +117 -0
  201. package/test/theme.test.ts +5 -9
  202. package/test/verdict.test.ts +44 -41
  203. package/test/view.test.ts +5 -12
  204. package/translations/en.json +61 -1
  205. package/translations/es.json +61 -1
  206. package/translations/ja.json +61 -1
  207. package/translations/ko.json +61 -1
  208. package/translations/ms.json +61 -1
  209. package/translations/zh.json +61 -1
  210. package/test/thumbnail.test.ts +0 -29
@@ -69,7 +69,8 @@ test('움직임 — 미끄러짐 · 돌기, 축, 무엇이 모느냐를 읽는
69
69
 
70
70
  import { readFileSync } from 'node:fs'
71
71
  import { fileURLToPath } from 'node:url'
72
- import { choiceOf } from '../client/modeller/links.ts'
72
+ import { alignRowsOf, choiceOf, followCommandOf, ownFacesOf } from '../client/modeller/links.ts'
73
+ import { faceViewOf } from '../client/modeller/fasten-geometry.ts'
73
74
  import { moving } from './joints.ts'
74
75
 
75
76
  const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
@@ -81,20 +82,6 @@ test('고르개의 값은 지금 따르는 것을 가리킨다', () => {
81
82
  assert.equal(choiceOf({ of: 'part', part: 'plate', axis: 'x' }), 'part:plate:x')
82
83
  })
83
84
 
84
- test('편집기는 세 칸의 명령을 세션의 이력 하나로 돌리고, 만날 수 있는 거절을 사람의 말로 한다', () => {
85
- const editor = read('../client/modeller/editor.ts')
86
- assert.match(editor, /@command=\$\{\(e: CustomEvent\) => this\.linkCommand\(e\.detail\.action\)\}/)
87
- assert.match(editor, /this\.session!\.command\(action\)/)
88
- assert.match(editor, /code === 'DIMENSION_CYCLE'[\s\S]*?figure\.text\.follow-would-loop/)
89
- assert.match(editor, /code === 'ATTACH_CYCLE'[\s\S]*?figure\.text\.fasten-would-loop/)
90
- })
91
-
92
- test('떼면 지금 크기로 고정한다 — 따르던 값을 잃지 않는다', () => {
93
- const links = read('../client/modeller/figure-links.ts')
94
- assert.ok(links.includes("this.command({ kind: 'unlink-dimension', part: this.partId, axis, mm: Math.max(1, Math.round(current)) })"))
95
- assert.ok(links.includes('const current = dimensionNow(this.revision!, this.partId, axis) ?? boxOf(this.revision!, this.partId)?.dims[AXES.indexOf(axis)] ?? 100'))
96
- })
97
-
98
85
  test('세 칸의 말은 다섯 말 모두에 있다', () => {
99
86
  const links = read('../client/modeller/figure-links.ts')
100
87
  const keys = [...links.matchAll(/'(figure\.[a-z.-]*[a-z])'/g)].map(m => m[1])
@@ -142,19 +129,9 @@ test('담는 능력이 없는 도형은 자리가 있어도 보드가 아무것
142
129
  asset.document.capabilities.names = ['FlowNode']
143
130
  assert.equal(holdsThings(asset), true, 'FlowNode 는 Capacity 를 품는다')
144
131
 
145
- const links = read('../client/modeller/figure-links.ts')
146
- assert.match(links, /holdsThings\(asset\)/)
147
- assert.match(links, /kind: 'set-capabilities', names/)
148
- const editor = read('../client/modeller/editor.ts')
149
- /* A slice and `includes`, not a lazy regex over the whole editor: that hangs when it does not match. */
150
- const start = editor.indexOf('<figure-links')
151
- const block = editor.slice(start, editor.indexOf('</figure-links>', start))
152
- assert.ok(start >= 0 && block.includes('@part-edit=${(e: CustomEvent) => this.run(() => this.session!.edit(e.detail.edit))}'), '자리 절의 능력 선언이 편집기에 닿는다')
153
132
  })
154
133
 
155
134
  test('받는 종류가 없으면 자리를 만들지 않는다 — 형식에 「아무거나」가 없다', () => {
156
- const links = read('../client/modeller/figure-links.ts')
157
- assert.match(links, /\?disabled=\$\{this\.locked \|\| !accepts\.length \|\| !capacityOk\}/)
158
135
  assert.deepEqual(acceptsOf(' , ,'), [])
159
136
  })
160
137
 
@@ -201,10 +178,32 @@ test('자리를 크기에 비례로 두면 그렇게 읽고, 다시 권하지
201
178
  asset = applyFigureAuthoring(asset, { kind: 'scale-position', part: 'flange', axis: 'y' })
202
179
  assert.equal(positionIsShare(asset, 'flange', 'y'), true)
203
180
  assert.equal(positionIsShare(asset, 'base', 'y'), false)
204
- const links = read('../client/modeller/figure-links.ts')
205
- assert.ok(links.includes("hasInstanceSize(asset, axis) && !positionIsShare(asset, this.partId, axis)"))
206
- assert.ok(links.includes("this.command({ kind: 'scale-position', part: this.partId, axis })"))
207
- assert.ok(links.includes("const times = Math.round((current / size) * 1e6) / 1e6"), '비례는 지금 치수의 몫')
181
+ })
182
+
183
+ test('떼면 지금 크기로 고정한다 — 따르던 값을 잃지 않는다', () => {
184
+ let asset = sizedPair()
185
+ /* The flange's radius made a fifth of the figure's height: 20 mm now, its width 40. */
186
+ asset = applyFigureAuthoring(asset, { kind: 'link-dimension', part: 'flange', axis: 'x', source: { of: 'instance', axis: 'y' }, times: 0.2 })
187
+ const unlink = (a: FigureAsset, part: string, axis: 'x' | 'y') => followCommandOf(revisionOf(a), part, axis, 'own', followOf(a, part, axis))
188
+ assert.deepEqual(unlink(asset, 'flange', 'x'), { kind: 'unlink-dimension', part: 'flange', axis: 'x', mm: 20 }, 'fixed at its own measure, the radius -- not the 40 it is drawn wide')
189
+ /* A measure under a millimetre is fixed at one: the kernel takes no size of zero. */
190
+ const thin = applyFigureAuthoring(asset, { kind: 'link-dimension', part: 'base', axis: 'y', source: { of: 'instance', axis: 'y' }, times: 0.004 })
191
+ assert.deepEqual(unlink(thin, 'base', 'y'), { kind: 'unlink-dimension', part: 'base', axis: 'y', mm: 1 })
192
+ assert.equal(unlink(asset, 'base', 'x'), undefined, 'a measure that follows nothing has nothing to let go of')
193
+ const fixed = applyFigureAuthoring(asset, unlink(asset, 'flange', 'x')!)
194
+ assert.equal(dimensionNow(revisionOf(fixed), 'flange', 'x'), 20, 'let go, it keeps its size')
195
+ })
196
+
197
+ test('made to follow in proportion, a measure is the share of the figure size it is now, to a millionth, and keeps its size', () => {
198
+ const asset = sizedPair()
199
+ asset.designInputs['size.y'] = 700
200
+ const share = followCommandOf(revisionOf(asset), 'flange', 'y', 'share:y', followOf(asset, 'flange', 'y'))
201
+ assert.deepEqual(share, { kind: 'link-dimension', part: 'flange', axis: 'y', source: { of: 'instance', axis: 'y' }, times: 0.014286, replace: false }, 'a length of 10 is 0.014286 of a height of 700')
202
+ assert.equal(followCommandOf(revisionOf(asset), 'flange', 'y', 'share:x', followOf(asset, 'flange', 'y')), undefined, 'no share of a size the figure does not declare')
203
+ const linked = applyFigureAuthoring(asset, share!)
204
+ assert.equal(Math.round(dimensionNow(revisionOf(linked), 'flange', 'y')! * 1000) / 1000, 10, 'following, the length is what it was')
205
+ /* Following already, the next rule replaces it; another part's measure is named as it is. */
206
+ assert.deepEqual(followCommandOf(revisionOf(linked), 'flange', 'y', 'part:base:x', followOf(linked, 'flange', 'y')), { kind: 'link-dimension', part: 'flange', axis: 'y', source: { of: 'part', part: 'base', axis: 'x' }, replace: true })
208
207
  })
209
208
 
210
209
  test('놓일 수 있는 크기 — 남은 만큼 채우던 부품을 비례로 바꾸면 다시 재어 넓힌다', () => {
@@ -223,8 +222,6 @@ test('놓일 수 있는 크기 — 남은 만큼 채우던 부품을 비례로
223
222
  assert.equal(rangeDiffers(range.declared, range.measured), true)
224
223
  asset = applyFigureAuthoring(asset, { kind: 'declare-instance-size', range: range.measured, replace: true })
225
224
  assert.equal(rangeDiffers(instanceRangeOf(asset)!.declared, instanceRangeOf(asset)!.measured), false, '다시 재면 같다')
226
- const editor = read('../client/modeller/editor.ts')
227
- assert.ok(editor.includes("this.linkCommand({ kind: 'declare-instance-size', range: range.measured, replace: true })"))
228
225
  })
229
226
 
230
227
  test('놓일 수 있는 크기 — 저작자가 끝을 적어 좁힌다(갠트리, 09-25)', () => {
@@ -241,9 +238,6 @@ test('놓일 수 있는 크기 — 저작자가 끝을 적어 좁힌다(갠트
241
238
  asset = applyFigureAuthoring(asset, { kind: 'declare-instance-size', range: next, replace: true })
242
239
  assert.deepEqual(instanceRangeOf(asset)!.declared.x, { min: 150, max: 20000 })
243
240
  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)를 벗어나면 거절')
244
- const editor = read('../client/modeller/editor.ts')
245
- assert.ok(editor.includes("const next = rangeWith(range.declared, axis, bound, (e.target as HTMLInputElement).valueAsNumber)"))
246
- assert.ok(editor.includes("if (next) this.linkCommand({ kind: 'declare-instance-size', range: next, replace: true })"))
247
241
  })
248
242
 
249
243
  test('한 비율로 커지는 도형 — 자기 숫자인 길이를 세고, 한 번에 따라가게 하며, 무엇이 남는지 이름을 댄다(아키텍트 판정 09-25)', () => {
@@ -261,9 +255,6 @@ test('한 비율로 커지는 도형 — 자기 숫자인 길이를 세고, 한
261
255
  asset = applyFigureAuthoring(asset, { kind: 'follow-size-in-proportion' })
262
256
  assert.deepEqual(proportionToFollow(asset), { count: 0, kept: ['lamp.motion'] }, '다 따르고, mm 로 움직이는 것만 남는다')
263
257
  assert.equal(Math.round(valueNow(revisionOf(asset), 'lamp.dimension.x.value')! * 1e6) / 1e6, 60, '따르게 해도 지금 크기는 그대로')
264
- const editor = read('../client/modeller/editor.ts')
265
- assert.ok(editor.includes("this.linkCommand({ kind: 'follow-size-in-proportion' })"))
266
- assert.ok(editor.includes("?? valueNow(shown, layout?.args[4])"), '길이를 따르는 간격도 숫자로 보인다')
267
258
  })
268
259
 
269
260
  test('관절이 있는 도형에는 한 비율로 키우기를 권하고, 선언하면 규칙으로 보인다(수석 아키텍트 판정 09-24)', () => {
@@ -271,11 +262,6 @@ test('관절이 있는 도형에는 한 비율로 키우기를 권하고, 선언
271
262
  assert.equal(turnsAboutAJoint(asset), false)
272
263
  asset = moving(asset, 'flange', { kind: 'turn', axis: 'y', id: 'flange.turn', min: 0, max: 90 })
273
264
  assert.equal(turnsAboutAJoint(asset), true)
274
- const editor = read('../client/modeller/editor.ts')
275
- const rule = editor.slice(editor.indexOf(' private renderSizingRule('), editor.indexOf(' /** Choose a part,'))
276
- assert.ok(rule.includes("asset.sizing === 'uniform'") && rule.includes("'figure.label.sizing-uniform'"), '선언하면 규칙을 말한다')
277
- assert.ok(rule.includes("turnsAboutAJoint(asset) ? html`<p hint>${i18next.t('figure.text.sizing-uniform-proposed')}"), '관절이 있으면 권한다')
278
- assert.ok(rule.includes("this.linkCommand({ kind: 'scale-uniformly' })"), '누르면 선언한다 — 사람이 정한다')
279
265
  })
280
266
 
281
267
 
@@ -304,6 +290,20 @@ test('붙일 때 면 위 어디에 — 그 면을 가로지르는 두 축만,
304
290
  assert.deepEqual(leaf.pose.t.map(Math.round), [500, 1000, 55], '문짝 아래가 문틀 아래에, 앞이 문틀 앞에')
305
291
  })
306
292
 
293
+ test('the fastening form asks across the face in rows only where the drawing cannot set it: no drawing, a field followed, a share of the figure size (2026-10-02)', () => {
294
+ let asset = createFigureAsset('Door')
295
+ asset = addFigureBox(asset, { id: 'jamb', size: { x: 100, y: 2100, z: 150 }, position: { x: 0, y: 1050, z: 0 } })
296
+ asset = addFigureBox(asset, { id: 'leaf', size: { x: 900, y: 2000, z: 40 }, position: { x: 600, y: 1000, z: 0 } })
297
+ const right = { part: 'jamb', face: 'right' }
298
+ const view = faceViewOf(revisionOf(asset), 'leaf', right)
299
+ assert.deepEqual(view?.axes, ['y', 'z'])
300
+ assert.deepEqual(alignRowsOf(asset, 'leaf', right, view, false), [], 'the drawing sets both')
301
+ assert.deepEqual(alignRowsOf(asset, 'leaf', right, undefined, false), [{ axis: 'y', edges: true }, { axis: 'z', edges: true }], 'with no drawing, both are asked')
302
+ assert.deepEqual(alignRowsOf(asset, 'leaf', right, view, false, { y: 'leaf.position.y' }), [{ axis: 'y', edges: true }], 'the axis that follows a position field')
303
+ const sized = applyFigureAuthoring(asset, { kind: 'declare-instance-size', range: { x: { min: 100, max: 5000 }, y: { min: 100, max: 5000 }, z: { min: 10, max: 500 } }, at: { x: 1000, y: 2100, z: 150 } })
304
+ assert.deepEqual(alignRowsOf(sized, 'leaf', right, faceViewOf(revisionOf(sized), 'leaf', right), true).map(r => r.axis), ['y', 'z'], 'the axes a share of the figure size may set')
305
+ })
306
+
307
307
  test('붙여도 제자리 — 폼은 지금 서 있는 자리를 가운데서 거리로 채운다 (문짝이 문틀 앞 100 mm 에서 가운데로 옮겨졌다, 09-26)', async () => {
308
308
  const { alignOf, placeKeptOf } = await import('../client/modeller/links.ts')
309
309
  const { compileFigureAsset } = await import('@hatiolab/figure-model')
@@ -331,14 +331,6 @@ test('붙여도 제자리 — 폼은 지금 서 있는 자리를 가운데서
331
331
  row = applyFigureAuthoring(row, { kind: 'attach', part: 'roller', face: 'bottom', to: { part: 'plate', face: 'top' } })
332
332
  row = applyFigureAuthoring(row, { kind: 'attach', part: 'cap', face: 'bottom', to: { part: 'roller', face: 'top' } })
333
333
  assert.deepEqual([hangsFrom(row, 'roller', 'plate'), hangsFrom(row, 'cap', 'plate'), hangsFrom(row, 'plate', 'roller')], [true, true, false])
334
- const form = read('../client/modeller/figure-links.ts')
335
- /* The target (a part or the plane, user 2026-10-01) and its face: the form is filled in whenever either changes. */
336
- assert.ok(form.includes("this.aimAt(asset, value === PLANE ? { ...f, toPlane: true, target: '' } : { ...f, toPlane: false, target: value })"), '상대를 바꿀 때마다')
337
- assert.ok(form.includes("this.aimAt(asset, { ...f, targetFace: c.targetFace as FigureFace })"), '상대 면을 바꿀 때마다')
338
- assert.ok(!form.includes("figure.label.stands-on-plane"), '부품 붙이기 폼에 모델 설치 기준을 섞지 않는다')
339
- /* Kept in the list, not choosable, with the reason (ADR-0097 §5.5). */
340
- assert.ok(form.includes('const loops = (one: string) => hangsFrom(asset, one, this.partId)'))
341
- assert.ok(form.includes("html`<option value=${one} disabled>${i18next.t('figure.text.target-would-loop'"))
342
334
  })
343
335
 
344
336
  test('따라 움직이는 관절 — 같은 방식으로 움직이고 제 조작값이 있는 관절만 고르고, 기록에는 배율과 더하기만 (집게 두 손가락, 09-25)', async () => {
@@ -563,8 +555,10 @@ test('벽 배치 — 장착 평면은 Z = 0, 등으로 붙이고 가로질러 X
563
555
  a = applyFigureAuthoring(a, { kind: 'declare-occupancy', proposal: (await import('@hatiolab/figure-model')).proposeFigureOccupancy(a), placement: 'wall' as never })
564
556
  assert.deepEqual(mountingPlaneOf(a), { normal: 'z', faces: ['back'] })
565
557
  assert.deepEqual(alignAxesOf(a, 'plate', 'plane').map(x => x.axis), ['x', 'y'])
566
- const form = read('../client/modeller/figure-links.ts')
567
- assert.ok(form.includes('(!f.toPlane || plane.faces.includes(face as FigureFace))'), '평면에 붙일 때는 평면이 받는 면만')
558
+ assert.deepEqual(ownFacesOf(a, 'plate', true), ['back'], 'on the plane, only the face the plane takes')
559
+ assert.deepEqual(ownFacesOf(a, 'plate', false), ['left', 'right', 'bottom', 'top', 'back', 'front', 'centre'], 'to a part, its six outer faces and its centre')
560
+ const floor = addFigureBox(createFigureAsset('Floor'), { id: 'plate', size: { x: 200, y: 300, z: 20 }, position: { x: 0, y: 0, z: 0 } })
561
+ assert.deepEqual(ownFacesOf(floor, 'plate', true), ['bottom', 'top'], 'on the floor, its bottom or its top')
568
562
  })
569
563
 
570
564
  test('부품 칸의 두 제목이 같은 말이 아니다 — 자리 숫자는 「위치와 크기」, 붙이기 칸은 「붙이기」 (09-26)', () => {
@@ -593,10 +587,6 @@ test('보이는 때 — 명령이 남긴 것을 읽고, 폼은 그 명령을 낸
593
587
  const shown = editFigurePart(asset, { kind: 'set-visibility', target: 'lamp', shownWhen: { input: 'gate-open', atLeast: 9 } })
594
588
  assert.deepEqual(shownWhenOf(shown, 'lamp'), { input: 'gate-open', atLeast: 9 })
595
589
  assert.equal(shownWhenOf(editFigurePart(shown, { kind: 'set-visibility', target: 'lamp', shownWhen: null }), 'lamp'), null)
596
- const form = read('../client/modeller/figure-links.ts')
597
- const section = form.slice(form.indexOf(' private renderShown('), form.indexOf(' private renderMotion('))
598
- assert.ok(section.includes("kind: 'set-visibility'"), '폼은 그 명령을 낸다')
599
- assert.ok(form.includes('${this.renderShown(asset)}'), '움직임 칸에')
600
590
  })
601
591
 
602
592
  /* 문틀은 폭 · 높이 · 깊이로 — 상자처럼 크기를 따르고, 손잡이로 키운다 (figure-model 의 portal 단면, 09-26 · ADR-0100). */
@@ -614,14 +604,6 @@ test('팔레트의 문틀은 문 크기로 서고, 세 축 모두 크기를 따
614
604
 
615
605
  /* 크기를 따라감 한 번 — 치수 칸의 단추 하나, 크기를 대신 정했으면 한 줄로 말한다 (ADR 판정 Q1, 09-26). */
616
606
  test('치수 칸 — 「도형 크기를 따라감」 단추는 follow-size 한 번, 다 따르면 사라진다; 크기를 대신 정하면 한 줄', () => {
617
- const form = read('../client/modeller/figure-links.ts')
618
- /* The whole method: the resize-with-placement block above the advanced settings made it longer than a fixed slice. */
619
- const dims = form.slice(form.indexOf(' private renderDimensions('), form.indexOf(' /** The same source, the same share'))
620
- assert.ok(dims.includes("this.command({ kind: 'follow-size', part: this.partId })"))
621
- assert.ok(dims.includes("followOf(asset, this.partId, axis).of === 'instance'"), '다 따르면 단추가 없다')
622
- const editor = read('../client/modeller/editor.ts')
623
- assert.ok(editor.includes("if (action.kind === 'follow-size' && !hadSize)"), '크기를 대신 정했을 때만')
624
- assert.ok(editor.includes("notify({ level: 'info', message: this.notice })"), '그 한 줄은 앱의 알림으로')
625
607
  const ko = JSON.parse(read('../translations/ko.json'))
626
608
  assert.match(ko['figure.text.size-declared-to-follow'], /\{x\} × \{y\} × \{z\} mm/)
627
609
  })
@@ -658,9 +640,8 @@ test('붙이기 칸은 부품 제 붙임으로 열린다 — 가운데로 붙은
658
640
  const bed = fasteningOf(revisionOf(asset), 'bed')!
659
641
  assert.deepEqual([bed.face, bed.toPlane], ['bottom', true])
660
642
  assert.equal(fasteningOf(revisionOf(addFigureBox(createFigureAsset('Loose'), { id: 'b', size: { x: 1, y: 1, z: 1 }, position: { x: 0, y: 0, z: 0 } })), 'b'), null, '안 붙은 부품은 기본값으로')
661
- const panel = readFileSync(fileURLToPath(new URL('../client/modeller/figure-links.ts', import.meta.url)), 'utf8')
662
- assert.ok(panel.includes('fasteningOf(this.revision, this.partId)'), '부품을 고르면 제 붙임으로 연다')
663
- assert.ok(panel.includes("...(f.toPlane ? [] : (['centre'] as const))"), '내 면에 가운데가 있다')
643
+ assert.equal(ownFacesOf(asset, 'roller', false).at(-1), 'centre', 'its own faces include its centre')
644
+ assert.ok(!ownFacesOf(asset, 'roller', true).includes('centre'), 'on the floor, by a face only')
664
645
  })
665
646
 
666
647
  test('a follower of several leaders and a stroke that is a sum of lengths are written back unchanged by the motion form (ADR-0107)', async () => {
package/test/live.test.ts CHANGED
@@ -4,8 +4,6 @@
4
4
  */
5
5
  import { test } from 'node:test'
6
6
  import assert from 'node:assert/strict'
7
- import { readFileSync } from 'node:fs'
8
- import { fileURLToPath } from 'node:url'
9
7
 
10
8
  import { addFigureBox, applyFigureAuthoring, compileFigureAsset, createFigureAsset } from '@hatiolab/figure-model'
11
9
 
@@ -13,8 +11,6 @@ import { liveOverridesOf, liveOverSession, livePartsOf, NOTHING_LIVE, scrubbed,
13
11
  import { moving } from './joints.ts'
14
12
  import { revisionOf } from './revision-of.ts'
15
13
 
16
- const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
17
-
18
14
  const slider = () => {
19
15
  let asset = createFigureAsset('Slider')
20
16
  asset = addFigureBox(asset, { id: 'block', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 } })
@@ -69,36 +65,6 @@ test('이름표를 끌면 값이 크기에 맞는 걸음으로 움직인다 —
69
65
  assert.equal(scrubbed(0.4, 3, 0.01), 0.43, '0.43000000000000005 가 아니다')
70
66
  })
71
67
 
72
- test('속성창 숫자 칸은 치는 동안 알리고, 이름표를 끌 수 있다 — 기록은 change 에서', () => {
73
- const editor = read('../client/modeller/editor.ts')
74
- assert.ok(editor.includes('@input=${(e: Event) => this.typing(group.cells[axis] as never, e)}'))
75
- assert.ok(editor.includes('@input=${(e: Event) => this.typing(field as never, e)}'))
76
- assert.ok(editor.includes('@pointerdown=${(e: PointerEvent) => this.scrub(group.cells[axis] as never, e, unavailable)}'))
77
- for (const event of ['@state-live=', '@states-live=', '@part-live=']) assert.ok(editor.includes(event), event)
78
- assert.ok(editor.includes('liveOverSession(shown, this.live)'), '캔버스는 세션 위에 끄는 값을 그린다 (행동은 아래 시험)')
79
- const run = editor.slice(editor.indexOf(' private run(action'), editor.indexOf(' private hold('))
80
- assert.ok(run.includes('this.live = NOTHING_LIVE'), '기록하거나 거절되면 끄는 값을 버린다')
81
- const material = read('../client/modeller/figure-material-panel.ts')
82
- assert.ok(material.includes('@input=${(e: Event) => this.glow(Number((e.target as HTMLInputElement).value), true)}'))
83
- })
84
-
85
- test('슬라이더와 클립 막대는 끄는 동안 알리고, 놓을 때 한 번 기록한다', () => {
86
- const controls = read('../client/modeller/figure-controls.ts')
87
- assert.ok(controls.includes('@input=${(e: Event) => this.drag(c, number(e))}') && controls.includes('@change=${(e: Event) => this.try(c, number(e))}'))
88
- assert.ok(controls.includes("new CustomEvent('state-live'"))
89
- const clips = read('../client/modeller/figure-clips.ts')
90
- assert.ok(clips.includes('@input=${(e: Event) => this.scrub(clip, number(e), true)}'))
91
- assert.ok(clips.includes("new CustomEvent('states-live'"))
92
- })
93
-
94
- test('클립 트랙의 머리는 이름 한 줄, 그 아래 반복 방식과 지우기 — 이름이 한 글자씩 쌓이지 않는다', () => {
95
- const clips = read('../client/modeller/figure-clips.ts')
96
- const head = clips.slice(clips.indexOf(' div[track-head] {'), clips.indexOf(' div[keys] {'))
97
- assert.ok(head.includes('display: grid;') && !head.includes('display: flex;'), '공통 스타일의 폭 100% 가 이름을 누르지 않는다')
98
- assert.ok(head.includes('grid-column: 1 / -1;'), '이름은 제 줄')
99
- assert.ok(head.includes('width: 30px;'), '지우기는 작은 버튼')
100
- })
101
-
102
68
  test('끄는 동안은 세션이 지금 든 자세 위에 그린다 — 다른 관절이 저장된 각도로 돌아가지 않는다', () => {
103
69
  /* Two joints: the author turned one with its control; then drags the other. */
104
70
  let asset = slider()
@@ -6,14 +6,10 @@
6
6
  */
7
7
  import { test } from 'node:test'
8
8
  import assert from 'node:assert/strict'
9
- import { readFileSync } from 'node:fs'
10
- import { fileURLToPath } from 'node:url'
11
9
 
12
10
  import { addFigureBox, addFigurePrimitive, compileFigureAsset, createFigureAsset, editFigurePart, figureCostOf } from '@hatiolab/figure-model'
13
11
 
14
- const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
15
- const material = read('../client/modeller/figure-material-panel.ts')
16
- const editor = read('../client/modeller/editor.ts')
12
+ import { glowOf, materialEditOf, offeredPaletteOf } from '../client/modeller/material-edit.ts'
17
13
 
18
14
  const probe = () => {
19
15
  let asset = createFigureAsset('Probe')
@@ -41,12 +37,16 @@ test('재질 칸이 보내는 모양 그대로 figure-model 이 받고, 그려
41
37
  })
42
38
 
43
39
  test('발광 세기 0 은 발광을 없앤다 — 빈 칸을 남기지 않는다(형식이 undefined 칸을 거절한다)', () => {
44
- assert.match(material, /emissive: intensity > 0 \? \{[^}]*\} : undefined/)
45
- assert.match(material, /if \(material\[key\] === undefined\) delete material\[key\]/)
40
+ const glowing = { token: 'palette.primary', transparent: true, emissive: { token: 'palette.accent', intensity: 0.5, on: true } }
41
+ const palette = { 'palette.primary': '#477b9e', 'palette.accent': '#f28c28' }
42
+ const off = materialEditOf('body', glowing, { ...glowOf(glowing, 0), transparent: undefined }, palette, palette) as { material: Record<string, unknown> }
43
+ assert.deepEqual(off.material, { token: 'palette.primary' }, 'no glow and no empty field left')
44
+ assert.equal(drawn(editFigurePart(probe(), off as never), 'body').material.emissive, undefined, 'figure-model takes it')
45
+ assert.deepEqual(glowOf({ token: 'palette.primary' }, 0.4), { emissive: { token: 'palette.primary', intensity: 0.4, on: true } }, 'a new glow takes the part\'s own colour')
46
+ assert.equal((glowOf(glowing, 0.4).emissive as { token: string }).token, 'palette.accent', 'a glow keeps the colour picked for it')
46
47
  })
47
48
 
48
49
  test('곡면 분할은 둥근 몸체에만 내고, 고르면 삼각형 수가 그만큼 바뀐다', async () => {
49
- assert.match(material, /if \(!isRoundBody\(/, 'the panel asks the one rule')
50
50
  const { isRoundBody } = await import('../client/modeller/starting-shape.ts')
51
51
  const { boxShape, discShape } = await import('../client/modeller/drawn-shapes.ts')
52
52
  assert.equal(isRoundBody(discShape(50, 10) as never), true, 'a circle pushed is round')
@@ -57,23 +57,14 @@ test('곡면 분할은 둥근 몸체에만 내고, 고르면 삼각형 수가
57
57
  assert.ok(fine > coarse, `48 분할이 8 분할보다 삼각형이 많다 (${coarse} → ${fine})`)
58
58
  })
59
59
 
60
- test('색 역할은 날것의 이름이 아니라 쓰임새로 부른다', () => {
61
- assert.match(material, /title=\$\{purposeOf\(token\)\}/)
62
- assert.doesNotMatch(editor, /token === 'palette\.primary' \? '기본 몸체' : token/, '옛 날것 이름 고르개가 남아 있다')
63
- })
64
-
65
- test('편집기는 재질 칸의 편집을 세션의 이력 하나로 돌린다', () => {
66
- assert.match(editor, /<figure-material-panel[\s\S]*?@part-edit=\$\{\(e: CustomEvent\) => this\.run\(\(\) => this\.session!\.edit\(e\.detail\.edit\)\)\}/)
67
- })
68
-
69
60
  test('자산에 없는 색 역할은 그 색을 함께 보낸다 — 새 도형의 팔레트는 기본색 하나다', () => {
70
- assert.match(material, /return \{ \.\.\.\(activePalette\(\) as Record<string, string>\), \.\.\.this\.palette \}/, '앱 팔레트를 보인다')
71
- assert.match(material, /this\.palette\[token\] === undefined && offered\[token\] !== undefined/)
72
- assert.match(material, /\.\.\.\(Object\.keys\(palette\)\.length \? \{ palette \} : \{\}\)/)
73
- })
74
-
75
- test('분할 수 — 흔한 넷은 다각형 윤곽과 삼각형 수로, 나머지는 접어 둔다', () => {
76
- const source = readFileSync(fileURLToPath(new URL('../client/modeller/figure-material-panel.ts', import.meta.url)), 'utf8')
77
- assert.ok(source.includes('SEGMENT_PRESETS.slice(0, 4).map(') && source.includes('${silhouette(preset)}'))
78
- assert.ok(source.includes('<details free') && source.includes('SEGMENT_PRESETS.slice(4)'))
61
+ const asset = probe()
62
+ const offered = offeredPaletteOf({ 'palette.primary': '#000000', 'palette.accent': '#f28c28', 'palette.dark': '#2b2b2b' }, asset.palette)
63
+ assert.equal(offered['palette.primary'], asset.palette['palette.primary'], '자산의 색이 앱의 색 위에 선다')
64
+ assert.equal(offered['palette.accent'], '#f28c28', '앱 팔레트를 보인다')
65
+ const accent = materialEditOf('body', { token: 'palette.primary' }, { token: 'palette.accent', emissive: { token: 'palette.dark', intensity: 0.5, on: true } }, asset.palette, offered) as { palette?: Record<string, string> }
66
+ assert.deepEqual(accent.palette, { 'palette.accent': '#f28c28', 'palette.dark': '#2b2b2b' }, '몸체 색과 발광 색 모두')
67
+ assert.equal(drawn(editFigurePart(asset, accent as never), 'body').material.token, 'palette.accent', 'figure-model 이 받는다')
68
+ const own = materialEditOf('body', { token: 'palette.accent' }, { token: 'palette.primary' }, asset.palette, offered)
69
+ assert.equal('palette' in own, false, '자산에 있는 색이면 보낼 것이 없다')
79
70
  })
@@ -19,6 +19,7 @@ import {
19
19
  curveLayout,
20
20
  decadeSteps,
21
21
  GRADE_SCALE,
22
+ gradeBoundsOf,
22
23
  gradeOf,
23
24
  FIGURE_BUDGET_KEYS,
24
25
  figureReportOf
@@ -204,14 +205,17 @@ test('추이 — 1 에서 1만까지 1·2·5, 삼각형과 draw call 은 오르
204
205
  assert.deepEqual(layout.xTicks.map(t => t.label), ['1', '10', '100', '1k', '10k'])
205
206
  })
206
207
 
207
- test('화면은 리포트의 차례로 그린다 — 개수 · 축 · 한도 · 그리는 비용 · 추이 · 발행 판정', () => {
208
- const order = ['this.renderVerdict(report)', 'this.renderAxes(report)', 'this.renderBudgets(report)', 'this.renderMetrics(report)', 'this.renderCurve()', 'this.renderGate()']
209
- // From the full report, past the early return that shows only guidance and the release verdict.
210
- const at = order.map(call => verdict.indexOf(call, verdict.indexOf('${this.renderVerdict(report)}')))
211
- assert.ok(at.every(i => i > 0), `모든 절을 그린다: ${order.filter((_, i) => at[i] < 0)}`)
212
- assert.deepEqual([...at].sort((a, b) => a - b), at)
213
- assert.ok(verdict.includes('...GRADE_SCALE.map('), '눈금은 등급을 셈한 경계에서 그린다 — 따로 적으면 어긋난다')
214
- assert.ok(verdict.includes('SidePanelStyles'), '옆 칸의 한 모양을 쓴다')
208
+ test('눈금은 등급을 셈한 경계에서 그린다 — 따로 적으면 어긋난다', () => {
209
+ /* The order of the tab's sections, and that it draws this scale, are the browser check's (test/browser/screen.check.mjs). */
210
+ const bounds = gradeBoundsOf()
211
+ assert.deepEqual(bounds.map(([grade]) => grade), ['A', 'B', 'C', 'D'])
212
+ for (const [grade, said] of bounds.slice(0, -1)) {
213
+ const bound = Number(/^≤ ([\d.]+)%$/.exec(said)![1]) / 100
214
+ assert.equal(gradeOf(bound), grade, `${grade} ${said}: 경계에서는 그 등급`)
215
+ assert.notEqual(gradeOf(bound + 0.001), grade, `${grade} ${said}: 경계를 넘으면 다음 등급`)
216
+ }
217
+ assert.deepEqual(bounds.at(-1), ['D', '> 100%'])
218
+ assert.equal(gradeOf(1.001), 'D')
215
219
  })
216
220
 
217
221
  test('radar 와 추이 그림의 조각은 svg 템플릿이다 — html 로 만들면 소리 없이 안 그려진다', () => {
@@ -252,12 +256,5 @@ test('부품 목록 머리의 값 한 줄 — 삼각형 · 묶음 · 한 화면
252
256
  const over = budgetOf(figureReportOf(glass))!
253
257
  assert.equal(over.grade, 'D')
254
258
  assert.deepEqual(over.tightest, { key: 'transparent', used: 2, limit: 1 })
255
-
256
- const parts = read('../client/modeller/figure-parts.ts')
257
- const drawn = parts.slice(parts.indexOf(' private renderBudget()'), parts.indexOf(' /** 한꺼번에 감추고'))
258
- assert.ok(drawn.includes("const over = b.grade === 'D'") && drawn.includes("'figure.text.parts-budget-over'"), 'D 에서 넘친 한도를 적는다')
259
- assert.ok(parts.includes("new CustomEvent('show-maturity'"), '누르면 성숙도로')
260
- const editor = read('../client/modeller/editor.ts')
261
- assert.ok(editor.includes('.budget=${this.budget}') && editor.includes("@show-maturity=${() => (this.tab = 'verdict')}"))
262
- assert.ok(verdict.includes('budget: budgetOf(report)'), '성숙도 탭이 같은 보고서에서 알린다')
259
+ /* The line drawn in the parts list, and its click to the verdict tab, are the browser check's (test/browser/screen.check.mjs). */
263
260
  })
@@ -0,0 +1,76 @@
1
+ /*
2
+ * A GLB made into a mesh part (ADR-0129): the whole file or one node of it, at 1:1, standing where it stood in the file,
3
+ * with what the server read of it. The kernel side (mesh@1) is tested in figure-model; the server's reading in
4
+ * figure-service (figure-glb, figure-mesh-db).
5
+ */
6
+ import { test } from 'node:test'
7
+ import assert from 'node:assert/strict'
8
+ import { readFileSync } from 'node:fs'
9
+ import { fileURLToPath } from 'node:url'
10
+
11
+ import { meshChoicesOf, meshPartIdOf, meshPartOf, type MeshUpload } from '../client/modeller/mesh-import.js'
12
+
13
+ const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
14
+ const HASH = 'c'.repeat(64)
15
+ const box = (x0: number, y0: number, z0: number, x1: number, y1: number, z1: number) => ({ min: { x: x0, y: y0, z: z0 }, max: { x: x1, y: y1, z: z1 } })
16
+
17
+ /* A crane: a 6 m girder, a hoist hung 1 m along it and turned a quarter, a marker with no mesh, a sheared node. */
18
+ const upload: MeshUpload = {
19
+ hash: HASH,
20
+ summary: {
21
+ bounds: box(-3000, -800, -400, 3000, 200, 400),
22
+ triangles: 5000,
23
+ primitives: 4,
24
+ unit: 'm',
25
+ nodes: [
26
+ { index: 0, name: 'Girder', parent: null, bounds: box(-3000, -800, -400, 3000, 200, 400), triangles: 5000, primitives: 4, position: { x: 0, y: 0, z: 0 }, rotation: { x: 0, y: 0, z: 0 } },
27
+ { index: 1, name: 'Hoist Unit #2', parent: 0, bounds: box(-200, -300, -150, 200, 0, 150), triangles: 1200, primitives: 2, position: { x: 1000, y: -500, z: 0 }, rotation: { x: 0, y: 90, z: 0 } },
28
+ { index: 2, name: 'marker', parent: 0, bounds: null, triangles: 0, primitives: 0, position: { x: 0, y: 0, z: 0 }, rotation: { x: 0, y: 0, z: 0 }, refused: 'no-mesh' },
29
+ { index: 3, name: 'skewed', parent: 0, bounds: null, triangles: 10, primitives: 1, position: { x: 0, y: 0, z: 0 }, rotation: { x: 0, y: 0, z: 0 }, refused: 'sheared' }
30
+ ]
31
+ }
32
+ }
33
+
34
+ test('the whole file first, then each node that can be a part; no marker, nothing sheared', () => {
35
+ assert.deepEqual(meshChoicesOf(upload), [
36
+ { name: '', triangles: 5000 },
37
+ { node: 0, name: 'Girder', triangles: 5000 },
38
+ { node: 1, name: 'Hoist Unit #2', triangles: 1200 }
39
+ ])
40
+ })
41
+
42
+ test('the whole file: at 1:1 along its longest axis, at the origin, with the file named by its hash', () => {
43
+ const part = meshPartOf(upload, {}, 'crane') as any
44
+ assert.equal(part.kind, 'mesh')
45
+ assert.deepEqual(part.dimensions, { size: 6000 })
46
+ assert.deepEqual(part.mesh, { asset: HASH, format: 'glb', unit: 'm', bounds: upload.summary.bounds, triangles: 5000, primitives: 4 })
47
+ assert.deepEqual(part.position, { x: 0, y: 0, z: 0 })
48
+ assert.equal(part.rotation, undefined)
49
+ })
50
+
51
+ test('a node: its own bounds and counts, standing and turned where the file had it', () => {
52
+ const part = meshPartOf(upload, { node: 1 }, 'hoist') as any
53
+ assert.deepEqual(part.dimensions, { size: 400 })
54
+ assert.equal(part.mesh.node, 1)
55
+ assert.deepEqual(part.mesh.bounds, box(-200, -300, -150, 200, 0, 150))
56
+ assert.equal(part.mesh.triangles, 1200)
57
+ assert.deepEqual(part.position, { x: 1000, y: -500, z: 0 })
58
+ assert.deepEqual(part.rotation, { x: 0, y: 90, z: 0 })
59
+ assert.throws(() => meshPartOf(upload, { node: 3 }, 'x'), 'a node that cannot be a part is not made one')
60
+ })
61
+
62
+ test("a part's id from a node's name: letter-led and clean, another number when taken", () => {
63
+ assert.equal(meshPartIdOf('Hoist Unit #2', new Set()), 'Hoist-Unit-2')
64
+ assert.equal(meshPartIdOf('2nd beam', new Set()), 'nd-beam')
65
+ assert.equal(meshPartIdOf('###', new Set()), 'mesh')
66
+ assert.equal(meshPartIdOf('hoist', new Set(['hoist', 'hoist-2'])), 'hoist-3')
67
+ })
68
+
69
+ /* The screen itself -- the palette's button, the upload, the question, the step added -- is driven in browser/places-mesh.check.mjs. */
70
+ test('the mesh words are in every language', () => {
71
+ for (const lang of ['en', 'es', 'ja', 'ko', 'ms', 'zh']) {
72
+ const t = JSON.parse(read(`../translations/${lang}.json`))
73
+ for (const key of ['figure.button.import-mesh', 'figure.text.import-mesh-hint', 'figure.text.mesh-whole-file', 'figure.text.mesh-triangles', 'figure.title.choose-mesh-part'])
74
+ assert.ok(t[key], `${lang} ${key}`)
75
+ }
76
+ })
@@ -16,33 +16,17 @@ test('every figure and a new one open the one modeller', () => {
16
16
  })
17
17
 
18
18
  test('one modeller page at one address: the route and the shell table agree', () => {
19
+ /* The shell's router runs route.ts; this suite has no router to drive it with, so the import is read as it is written. */
19
20
  const route = read('../client/route.ts')
20
- assert.equal(route.match(/case 'figure-modeller':/g)?.length, 1)
21
21
  assert.match(route, /import\('\.\/pages\/figure-modeller-page'\)/)
22
22
  const config = read('../things-factory.config.js')
23
23
  assert.equal(config.match(/tagname: 'figure-modeller-page', page: 'figure-modeller'/g)?.length, 1)
24
24
  })
25
25
 
26
26
  test('the catalogue opens details first; editing is an explicit action in the detail dock', () => {
27
+ /* The catalogue page (its grid and the shell) is not driven here; the dock's edit button is, in product-page.check.mjs. */
27
28
  const list = read('../client/pages/figure-list-page.ts')
28
29
  assert.match(list, /openFigureDetail\(record\)/)
29
- const detail = read('../client/viewparts/figure-detail-dock.ts')
30
- assert.match(detail, /navigate\(modellerPathOf\(saved\)\)/)
31
30
  })
32
31
 
33
- test('reloading or closing the tab with unsaved work asks first — the shell does not ask canDeactivate then (09-26)', () => {
34
- const page = read('../client/pages/figure-modeller-page.ts')
35
- const hold = page.slice(page.indexOf('private holdUnsaved'), page.indexOf('connectedCallback()'))
36
- assert.match(hold, /session\.read\(\)\.dirty/, 'only when there is something unsaved')
37
- assert.match(hold, /event\.preventDefault\(\)/)
38
- assert.match(page, /addEventListener\('beforeunload', this\.holdUnsaved\)/)
39
- assert.match(page, /removeEventListener\('beforeunload', this\.holdUnsaved\)/)
40
- })
41
-
42
- test('기존 도형을 열면 rail 은 카탈로그를 가리킨다 — 「새 Figure」를 가리키지 않는다(2026-10-03)', async () => {
43
- const { readFileSync } = await import('node:fs')
44
- const page = readFileSync(new URL('../client/pages/figure-modeller-page.ts', import.meta.url), 'utf8')
45
- const context = page.slice(page.indexOf('get context()'), page.indexOf('render()', page.indexOf('get context()')))
46
- /* The rail's rule (@operato/menu menu-active.ts): a page that names its menu wins over a menu with the same page name. */
47
- assert.ok(context.includes("menu: view.id ? 'figure-list' : undefined"), 'id 가 있으면 카탈로그, 새 도형이면 이름을 대지 않는다')
48
- })
32
+ /* Reloading with unsaved work, and the rail on an opened figure, are checked on the page: test/browser/product-page.check.mjs. */
@@ -3,14 +3,13 @@
3
3
  */
4
4
  import { test } from 'node:test'
5
5
  import assert from 'node:assert/strict'
6
- import { readFileSync } from 'node:fs'
7
6
 
8
- const panel = readFileSync(new URL('../client/modeller/figure-history-panel.ts', import.meta.url), 'utf8')
7
+ import { packageHrefOf } from '../client/modeller/modeller-route.ts'
9
8
 
9
+ /* The history panel draws the link from this for each released version (product-page.check.mjs reads it there). */
10
10
  test('each released version links to the package door by its full reference, only once the figure has a full name', () => {
11
- const href = panel.slice(panel.indexOf('private packageHref'), panel.indexOf('private renderVersion'))
12
- assert.match(href, /if \(!this\.figureNamespace \|\| !this\.figureType\) return undefined/)
13
- assert.match(href, /`\$\{contextPath\}\/figure-package\?refs=\$\{encodeURIComponent\(`\$\{this\.figureNamespace\}\/\$\{this\.figureType\}@\$\{version\}`\)\}`/)
14
- const row = panel.slice(panel.indexOf('private renderVersion'), panel.indexOf('private renderKept'))
15
- assert.match(row, /<a package download href=/)
11
+ assert.equal(packageHrefOf('/domain/x', 'acme', 'agv', 3), '/domain/x/figure-package?refs=acme%2Fagv%403')
12
+ assert.equal(packageHrefOf('', 'acme', 'roller-conveyor', 12), '/figure-package?refs=acme%2Froller-conveyor%4012')
13
+ assert.equal(packageHrefOf('/domain/x', undefined, 'agv', 3), undefined, 'no namespace yet')
14
+ assert.equal(packageHrefOf('/domain/x', 'acme', null, 3), undefined, 'no type yet')
16
15
  })