@things-factory/figure-ui 10.1.147 → 10.1.149

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 (209) 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.mjs +6 -1
  141. package/test/browser/editing-page.js +224 -0
  142. package/test/browser/editing.check.mjs +550 -0
  143. package/test/browser/links-page.js +113 -0
  144. package/test/browser/links.check.mjs +568 -0
  145. package/test/browser/modeller.check.mjs +21 -0
  146. package/test/browser/parts-page.js +120 -0
  147. package/test/browser/parts.check.mjs +328 -0
  148. package/test/browser/places-mesh-page.js +36 -0
  149. package/test/browser/places-mesh.check.mjs +239 -0
  150. package/test/browser/product-page.check.mjs +682 -0
  151. package/test/browser/product-page.js +276 -0
  152. package/test/browser/screen-page.js +112 -0
  153. package/test/browser/screen-product-page.js +62 -0
  154. package/test/browser/screen.check.mjs +1003 -0
  155. package/test/browser/section-page.js +256 -0
  156. package/test/browser/section.check.mjs +719 -0
  157. package/test/browser/serve.mjs +20 -12
  158. package/test/carrier-declaration.test.ts +69 -0
  159. package/test/cavity-drawing.test.ts +2 -18
  160. package/test/clip-ai-proposal.test.ts +1 -5
  161. package/test/clip-consent.test.ts +0 -5
  162. package/test/clips.test.ts +0 -7
  163. package/test/command-preview.test.ts +0 -68
  164. package/test/controls.test.ts +0 -9
  165. package/test/detail-dock.test.ts +6 -25
  166. package/test/face-mapping.test.ts +1 -20
  167. package/test/fasten-drawing.test.ts +0 -23
  168. package/test/fields.test.ts +6 -33
  169. package/test/figure-assistant-surface.test.ts +32 -38
  170. package/test/first-minute.test.ts +3 -55
  171. package/test/first-save-suggest.test.ts +11 -8
  172. package/test/first-save.test.ts +4 -26
  173. package/test/gizmo.test.ts +0 -25
  174. package/test/joint-fan.test.ts +0 -10
  175. package/test/keys.test.ts +0 -7
  176. package/test/layout.test.ts +4 -73
  177. package/test/links.test.ts +48 -67
  178. package/test/live.test.ts +0 -34
  179. package/test/material.test.ts +17 -26
  180. package/test/maturity.test.ts +13 -16
  181. package/test/mesh-import.test.ts +76 -0
  182. package/test/modeller-route.test.ts +3 -19
  183. package/test/package-download.test.ts +6 -7
  184. package/test/parts-wiring.test.ts +9 -51
  185. package/test/preview.test.ts +5 -46
  186. package/test/properties.test.ts +11 -26
  187. package/test/proposal-record.test.ts +2 -19
  188. package/test/recipes.test.ts +0 -15
  189. package/test/refusals.test.ts +257 -151
  190. package/test/release-entry.test.ts +1 -9
  191. package/test/release-panel.test.ts +1 -22
  192. package/test/resize-with-placement.test.ts +0 -32
  193. package/test/revert-choice.test.ts +1 -22
  194. package/test/revert-keeping.test.ts +1 -12
  195. package/test/save-landed.test.ts +16 -14
  196. package/test/scene-canvas.test.ts +2 -13
  197. package/test/section-editor.test.ts +30 -83
  198. package/test/section-panel.test.ts +22 -17
  199. package/test/slot-layout-host.test.ts +117 -0
  200. package/test/theme.test.ts +5 -9
  201. package/test/verdict.test.ts +44 -41
  202. package/test/view.test.ts +5 -12
  203. package/translations/en.json +61 -1
  204. package/translations/es.json +61 -1
  205. package/translations/ja.json +61 -1
  206. package/translations/ko.json +61 -1
  207. package/translations/ms.json +61 -1
  208. package/translations/zh.json +61 -1
  209. package/test/thumbnail.test.ts +0 -29
@@ -93,12 +93,6 @@ test('fresh, width only, and all set back to fixed are three different answers',
93
93
  const following = applyFigureAuthoring(a, { kind: 'follow-size', part: 'box', axes: ['y'] })
94
94
  assert.deepEqual(resizeAxesOf(following, 'box'), { axes: ['y'], basis: 'chosen' })
95
95
  assert.deepEqual(resizeAxesOf(following, 'box', ['x']), { axes: ['x', 'y'], basis: 'chosen' })
96
- /* The screen: size response is no longer a disguised placement command. */
97
- const links = readFileSync(new URL('../client/modeller/figure-links.ts', import.meta.url), 'utf-8')
98
- const dimensions = links.slice(links.indexOf(' private renderDimensions('), links.indexOf(' /** The same source'))
99
- assert.ok(dimensions.includes("this.command({ kind: 'follow-size', part: this.partId })"))
100
- assert.ok(!dimensions.includes('resize-with-placement'), 'size response does not attach the part')
101
- assert.ok(!links.includes('figure.label.stands-on-plane'), 'the attachment form does not present a model-placement toggle')
102
96
  const ko = JSON.parse(readFileSync(new URL('../translations/ko.json', import.meta.url), 'utf-8'))
103
97
  assert.equal(ko['figure.button.follow-size'], '모델 크기에 맞추기')
104
98
  })
@@ -108,8 +102,6 @@ test('a part off the floor is not moved by choosing its size response', () => {
108
102
  a = applyFigureAuthoring(a, { kind: 'declare-occupancy', proposal: proposeFigureOccupancy(a), placement: 'floor' })
109
103
  const asked = planFigureResize(a, { part: 'shelf', axes: resizeAxesOf(a, 'shelf').axes })
110
104
  assert.deepEqual([asked.refusal?.code, asked.refusal?.mm], ['GAP_CHOICE', 500])
111
- const links = readFileSync(new URL('../client/modeller/figure-links.ts', import.meta.url), 'utf-8')
112
- assert.ok(!links.includes("@click=${() => ask('keep')}"), 'the UI does not offer a size command that also moves the part')
113
105
  })
114
106
 
115
107
  test('a field only the room reads is taken out of the part\'s position row and shown apart', () => {
@@ -122,9 +114,6 @@ test('a field only the room reads is taken out of the part\'s position row and s
122
114
  const { vectors } = groupFields(fields.filter(f => !roomOnly.includes(f.id)), 'box')
123
115
  const position = vectors.find(v => v.kind === 'vector' && v.name === 'position')!
124
116
  assert.ok(position.kind === 'vector' && position.cells.x && !position.cells.y && position.cells.z, 'X and Z still move it; Y is held by the floor')
125
- const editor = readFileSync(new URL('../client/modeller/editor.ts', import.meta.url), 'utf-8')
126
- assert.ok(editor.includes('${roomFields.length ? this.renderRoomOnly(roomFields, unavailable) : nothing}'))
127
- assert.ok(editor.includes('if (agreed) this.linkCommand(refit.step)'), 'the room is declared again only when the author confirms')
128
117
  })
129
118
 
130
119
  /*
@@ -145,8 +134,6 @@ test('the part component carries the face a size drag keeps, for each placement'
145
134
  assert.deepEqual(state.scaleAnchor, expected[mode])
146
135
  assert.equal(componentOf(body, {}, { width: 1000, height: 1000 })!.scaleAnchor, undefined, 'none given, none set')
147
136
  }
148
- const editor = readFileSync(new URL('../client/modeller/editor.ts', import.meta.url), 'utf-8')
149
- assert.ok(editor.includes('.anchors=${this.anchorsOf(asset)}'))
150
137
  })
151
138
 
152
139
  /* Designer 2026-09-27 (acceptance round 2): what the screen shows for a part a rule places, and what it withholds. */
@@ -161,16 +148,9 @@ test('a fastened part keeps its position and turn rows, with the numbers the rul
161
148
  assert.ok(rows.rotation && rows.position, 'the turn row stands though every turn axis is the joint\'s')
162
149
  const body = compileFigureAsset(resized).evaluate().geometry.find(g => g.id === 'box') as never
163
150
  assert.deepEqual(placementOf(body), { position: [20, 50, 30], rotation: [0, 0, 0] }, 'the numbers a held cell shows')
164
- const editor = readFileSync(new URL('../client/modeller/editor.ts', import.meta.url), 'utf-8')
165
- assert.ok(editor.includes('<input type="text" readonly .value=${given(axis)}'), 'a held cell shows its number, read-only')
166
- assert.ok(!editor.includes('value="—" disabled'), 'not a blank')
167
151
  })
168
152
 
169
153
  test('size response and attachment are separate screen paths', () => {
170
- const links = readFileSync(new URL('../client/modeller/figure-links.ts', import.meta.url), 'utf-8')
171
- assert.ok(links.includes('figure.button.fasten-start'))
172
- assert.ok(links.includes('figure.label.target-part-placeholder'))
173
- assert.ok(!links.includes('? this.renderHeld(asset)'))
174
154
  const ko = JSON.parse(readFileSync(new URL('../translations/ko.json', import.meta.url), 'utf-8'))
175
155
  assert.equal(ko['figure.text.held-on-plane'].replace('{face}', '아랫면').replace('{place}', '바닥').replace('{gap}', '0'), '아랫면을 바닥에 유지 중 (간격 0 mm)')
176
156
  })
@@ -181,10 +161,6 @@ test('a part fastened twisted has no face figure-model can keep for a drag, so t
181
161
  a = addFigureBox(a, { id: 'blade', size: { x: 300, y: 20, z: 60 }, position: { x: 0, y: 110, z: 0 } })
182
162
  a = applyFigureAuthoring(a, { kind: 'attach', part: 'blade', face: 'bottom', to: { part: 'hub', face: 'top' }, twist: 30 })
183
163
  assert.equal(figureScaleAnchorOf(a, 'blade'), undefined)
184
- const editor = readFileSync(new URL('../client/modeller/editor.ts', import.meta.url), 'utf-8')
185
- assert.ok(editor.includes('if (reach.scale && this.sizeDragUnsure(asset, id)) return { ...reach, scale: false }'), 'the tool is off')
186
- assert.ok(editor.includes("if (asset && this.sizeDragUnsure(asset, this.selected)) {"), 'the R key says why')
187
- assert.ok(editor.includes("result.scale.some(k => Math.abs(k - 1) > 1e-4) && this.sizeDragUnsure(asset, id)"), 'a drag that still arrives is not written')
188
164
  })
189
165
 
190
166
  /*
@@ -200,14 +176,6 @@ test('"open attachment settings" opens this part\'s fastening, as it is now, and
200
176
  assert.equal(form.toPlane, true, 'on the plane')
201
177
  assert.equal(form.face, 'bottom')
202
178
  assert.equal(form.gap, 100, 'the gap it keeps')
203
- const links = readFileSync(new URL('../client/modeller/figure-links.ts', import.meta.url), 'utf-8')
204
- const editor = readFileSync(new URL('../client/modeller/editor.ts', import.meta.url), 'utf-8')
205
- assert.ok(links.includes('@click=${() => this.openFastening(asset)}'))
206
- const open = links.slice(links.indexOf(' private async openFastening('), links.indexOf(' /**\n * What holds the part, said as its state'))
207
- assert.ok(open.includes('const own = fasteningOf(this.revision!, this.partId)'), 'the form is loaded from the fastening it has now')
208
- assert.ok(open.includes("querySelector<HTMLElement>('section[fastening]')"), 'the fastening section is what comes into view')
209
- assert.ok(!editor.includes("@open-fastening=${() => (this.tab = 'capability')}"), 'it does not switch to the capability tab')
210
- assert.ok(links.includes('<section fastening ?spotlit=${this.spotlit}'))
211
179
  })
212
180
 
213
181
  /*
@@ -9,29 +9,8 @@ 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('with edits not saved, the three are asked; without, the plain confirmation', () => {
13
- const panel = read('../client/modeller/figure-history-panel.ts')
14
- const revert = panel.slice(panel.indexOf(' private async revert('))
15
- assert.ok(revert.indexOf('if (this.dirty) {') < revert.indexOf('revertFigureVersion('), 'asked before anything is reverted')
16
- assert.ok(revert.includes("if (!(await this.settleUnsaved('revert', version))) return"), 'cancelled or not saved: nothing')
17
- const settle = panel.slice(panel.indexOf(' private async settleUnsaved('), panel.indexOf(' private async revert('))
18
- assert.ok(settle.includes('html`<figure-revert-choice .version=${version} .purpose=${purpose}></figure-revert-choice>`'))
19
- assert.ok(settle.includes('if (!choice) return false'), 'cancelled: nothing')
20
- const choice = read('../client/modeller/figure-revert-choice.ts')
21
- assert.ok(choice.includes("closePopup(this, 'discard' satisfies RevertChoice)") && choice.includes("closePopup(this, 'save' satisfies RevertChoice)"))
22
- assert.ok(choice.includes('slot="start" destructive'), 'letting edits go is marked as what it is')
23
- })
24
-
12
+ /* The three asked, and a save first that holds the revert, are checked on the page: test/browser/product-page.check.mjs. */
25
13
  test('saved first, and known to be -- or nothing is reverted', () => {
26
- const panel = read('../client/modeller/figure-history-panel.ts')
27
- const settle = panel.slice(panel.indexOf(' private async settleUnsaved('), panel.indexOf(' private async revert('))
28
- assert.ok(settle.includes("if (!saved?.ok) {\n this.failure = i18next.t('figure.text.revert-held-unsaved'"), 'a save not done holds the revert')
29
- assert.ok(settle.includes('return false\n }\n }\n return true'))
30
- const page = read('../client/pages/figure-modeller-page.ts')
31
- assert.ok(page.includes('.saveFirst=${this.saveBeforeRevert}'))
32
- const save = page.slice(page.indexOf(' private saveBeforeRevert'), page.indexOf(' private reverted'))
33
- assert.ok(save.includes("return this.session.read().dirty ? { ok: false } : { ok: true }"), 'ok only when the session says saved')
34
- assert.ok(save.includes("code === 'SAVE_UNREACHABLE' ? i18next.t('figure.text.save-unreachable')"), 'not known is said as not known')
35
14
  for (const lang of ['en', 'ja', 'ko', 'ms', 'zh', 'es']) {
36
15
  const t = JSON.parse(read(`../translations/${lang}.json`))
37
16
  for (const key of ['figure.text.revert-with-unsaved', 'figure.button.revert-saving', 'figure.button.revert-discarding', 'figure.text.revert-held-unsaved']) assert.ok(t[key], `${lang} ${key}`)
@@ -69,18 +69,7 @@ test('the source a revert expects is what the server is confirmed to hold -- not
69
69
  })
70
70
 
71
71
  test('the screen: reverting is on, sends the confirmed source and a request id, and lists the kept drafts apart', () => {
72
- const panel = read('../client/modeller/figure-history-panel.ts')
73
- assert.ok(!panel.includes('REVERT_KEEPS_REPLACED_DRAFT'), 'the gate is gone')
74
- const revert = panel.slice(panel.indexOf(' private async revert('), panel.indexOf(' private async undo('))
75
- /* Read after the unsaved edits are settled -- a save first moves it. */
76
- assert.ok(revert.indexOf('settleUnsaved(') < revert.indexOf('const expected = this.serverSource?.()'))
77
- assert.ok(revert.includes('if (expected === undefined) {'), 'not known: not reverted')
78
- assert.ok(revert.includes('revertFigureVersion(this.figureId!, version, requestId, expected)'))
79
- const undo = panel.slice(panel.indexOf(' private async undo('), panel.indexOf(' private async send('))
80
- assert.ok(undo.includes("this.dirty && !(await this.settleUnsaved('undo'))"), 'an undo asks about unsaved edits too')
81
- assert.ok(panel.includes('<section kept>'), 'kept drafts in their own section, not among the releases')
82
- assert.ok(panel.includes('${newest && !kept.restoredAt'), 'only the newest, not yet brought back, is offered')
83
- assert.ok(read('../client/pages/figure-modeller-page.ts').includes('.serverSource=${() => this.session.confirmedSource()}'))
72
+ /* The panel on the page -- the confirmed source sent, the same request asked again, the kept drafts -- is checked in product-page.check.mjs. */
84
73
  for (const lang of ['en', 'ja', 'ko', 'ms', 'zh', 'es']) {
85
74
  const t = JSON.parse(read(`../translations/${lang}.json`))
86
75
  for (const key of [
@@ -11,6 +11,7 @@ import { addFigureBox, createFigureAsset, serializeFigureAsset } from '@hatiolab
11
11
 
12
12
  import { createFigureProductSession } from '../client/modeller/product-session.js'
13
13
  import { thumbnailHashOf } from '../client/modeller/thumbnail-hash.js'
14
+ import { reached } from '../client/graphql/reached.js'
14
15
 
15
16
  const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
16
17
  /** A picture as the modeller sends one: a data URL. */
@@ -95,27 +96,28 @@ test('a real conflict, or no server at all: the failure stands and the edits sta
95
96
  assert.equal(t.state.writes, 0)
96
97
  })
97
98
 
98
- test('not reached is said as not known, not as not saved', () => {
99
- const store = read('../client/graphql/store.ts')
100
- assert.ok(read('../client/graphql/reached.ts').includes("if (lost) throw Object.assign(Error('서버에 연결하지 못했습니다.'), { code: unreachable, cause: error })"))
101
- assert.ok(read('../client/graphql/reached.ts').includes("unreachable = 'SAVE_UNREACHABLE'"), 'a save not reached is SAVE_UNREACHABLE')
102
- assert.ok(store.includes('await reached(client.mutate('), 'the save goes through it')
103
- assert.ok(store.includes('async readBack(id)'), 'every field a save writes, read back')
99
+ /* The page's side -- a save lost and read back, a save not known said as such -- is checked in the browser (test/browser/product-page.check.mjs). */
100
+ test('not reached is said as not known, not as not saved', async () => {
101
+ type Told = Error & { code?: string; cause?: unknown }
102
+ assert.equal(await reached(Promise.resolve('answered')), 'answered')
103
+ /* Lost on the way or on the way back: not known -- SAVE_UNREACHABLE unless the caller names its own. */
104
+ const lost = Object.assign(Error('Failed to fetch'), { networkError: { statusCode: 0 } })
105
+ await assert.rejects(reached(Promise.reject(lost)), (e: Told) => e.code === 'SAVE_UNREACHABLE' && e.cause === lost && e.message === '서버에 연결하지 못했습니다.')
106
+ const offline = new TypeError('Failed to fetch')
107
+ await assert.rejects(reached(Promise.reject(offline), 'REVERT_UNREACHABLE'), (e: Told) => e.code === 'REVERT_UNREACHABLE' && e.cause === offline)
108
+ /* A refusal keeps the server's code, so a conflict is told from a refusal on grounds. */
109
+ const conflict = Object.assign(Error('Response not successful'), { graphQLErrors: [{ message: '다른 편집에서 모델이 변경되었습니다.', extensions: { code: 'FIGURE_SOURCE_CONFLICT' } }] })
110
+ await assert.rejects(reached(Promise.reject(conflict)), (e: Told) => e.code === 'FIGURE_SOURCE_CONFLICT' && e.message === '다른 편집에서 모델이 변경되었습니다.')
111
+ const other = Error('something else')
112
+ await assert.rejects(reached(Promise.reject(other)), (e: Told) => e === other)
104
113
  const write = read('../../figure-service/server/service/figure/figure-write.ts')
105
114
  assert.ok(write.includes("return new GraphQLError(message, { extensions: { code: 'FIGURE_SOURCE_CONFLICT' } })"), 'the server says a conflict is one')
106
- const editor = read('../client/modeller/editor.ts')
107
- assert.ok(editor.includes("=== 'SAVE_UNREACHABLE' ? i18next.t('figure.text.save-unreachable')"))
108
115
  for (const lang of ['en', 'ja', 'ko', 'ms', 'zh', 'es']) assert.ok(JSON.parse(read(`../translations/${lang}.json`))['figure.text.save-unreachable'], lang)
109
116
  assert.match(JSON.parse(read('../translations/ko.json'))['figure.text.save-unreachable'], /저장됐는지 확인하지 못했습니다/)
110
117
  })
111
118
 
112
- /* A released figure made a draft by a change is said when it happens -- saved source, or category and tags. */
119
+ /* A released figure made a draft by a change is said when it happens -- saved source, or category and tags (product-page.check.mjs). */
113
120
  test('a released figure changed back to a draft is told, whichever change did it', () => {
114
- const page = read('../client/pages/figure-modeller-page.ts')
115
- assert.ok(page.includes("if (before === 'released' && after === 'draft') notify({ level: 'info', message: i18next.t('figure.text.released-now-draft') })"))
116
- assert.ok(page.includes('if (this.session.read().id === id) this.toldDemoted(this.row.state, figure.state)'), 'after a save, read back')
117
- assert.ok(page.includes('this.toldDemoted(this.row.state, result.figure.state)'), 'after category or tags')
118
- assert.ok(page.includes("...(result.figure.state ? { state: result.figure.state as 'draft' | 'released' } : {})"), 'and the badge follows')
119
121
  for (const lang of ['en', 'ja', 'ko', 'ms', 'zh', 'es']) assert.ok(JSON.parse(read(`../translations/${lang}.json`))['figure.text.released-now-draft'], lang)
120
122
  })
121
123
 
@@ -33,12 +33,8 @@ const box = (t: number[], r = turned(0)): FigureRenderPart => ({
33
33
  material: { token: 'palette.primary' }
34
34
  })
35
35
 
36
- test('things-scene 의 판을 편집 모드로 세우고, 보기 설정은 판의 상태 이름 그대로 넣는다', () => {
37
- assert.ok(canvas.includes("import { create, Model, niceGridStep } from '@hatiolab/things-scene'"))
38
- assert.match(canvas, /type: 'model-layer'/)
39
- assert.match(canvas, /mode: 1\n/, '편집 모드 — 시점 단추 · 기즈모 · 도움말 · 단축키')
40
- assert.ok(canvas.includes('...sceneLook(this.view ?? DEFAULT_VIEW)'))
41
- assert.ok(canvas.includes('pixelRatio: Math.min(3, (window.devicePixelRatio || 1) * 2)'), '두 배 표본')
36
+ test('things-scene 의 판을 세운다 — 제 장면을 짜지 않는다', () => {
37
+ /* The board in editing mode, the view settings and the supersampling it is given are the browser check's (test/browser/screen.check.mjs). */
42
38
  assert.doesNotMatch(canvas, /new THREE\.|WebGLRenderer|OrbitControls|TransformControls/, '제 장면을 짜지 않는다')
43
39
  })
44
40
 
@@ -77,10 +73,3 @@ test('돌린 몸체도 왕복한다 — 판에 적은 상태를 그대로 되읽
77
73
  assert.equal(changedKeys(at, { ...at }), null, '같은 값은 다시 넣지 않는다 — 씬이 메아리를 내지 않게')
78
74
  assert.deepEqual(changedKeys(at, { ...at, left: 1 }), { left: 1 })
79
75
  })
80
-
81
- test('고르기와 끌기는 이름으로 알리고, 스스로 적은 것의 메아리는 끌기로 읽지 않는다', () => {
82
- assert.ok(canvas.includes("root.on('selected'"))
83
- assert.ok(canvas.includes("if (this.writing) return"))
84
- assert.ok(canvas.includes("new CustomEvent('part-transformed', { detail: { id: part.id, result }"))
85
- assert.ok(canvas.includes('setTimeout(() => this.sync())'), '받지 못한 변경은 입력이 둔 자리로 돌아간다')
86
- })
@@ -12,17 +12,12 @@ import { fileURLToPath } from 'node:url'
12
12
  import { addFigurePrimitive, applyFigureAuthoring, compileFigureAsset, createFigureAsset } from '@hatiolab/figure-model'
13
13
  import type { FigureAsset, FigureEvaluatedShape, FigureSectionPoint } from '@hatiolab/figure-model'
14
14
 
15
- import { apexOver, extentOf, sectionOf as sectionOfWorking, workingIssuesOf as issuesOfWorking, workingOf as workingOfShape, insertAfter, issueWords, issuesOf, markedBy, movePoint, nextPointId, pointLabel, removePoint } from '../client/modeller/section-drawing.ts'
15
+ import { apexOver, drawnOf, extentOf, measureNoteOf, sectionActionOf, sectionOf as sectionOfWorking, sectionPointsWordsOf, workingIssuesOf as issuesOfWorking, workingOf as workingOfShape, insertAfter, issueWords, issuesOf, markedBy, movePoint, nextPointId, pointLabel, removePoint } from '../client/modeller/section-drawing.ts'
16
16
  import { livePartsOf } from '../client/modeller/live.ts'
17
17
  import { revisionOf } from './revision-of.ts'
18
18
 
19
19
  const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
20
- /* A source file is a big string: a failing assert.match on it spends minutes building the diff and looks hung. */
21
- const has = (source: string, re: RegExp, message?: string) => assert.ok(re.test(source), message ?? `missing: ${re}`)
22
- const hasNot = (source: string, re: RegExp, message?: string) => assert.ok(!re.test(source), message ?? `present: ${re}`)
23
20
  const editorPanel = read('../client/modeller/figure-section-editor.ts')
24
- const sectionPanel = read('../client/modeller/figure-section-panel.ts')
25
- const editor = read('../client/modeller/editor.ts')
26
21
  const ko = JSON.parse(read('../translations/ko.json')) as Record<string, string>
27
22
  const say = (key: string, params: Record<string, string>) => Object.entries(params).reduce((s, [k, v]) => s.replace(`{${k}}`, v), ko[key] ?? key)
28
23
 
@@ -112,31 +107,6 @@ test('치는 동안 입체는 그 점으로 그려지고, 잘못된 동안은
112
107
  const body = drawn.find(p => p.id === 'wedge') as unknown as { shape: FigureEvaluatedShape }
113
108
  assert.equal((body.shape.extrusion!.section as { points: unknown[] }).points.length, 4)
114
109
  assert.equal(pointsOf(t).length, 3, 'the figure itself is not changed')
115
- has(editorPanel, /private preview\(w = this\.lastValid\) \{\s*this\.dispatchEvent\(new CustomEvent\('command-live', \{ detail: \{ action: this\.action\(w\) \}/, 'the preview is always of a valid shape')
116
- has(editorPanel, /this\.preview\(c && this\.valid\(\{ \.\.\.this\.working, cavity: c \}\) \? \{ \.\.\.this\.working, cavity: c \} : this\.lastValid\)/, 'the one other: the inside being dragged, when valid')
117
- has(editorPanel, /figure\.text\.section-editor-last-valid/)
118
- })
119
-
120
- test('적용은 한 명령, 취소·Esc 는 아무것도 보내지 않는다 — 창을 연 뒤 도형이 바뀌면 적용을 막는다', () => {
121
- has(editorPanel, /private apply\(\) \{\s*if \(this\.stale \|\| !this\.valid\(this\.working\)\) return/)
122
- has(editorPanel, /private cancel\(\) \{\s*this\.dispatchEvent\(new CustomEvent\('command-live', \{ detail: \{ action: null \}/)
123
- has(editorPanel, /return this\.revision !== this\.openedAt/)
124
- has(editor, /<figure-section-editor[\s\S]*?\.revision=\$\{this\.session\?\.read\(\)\.revision \?\? 0\}[\s\S]*?@command=\$\{\(e: CustomEvent\) => this\.linkCommand\(e\.detail\.action\)\}/)
125
- has(editor,
126
- /protected willUpdate[\s\S]*?if \(this\.sectionEditing && this\.sectionEditing !== this\.selected\) this\.leaveSectionEditor\(this\.selected\)\s*\}/,
127
- 'the selection moved off the part by any way closes it and lets go of the preview'
128
- )
129
- has(editor, /if \(!editor\?\.dirty \|\| !stillThere\) \{\s*this\.closeSectionEditor\(\)/, 'nothing changed, or nothing to go back to: it just closes')
130
- has(editor, /private closeSectionEditor\(\) \{\s*this\.sectionEditing = null\s*this\.holdAction\(null\)/)
131
- has(sectionPanel, /section-editor-open/)
132
- })
133
-
134
- test('Delete 키는 칸 안에서는 글자를, 밖에서는 고른 점을 지운다 — 창이 먼저 듣고, 받은 키는 뒤의 장면에 넘기지 않는다', () => {
135
- has(editorPanel, /\(e\.key === 'Delete' \|\| e\.key === 'Backspace'\) && !inField && this\.selected/)
136
- /* The scene listens on its own element and stops the key there: heard later, Esc selected nothing and Delete took the part. */
137
- has(editorPanel, /window\.addEventListener\('keydown', this\.keys, true\)/)
138
- has(editorPanel, /const take = \(\) => \(e\.preventDefault\(\), e\.stopImmediatePropagation\(\)\)/)
139
- has(editorPanel, /if \(e\.key === 'Escape'\) \{\s*take\(\)\s*this\.requestClose\(\)/)
140
110
  })
141
111
 
142
112
  test('그림은 점을 다 담고 여유를 둔다 — 50 mm 단위로', () => {
@@ -154,11 +124,6 @@ test('편집 창의 말은 다섯 말 모두에 있다', () => {
154
124
  }
155
125
  })
156
126
 
157
- test('창의 네 줄은 줄 번호로 선다 — 넓은 화면에서 탭 줄이 숨어도 적용·취소가 밀려 나가지 않는다', () => {
158
- for (const [part, row] of [['header', 1], ['nav\\[tabs\\]', 2], ['div\\[body\\]', 3], ['footer', 4]] as const)
159
- has(editorPanel, new RegExp(`${part} \\{\\s*grid-row: ${row};`))
160
- })
161
-
162
127
  test('치는 동안의 명령이 쓴 입력은 명령의 값으로 그린다 — 지금 값이 옮긴 점·다시 친 반지름을 되돌리지 않는다', async () => {
163
128
  const { liveOverSession } = await import('../client/modeller/live.ts')
164
129
  const t = triangle()
@@ -174,12 +139,6 @@ test('치는 동안의 명령이 쓴 입력은 명령의 값으로 그린다 —
174
139
  assert.deepEqual((b.shape.extrusion!.section as { corner: unknown }).corner, { x: 80, z: 80 })
175
140
  })
176
141
 
177
- test('편집 창이 열린 동안 모델러의 키는 비켜선다 — Esc 가 선택을, Delete 가 부품을, Ctrl+Z 가 도형의 기록을 건드리지 않는다', () => {
178
- const onKey = editor.slice(editor.indexOf(' private onKey = '), editor.indexOf(' connectedCallback() {', editor.indexOf(' private onKey = ')))
179
- has(onKey, /if \(this\.sectionEditing\) return\s*const canvas/)
180
- assert.ok(onKey.indexOf('if (this.sectionEditing) return') < onKey.indexOf('keyActionOf('), 'before any key is taken')
181
- })
182
-
183
142
  test('속성의 형상 치수에는 점·꼭짓점 칸이 없다 (사용자 10-01) — 높이만 남고, 단면 칸이 점 개수를 말한다', async () => {
184
143
  const { groupFields } = await import('../client/modeller/fields.ts')
185
144
  const f = (id: string) => ({ id, value: 0, unit: 'mm' })
@@ -188,16 +147,14 @@ test('속성의 형상 치수에는 점·꼭짓점 칸이 없다 (사용자 10-0
188
147
  'tip.dimension.round', 'tip.dimension.wall', 'tip.dimension.closed.start', 'tip.dimension.wallX', 'tip.corner.x', 'tip.wall.x-']
189
148
  const { singles } = groupFields(ids.map(f) as never, 'tip')
190
149
  assert.deepEqual(singles.map(g => (g as { field: { id: string } }).field.id), ['tip.dimension.height'])
191
- has(sectionPanel, /e\.apex \? 'figure\.text\.section-points-apex' : 'figure\.text\.section-points', \{ points: String\(e\.section\.points\.length\) \}/)
192
- assert.equal(say('figure.text.section-points-apex', { points: '3' }), '점 3개 · 꼭짓점 하나')
193
- })
194
-
195
- test('편집 창 옆에 입체를 볼 자리가 없으면 윤곽과 입체 결과를 번갈아 본다 (§7)', async () => {
196
- const { NARROW_BELOW } = { NARROW_BELOW: Number(/export const NARROW_BELOW = (\d+)/.exec(editorPanel)![1]) }
197
- assert.ok(NARROW_BELOW > 560, 'wider than the editor itself')
198
- has(editorPanel, /:host\(\[narrow\]\) nav\[tabs\] \{\s*display: flex;/)
199
- has(editorPanel, /:host\(\[narrow\]\[show='solid'\]\) div\[body\] \{\s*display: none;/)
200
- has(editorPanel, /this\.room\.observe\(this\.parentElement\)/)
150
+ /* The 단면 group says how many points instead: a triangle three, a pyramid three and its apex; a box nothing. */
151
+ assert.deepEqual(sectionPointsWordsOf(drawnOf(shapeOf(triangle()))), { key: 'figure.text.section-points', params: { points: '3' } })
152
+ const pyramid = applyFigureAuthoring(triangle(), { kind: 'set-section', part: 'wedge', apex: { x: 0, z: 0 } })
153
+ const words = sectionPointsWordsOf(drawnOf(shapeOf(pyramid)))!
154
+ assert.equal(say(words.key, words.params), '점 3개 · 꼭짓점 하나')
155
+ const { addFigureBox } = await import('@hatiolab/figure-model')
156
+ const box = addFigureBox(createFigureAsset('B'), { id: 'wedge', size: { x: 400, y: 100, z: 300 }, position: { x: 0, y: 50, z: 0 } })
157
+ assert.equal(sectionPointsWordsOf(drawnOf(shapeOf(box))), null)
201
158
  })
202
159
 
203
160
  test('사각형·원도 같은 창에서 — 폭·지름·길이를 한 명령으로, 바꾸지 않은 치수는 보내지 않는다', async () => {
@@ -210,7 +167,9 @@ test('사각형·원도 같은 창에서 — 폭·지름·길이를 한 명령
210
167
  assert.deepEqual(sectionOf(shape, { ...w, across: { x: 500, z: 300 } }), { kind: 'rectangle', size: { x: 500, z: 300 } })
211
168
  assert.deepEqual(figureSectionMeasuresOf(box, 'b')!.measures, { y: 'editable', x: 'editable', z: 'editable' })
212
169
  /* The editor sends only what changed: a measure left alone may follow another value and must not be written. */
213
- has(editorPanel, /const across = Object\.fromEntries\(Object\.entries\(w\.across\)\.filter\(\(\[k, v\]\) => changed\(v, o\.across\[k as Axis\]\)\)\)/)
170
+ assert.deepEqual(sectionActionOf('b', 'rectangle', w, { ...w, across: { x: 500, z: 300 } }), { kind: 'set-section', part: 'b', across: { x: 500 } })
171
+ assert.deepEqual(sectionActionOf('b', 'rectangle', w, { ...w, length: 260 }), { kind: 'set-section', part: 'b', length: 260 })
172
+ assert.deepEqual(sectionActionOf('b', 'rectangle', w, w), { kind: 'set-section', part: 'b' }, 'nothing changed, nothing written')
214
173
  const next = applyFigureAuthoring(box, { kind: 'set-section', part: 'b', across: { x: 500 }, length: 260 })
215
174
  const b = (compileFigureAsset(next).evaluate().geometry.find(g => (g as { id: string }).id === 'b') as unknown as { shape: FigureEvaluatedShape; pose: { t: number[] } })
216
175
  assert.deepEqual(b.shape.extrusion!.section, { kind: 'rectangle', size: { x: 500, z: 300 } })
@@ -252,17 +211,20 @@ test('창 안에서 폭과 속 공간을 함께 — 넓힌 폭에서만 들어
252
211
  const next = applyFigureAuthoring(tray, { kind: 'set-section', part: 't', across: { x: 600, z: 400 }, cavity: thick })
253
212
  const t = (compileFigureAsset(next).evaluate().geometry.find(g => (g as { id: string }).id === 't') as unknown as { shape: FigureEvaluatedShape }).shape
254
213
  assert.deepEqual(t.cavity!.wall, thick.wall)
255
- /* The drawing in the editor is the panel's, its commands kept in the editor as working state, sent with the widths. */
256
- has(editorPanel, /<figure-cavity-drawing\s+\.body=\$\{\{ id: this\.body!\.id, shape \}\}/)
257
- has(editorPanel, /@command=\$\{\(ev: CustomEvent\) => \{\s*ev\.stopPropagation\(\)\s*const c = this\.cavityOf\(ev\.detail\.action\)\s*if \(c\) this\.change\(\{ \.\.\.this\.working, cavity: c \}\)/)
258
- has(editorPanel, /\.\.\.\(cavityMoved \? \{ cavity: w\.cavity \? \{ wall: \{ \.\.\.w\.cavity\.wall \}, start: w\.cavity\.start, end: w\.cavity\.end \} : null \} : \{\}\)/, 'only when changed')
259
- has(editorPanel, /private get hollowable\(\) \{\s*return !!this\.body\?\.shape\.extrusion && \(this\.kind === 'rectangle' \|\| this\.kind === 'ellipse'\)/, 'no inside for a drawn section')
214
+ /* The editor's one command carries the inside only when it changed: the widths alone leave it out; taken away, null. */
215
+ assert.deepEqual(sectionActionOf('t', 'rectangle', w, { ...w, across: { x: 600, z: 400 } }), { kind: 'set-section', part: 't', across: { x: 600, z: 400 } })
216
+ assert.deepEqual(sectionActionOf('t', 'rectangle', w, wide), { kind: 'set-section', part: 't', across: { x: 600, z: 400 }, cavity: thick })
217
+ assert.deepEqual(sectionActionOf('t', 'rectangle', w, { ...w, cavity: null }), { kind: 'set-section', part: 't', cavity: null })
260
218
  })
261
219
 
262
- test('길이를 바꿀 때 남는 면은 부품의 관계가 정한 것을 읽어 보여 준다 (§4.1) — 다른 값을 따르는 치수는 칸을 막는다', () => {
263
- has(editor, /const kept = figureScaleAnchorOf\(asset, this\.selected\)\?\.\[drawn\.axis\]/)
264
- has(editorPanel, /m\?\.\[this\.axis\] === 'linked' \? linked : this\.keptWords\(\)/)
265
- has(editorPanel, /m\?\.\[this\.axis\] !== 'editable'/)
220
+ test('치수 칸 밑의 말: 다른 값을 따르면 그렇다고, 길이는 관계가 남기는 면을 — 말할 것이 없으면 아무것도', () => {
221
+ assert.deepEqual(measureNoteOf('linked', 1, 'y'), { key: 'figure.text.section-editor-linked' }, 'following another value is said first')
222
+ assert.deepEqual(measureNoteOf('editable', 1, 'y'), { key: 'figure.text.section-editor-keeps', face: 'bottom' }, 'a floor part keeps its start: the bottom')
223
+ assert.deepEqual(measureNoteOf('editable', -1, 'y'), { key: 'figure.text.section-editor-keeps', face: 'top' })
224
+ assert.deepEqual(measureNoteOf('editable', -1, 'x'), { key: 'figure.text.section-editor-keeps', face: 'right' })
225
+ assert.deepEqual(measureNoteOf('editable', 0, 'y'), { key: 'figure.text.section-editor-keeps-centre' })
226
+ assert.equal(measureNoteOf('editable', undefined, 'y'), null, 'the relations cannot say: nothing')
227
+ assert.equal(measureNoteOf(undefined, undefined, 'x'), null, 'a width across says only that it follows')
266
228
  assert.equal(say('figure.text.section-editor-keeps', { face: ko['figure.label.face-bottom']! }), '아랫면 유지')
267
229
  })
268
230
 
@@ -312,43 +274,28 @@ test('편집 창에서 끝을 한 점으로: 켜면 점들의 가운데 위에
312
274
  const on = { ...w, apex: apexOver(w.points, 'y') }
313
275
  assert.deepEqual(on.apex, { z: 0, x: 0 })
314
276
  assert.deepEqual(issuesOfWorking(shape, on).words, [])
315
- /* The editor sends apex only when it changed, null to go back to a push. */
316
- has(editorPanel, /\.\.\.\(apexMoved \? \{ apex: w\.apex \? \{ \.\.\.w\.apex \} : null \} : \{\}\)/)
277
+ /* The editor sends apex only when it changed, null to go back to a push; a drawn section's points always go. */
278
+ assert.deepEqual(sectionActionOf('wedge', 'polygon', w, on), { kind: 'set-section', part: 'wedge', points: w.points, apex: { z: 0, x: 0 } })
279
+ assert.deepEqual(sectionActionOf('wedge', 'polygon', w, w), { kind: 'set-section', part: 'wedge', points: w.points })
317
280
  const pyramid = applyFigureAuthoring(wedge, { kind: 'set-section', part: 'wedge', apex: on.apex })
318
281
  const p = shapeOf(pyramid)
319
282
  assert.equal(p.provider, 'loft')
320
283
  const again = workingOfShape(p)
321
284
  assert.deepEqual(again.apex, { x: 0, z: 0 })
285
+ assert.deepEqual(sectionActionOf('wedge', 'polygon', again, { ...again, apex: null }).apex, null, 'turned off: back to a push')
286
+ assert.equal('apex' in sectionActionOf('wedge', 'polygon', again, { ...again, apex: { x: 0, z: 0 } }), false, 'the same apex is not sent')
322
287
  assert.deepEqual(sectionOfWorking(p, again), { kind: 'polygon', points: again.points })
323
288
  assert.equal(issuesOfWorking(p, { ...again, apex: { x: Number.NaN, z: 0 } }).words[0]!.key, 'figure.text.section-issue-number')
324
- /* Its length is its height: the same field, called so. */
325
- has(editorPanel, /this\.working\.apex \? 'figure\.label\.section-editor-height' : 'figure\.label\.section-editor-length'/)
326
289
  const back = applyFigureAuthoring(pyramid, { kind: 'set-section', part: 'wedge', apex: null })
327
290
  assert.equal(shapeOf(back).provider, 'extrude')
328
291
  })
329
292
 
293
+
330
294
  test('끝면에 붙은 부품이 있으면 한 점으로 모으기를 막고 그 면을 말한다 — 커널의 거절로 판정한다', () => {
331
- has(editor, /const apex = apexOver\(drawn\.section\.points, drawn\.axis\)[\s\S]*?session\.previewCommand\(\{ kind: 'set-section', part, apex \}\)/)
332
- has(editor, /\.code === 'FEATURE_MAPPING_REQUIRED'\)/)
333
- has(editorPanel, /const blocked = !apex && !this\.opened\?\.apex \? this\.pointBlocked : ''/)
334
- has(editorPanel, /\?disabled=\$\{!!blocked\}/)
335
295
  assert.equal(say('figure.text.section-editor-apex-blocked', { face: ko['figure.label.face-top']! }), '윗면에 붙은 부품이 있어 한 점으로 모을 수 없습니다.')
336
- /* A pyramid in the section panel: its editor opens, and it has no corners and no inside. */
337
- has(sectionPanel, /const e = body\?\.shape \? drawnOf\(body\.shape\) : undefined/)
338
296
  })
339
297
 
340
298
  test('닫기 전에 묻는다 (사용자 10-01): 바꾼 것이 있으면 적용·버리기·계속 편집 — 바꾼 것이 없으면 그냥 닫는다', () => {
341
- const choice = read('../client/modeller/figure-section-close-choice.ts')
342
- /* Esc and 취소 both ask; while it asks, the question has the keys. */
343
- has(editorPanel, /if \(e\.key === 'Escape'\) \{\s*take\(\)\s*this\.requestClose\(\)/)
344
- has(editorPanel, /<button @click=\$\{\(\) => this\.requestClose\(\)\}>\$\{i18next\.t\('figure\.button\.section-editor-cancel'\)\}/)
345
- has(editorPanel, /if \(this\.asking\) return\n/)
346
- has(editorPanel, /if \(!this\.dirty\) \{\s*this\.cancel\(\)\s*return true/)
347
- /* Apply is only offered when it would be taken: not for a refused shape, not after the figure changed. */
348
- has(editorPanel, /if \(choice === 'apply' && !held\)/)
349
- has(choice, /\$\{this\.held\s*\? nothing/)
350
- /* Picking another part while changes wait keeps the part selected until the editor closed. */
351
- has(editor, /this\.selected = editing\s*editor\.requestClose\(\)\.then\(closed => \{\s*if \(closed && this\.selected === editing\) this\.selected = next/)
352
299
  for (const lang of ['ko', 'en', 'ja', 'zh', 'ms', 'es']) {
353
300
  const t = JSON.parse(read(`../translations/${lang}.json`)) as Record<string, string>
354
301
  for (const key of ['figure.text.section-editor-close-changed', 'figure.text.section-editor-close-invalid', 'figure.button.section-editor-apply-and-close', 'figure.button.section-editor-discard', 'figure.button.keep-editing'])
@@ -16,10 +16,13 @@ import {
16
16
  cavityDepthOf,
17
17
  cavityRefusalOf,
18
18
  cavityRequestOf,
19
+ cavityRefusalWordsOf,
19
20
  cavityResultOf,
21
+ cornerActionOf,
20
22
  cornerLimitOf,
21
23
  cornerRefusalOf,
22
24
  defaultWallOf,
25
+ hollowableOf,
23
26
  wallLimitOf
24
27
  } from '../client/modeller/section-controls.ts'
25
28
  import { livePartsOf } from '../client/modeller/live.ts'
@@ -27,13 +30,9 @@ import { editRefusalOf } from '../client/modeller/refusals.ts'
27
30
  import { revisionOf } from './revision-of.ts'
28
31
 
29
32
  const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
30
- /* A source file is a big string: a failing assert.match on it spends minutes building the diff and looks hung. */
31
- const has = (source: string, re: RegExp, message?: string) => assert.ok(re.test(source), message ?? `missing: ${re}`)
32
- const hasNot = (source: string, re: RegExp, message?: string) => assert.ok(!re.test(source), message ?? `present: ${re}`)
33
33
  const panel = read('../client/modeller/figure-section-panel.ts')
34
34
  /* The inside is drawn and typed in its own element (user 2026-10-01): its words and limits are read there. */
35
35
  const drawing = read('../client/modeller/figure-cavity-drawing.ts')
36
- const editor = read('../client/modeller/editor.ts')
37
36
  const ko = JSON.parse(read('../translations/ko.json')) as Record<string, string>
38
37
 
39
38
  const probe = () => addFigureBox(createFigureAsset('Probe'), { id: 'body', size: { x: 400, y: 200, z: 300 }, position: { x: 0, y: 100, z: 0 } })
@@ -56,7 +55,10 @@ test('모서리 반지름은 네 모서리를 한 값으로 둥글게 하고, 0
56
55
  const sharp = applyFigureAuthoring(round, { kind: 'set-section-corner', part: 'body', corner: null })
57
56
  assert.equal(shapeOf(sharp).extrusion!.section.kind, 'rectangle')
58
57
  assert.equal('corner' in shapeOf(sharp).extrusion!.section, false)
59
- has(panel, /corner: value > 0 \? value : null/, 'the field sends 0 as no rounding')
58
+ /* The field's one command: a radius rounds, 0 takes the rounding away -- and the kernel takes both. */
59
+ assert.deepEqual(cornerActionOf('body', 40), { kind: 'set-section-corner', part: 'body', corner: 40 })
60
+ assert.deepEqual(cornerActionOf('body', 0), { kind: 'set-section-corner', part: 'body', corner: null })
61
+ assert.equal('corner' in shapeOf(applyFigureAuthoring(round, cornerActionOf('body', 0))).extrusion!.section, false)
60
62
  })
61
63
 
62
64
  test('속을 비우면 기본 벽·막힌 바닥·열린 윗면의 쟁반이 되고, 끝과 벽을 하나씩 바꾼다', () => {
@@ -90,9 +92,6 @@ test('벽이 단면을 다 채우거나 모서리가 변의 절반을 넘으면,
90
92
  })
91
93
 
92
94
  test('편집기는 단면 칸의 명령을 세션의 이력 하나로 돌리고, 치는 동안의 값은 그리기만 한다', () => {
93
- has(editor, /<figure-section-panel[\s\S]*?@command=\$\{\(e: CustomEvent\) => this\.linkCommand\(e\.detail\.action\)\}/)
94
- has(editor, /@command-live=\$\{\(e: CustomEvent\) => this\.holdAction\(e\.detail\.action\)\}/)
95
- has(editor, /private linkCommand\(action: FigureAuthoringAction, from: 'section-editor' \| 'panel' = 'panel'\): boolean \{\s*\/\*[^*]*\*\/\s*this\.holdAction\(null\)/, 'a command sent lets go of the one in hand')
96
95
  for (const lang of ['ko', 'en', 'ja', 'zh', 'ms', 'es']) {
97
96
  const words = JSON.parse(read(`../translations/${lang}.json`)) as Record<string, string>
98
97
  const used = [...new Set([...(panel + drawing).matchAll(/'(figure\.(?:label|text)\.section-[\w-]+|figure\.label\.group-section)'/g)].map(m => m[1]!))]
@@ -139,7 +138,6 @@ test('속 공간은 무엇이 되었는지와 깊이로 보인다 — 한쪽이
139
138
  assert.equal(cavityDepthOf(tray), 170, 'the 200 mm push less its 30 mm floor')
140
139
  assert.equal(cavityResultOf({ ...tray.cavity!, start: 'open' }), 'pipe')
141
140
  assert.equal(cavityResultOf({ ...tray.cavity!, end: { closed: 5 } }), 'sealed')
142
- has(panel, /figure\.text\.section-depth/)
143
141
  })
144
142
 
145
143
  test('치는 동안의 값은 사본에 그려지고 원본은 그대로다 — 받을 수 없는 값은 그리지 않는다', () => {
@@ -151,9 +149,16 @@ test('치는 동안의 값은 사본에 그려지고 원본은 그대로다 —
151
149
  assert.equal(livePartsOf(asset, { values: {}, action: { kind: 'set-section-corner', part: 'body', corner: 999 } }), null)
152
150
  })
153
151
 
154
- test('다각 단면에는 속 공간을 만들 수 없다고 그 자리에서 말한다', () => {
152
+ test('다각 단면에는 속 공간을 만들 수 없다고 그 자리에서 말한다', async () => {
155
153
  /* A drawn section has no inside control at all, rather than a line saying it cannot have one (user 2026-10-01). */
156
- has(panel, /kind === 'rectangle' \|\| kind === 'ellipse' \? this\.renderCavity\(body\) : nothing/)
154
+ const { addFigurePrimitive } = await import('@hatiolab/figure-model')
155
+ const drawn = addFigurePrimitive(createFigureAsset('D'), { id: 'body', kind: 'polygon', dimensions: { height: 100 }, points: [{ x: -50, z: -40 }, { x: 50, z: -40 }, { x: 0, z: 40 }], position: { x: 0, y: 50, z: 0 } })
156
+ const pyramid = applyFigureAuthoring(drawn, { kind: 'set-section', part: 'body', apex: { x: 0, z: 0 } })
157
+ const tube = addFigurePrimitive(createFigureAsset('C'), { id: 'body', kind: 'cylinder', dimensions: { radius: 50, length: 200 }, position: { x: 0, y: 0, z: 0 } })
158
+ assert.equal(hollowableOf(shapeOf(probe())), true, 'a pushed rectangle')
159
+ assert.equal(hollowableOf(shapeOf(tube)), true, 'a pushed circle')
160
+ assert.equal(hollowableOf(shapeOf(drawn)), false, 'a drawn section')
161
+ assert.equal(hollowableOf(shapeOf(pyramid)), false, 'a pyramid')
157
162
  })
158
163
 
159
164
  test('붙어 있는 것이 기대는 면을 없애는 편집은 거절되고, 그 면을 사람의 말로 부른다', () => {
@@ -164,11 +169,6 @@ test('붙어 있는 것이 기대는 면을 없애는 편집은 거절되고,
164
169
  assert.equal(said, say('figure.text.section-face-in-use', { faces: ko['figure.label.face-inside-bottom']! }))
165
170
  })
166
171
 
167
- test('치는 동안 그려진 도형이 칸의 글자를 덮어쓰지 않는다 — 친 것은 보낼 때까지 칸이 든다', () => {
168
- has(panel, /\.value=\$\{live\(this\.typed\[key\] \?\? shown\)\}/)
169
- has(panel, /this\.typed = \{ \.\.\.this\.typed, \[key\]: \(ev\.target as HTMLInputElement\)\.value \}/)
170
- })
171
-
172
172
  test('면마다 벽 두께를 치는 동안에도 그려진다 — 명령이 없앤 입력(한 벽 두께)의 지금 값은 미리보기를 막지 않는다', async () => {
173
173
  const { liveOverSession } = await import('../client/modeller/live.ts')
174
174
  const tray = applyFigureAuthoring(probe(), { kind: 'set-cavity', part: 'body', cavity: { wall: 30, start: { closed: 30 }, end: 'open' } })
@@ -194,5 +194,10 @@ test('한 면의 벽 한도는 그 방향의 너비에서 맞은편 벽을 뺀
194
194
  /* The kernel agrees: just under takes, the limit refuses. */
195
195
  assert.equal(cavityRefusalOf(shape, cavityRequestOf(shape, { wall: { ...walls, 'x-': 359 } })), null)
196
196
  assert.equal(cavityRefusalOf(shape, cavityRequestOf(shape, { wall: { ...walls, 'x-': 360 } }))!.code, 'wall-too-thick')
197
- has(drawing, /this\.perSide && side \? mm\(sideWallLimitOf\(this\.body!\.shape, w\.wall, side\)\) : ask\.max/)
197
+ /* The drawing says that side's most with the walls one per side, and the kernel's all-round most otherwise. */
198
+ const over = { wall: { ...walls, 'x-': 380 }, start: { closed: 30 }, end: 'open' as const }
199
+ assert.deepEqual(cavityRefusalWordsOf(shape, over, true, 'x-'), { key: 'figure.text.section-wall-over', params: { max: '360' } })
200
+ assert.deepEqual(cavityRefusalWordsOf(shape, over, false, 'x-'), { key: 'figure.text.section-wall-over', params: { max: String(wallLimitOf(shape)) } })
201
+ assert.deepEqual(cavityRefusalWordsOf(shape, { ...over, wall: walls, start: { closed: 120 }, end: { closed: 90 } }, false), { key: 'figure.text.section-ends-over', params: { max: '200' } })
202
+ assert.equal(cavityRefusalWordsOf(shape, { ...over, wall: walls }, true, 'x-'), null)
198
203
  })