@things-factory/figure-ui 10.1.146 → 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
@@ -31,19 +31,27 @@ export async function openPage(bundle, { width = 1400, height = 900 } = {}) {
31
31
  }
32
32
  })
33
33
  await new Promise(done => server.listen(0, done))
34
- const browser = await puppeteer.launch({ headless: true, args: ['--no-sandbox'] })
35
- const page = await browser.newPage()
36
- await page.setViewport({ width, height })
37
- const errors = []
38
- page.on('pageerror', e => errors.push(String(e)))
39
- await page.goto(`http://localhost:${server.address().port}/`)
40
- await page.waitForFunction(() => !!window.harness, { timeout: 30000 })
41
- return {
42
- page,
43
- errors,
44
- async close() {
45
- await browser.close()
34
+ let browser
35
+ async function close() {
36
+ try {
37
+ await browser?.close()
38
+ } finally {
46
39
  server.close()
47
40
  }
48
41
  }
42
+ /* A page that never comes up must not leave the browser and the server behind: the caller gets no handle to close,
43
+ and the test process would never end. */
44
+ try {
45
+ browser = await puppeteer.launch({ headless: true, args: ['--no-sandbox'] })
46
+ const page = await browser.newPage()
47
+ await page.setViewport({ width, height })
48
+ const errors = []
49
+ page.on('pageerror', e => errors.push(String(e)))
50
+ await page.goto(`http://localhost:${server.address().port}/`)
51
+ await page.waitForFunction(() => !!window.harness, { timeout: 30000 })
52
+ return { page, errors, close }
53
+ } catch (e) {
54
+ await close()
55
+ throw e
56
+ }
49
57
  }
@@ -0,0 +1,69 @@
1
+ /*
2
+ * The properties tab's carrier field (ADR-0123): what it sends as `set-carrier`, what it says when the kernel would
3
+ * refuse, and that the declaration is a step of the session's history -- undo takes it back, the save carries it.
4
+ */
5
+ import { test } from 'node:test'
6
+ import assert from 'node:assert/strict'
7
+ import { addFigureBox, createFigureAsset, parseFigureAsset, serializeFigureAsset } from '@hatiolab/figure-model'
8
+
9
+ import { carrierCommandOf, carrierDraftOf, draftIsDeclared, type CarrierDraft } from '../client/modeller/carrier-declaration.ts'
10
+ import { createFigureProductSession } from '../client/modeller/product-session.ts'
11
+
12
+ const say = (key: string, params: Record<string, string>) => `${key}${Object.keys(params).length ? ' ' + JSON.stringify(params) : ''}`
13
+ const draft = (type: string, width = '1000', length = '600', height = '200'): CarrierDraft => ({ type, width, length, height })
14
+
15
+ const source = serializeFigureAsset(addFigureBox(createFigureAsset('roll'), { id: 'body', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 } }))
16
+ const store = {
17
+ load: async (id: string) => ({ id, source }),
18
+ save: async (q: { id?: string; source: string }) => ({ id: q.id ?? 'saved', source: q.source })
19
+ }
20
+
21
+ test('the field sends the code as the kernel stores it and the three lengths as numbers', () => {
22
+ assert.deepEqual(carrierCommandOf(draft(' foup '), say), {
23
+ action: { kind: 'set-carrier', carrier: { type: 'FOUP', dimensions: { width: 1000, length: 600, height: 200 } } }
24
+ })
25
+ })
26
+
27
+ test('a value the kernel refuses is said from its code and path, and nothing is sent', () => {
28
+ assert.deepEqual(carrierCommandOf(draft(' '), say), { refusal: 'figure.text.carrier-type-required' })
29
+ assert.deepEqual(carrierCommandOf(draft('X'.repeat(65)), say), { refusal: 'figure.text.carrier-type-too-long {"max":"64"}' })
30
+ assert.deepEqual(carrierCommandOf(draft('FOUP', '1000', ''), say), {
31
+ refusal: 'figure.text.carrier-length-positive {"axis":"figure.label.carrier-length"}'
32
+ })
33
+ assert.deepEqual(carrierCommandOf(draft('FOUP', '1000', '600', '-5'), say), {
34
+ refusal: 'figure.text.carrier-length-positive {"axis":"figure.label.carrier-height"}'
35
+ })
36
+ assert.deepEqual(carrierCommandOf(draft('FOUP', '0'), say), {
37
+ refusal: 'figure.text.carrier-length-positive {"axis":"figure.label.carrier-width"}'
38
+ })
39
+ })
40
+
41
+ test('the field reads a declaration back, and what is declared already has nothing to send', () => {
42
+ const declared = { type: 'FOUP', dimensions: { width: 1000, length: 600, height: 200 } }
43
+ assert.deepEqual(carrierDraftOf(declared), draft('FOUP'))
44
+ assert.deepEqual(carrierDraftOf(), draft('', '', '', ''))
45
+ assert.equal(draftIsDeclared(draft('foup ', '1000.0'), declared), true)
46
+ assert.equal(draftIsDeclared(draft('FOUP', '1001'), declared), false)
47
+ assert.equal(draftIsDeclared(draft('', '', '', ''), undefined), true)
48
+ assert.equal(draftIsDeclared(draft('FOUP'), undefined), false)
49
+ })
50
+
51
+ test('declaring and clearing are steps of the history: undo takes each back and the save carries the declaration', async () => {
52
+ const session = createFigureProductSession(store)
53
+ await session.open('A')
54
+ const made = carrierCommandOf(draft('foup'), say)
55
+ assert.ok('action' in made)
56
+
57
+ session.command(made.action)
58
+ const declared = { type: 'FOUP', dimensions: { width: 1000, length: 600, height: 200 } }
59
+ assert.deepEqual(session.revisionNow()?.asset.carrier, declared)
60
+ assert.deepEqual(parseFigureAsset(session.source()).carrier, declared)
61
+
62
+ session.command({ kind: 'set-carrier', carrier: null })
63
+ assert.equal(session.revisionNow()?.asset.carrier, undefined)
64
+
65
+ session.undo()
66
+ assert.deepEqual(session.revisionNow()?.asset.carrier, declared, 'undoing the clear brings the declaration back')
67
+ session.undo()
68
+ assert.equal(session.revisionNow()?.asset.carrier, undefined, 'undoing the declaration leaves the figure no carrier')
69
+ })
@@ -1,8 +1,8 @@
1
1
  /*
2
2
  * **속 공간 그림** (사용자 2026-10-01: 「속공간 만들기도 시각적으로 편집할 수 있도록」).
3
3
  *
4
- * 벽 손잡이를 끄는 것, 끝을 눌러 여닫는 것이 figure-model 의 set-cavity 한 명령으로 받아들여지는지, 커널이 거절할 모양은
5
- * 보내지 않고 그 까닭을 말하는지, 끄는 동안은 미리보기만 하고 놓을 때 한 번 보내는지를 잰다.
4
+ * 벽 손잡이를 끄는 것, 끝을 눌러 여닫는 것이 figure-model 의 set-cavity 한 명령으로 받아들여지는지, 커널이 거절할 모양을
5
+ * 알아보는지를 잰다. 끄는 동안은 미리보기만 하고 놓을 때 한 번 보내는지는 브라우저 점검(test/browser/section.check.mjs)이 잰다.
6
6
  */
7
7
  import { test } from 'node:test'
8
8
  import assert from 'node:assert/strict'
@@ -15,9 +15,6 @@ import type { FigureAsset, FigureEvaluatedShape } from '@hatiolab/figure-model'
15
15
  import { cavityRefusalOf, cavityResultOf, cavityWorkingOf, endToggled, wallDragged } from '../client/modeller/section-controls.ts'
16
16
 
17
17
  const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
18
- const has = (source: string, re: RegExp, message?: string) => assert.ok(re.test(source), message ?? `missing: ${re}`)
19
- const drawing = read('../client/modeller/figure-cavity-drawing.ts')
20
- const panel = read('../client/modeller/figure-section-panel.ts')
21
18
 
22
19
  const tote = (): FigureAsset => {
23
20
  const box = addFigureBox(createFigureAsset('Tote'), { id: 'bin', size: { x: 600, y: 300, z: 400 }, position: { x: 0, y: 150, z: 0 } })
@@ -55,24 +52,11 @@ test('커널이 거절할 모양은 보내지 않고 까닭을 말한다 — 끄
55
52
  const w = cavityWorkingOf(s)!
56
53
  assert.equal(cavityRefusalOf(s, { ...w, wall: wallDragged(w.wall, 'x-', 250, false) })?.code, 'wall-too-thick')
57
54
  assert.equal(cavityRefusalOf(s, { ...w, start: { closed: 200 }, end: { closed: 150 } })?.code, 'ends-too-thick')
58
- has(drawing, /this\.emit\('command-live', this\.refusal \? null : this\.action\(w\)\)/, 'a refused shape is not previewed')
59
- has(drawing, /if \(this\.refusal\) \{\s*this\.emit\('command-live', null\)\s*return\s*\}\s*this\.emit\('command', this\.action\(w\)\)/, 'let go: one command, or nothing -- and the reason stays said')
60
- has(drawing, /this\.refusal = this\.judge\(next\)\s*if \(!this\.refusal\) this\.emit\('command', this\.action\(next\)\)/, 'a press toggles by one command')
61
55
  })
62
56
 
63
57
  test('단면 칸에 그림이 들어간다: 속 공간이 있을 때, 면마다 벽 토글을 따른다', () => {
64
- has(panel, /\? html`<figure-cavity-drawing \.body=\$\{body\} \.perSide=\$\{this\.perSide\} \?locked=\$\{this\.locked\}><\/figure-cavity-drawing>/)
65
- /* The number fields under the drawing went: a wall's number or a closed end's is pressed and typed in the drawing. */
66
- assert.ok(!panel.includes("cavityField(") && !panel.includes('renderEnd('), 'no number fields repeating the drawing')
67
- has(drawing, /@pointerdown=\$\{\(ev: PointerEvent\) => this\.type\(ev, \{ kind: 'wall', side \}, 'top', w\.wall\[side\]!\)\}/)
68
- has(drawing, /!open && this\.type\(ev, \{ kind: 'end', end \}, 'side'/)
69
58
  for (const lang of ['ko', 'en', 'ja', 'zh', 'ms', 'es']) {
70
59
  const t = JSON.parse(read(`../translations/${lang}.json`)) as Record<string, string>
71
60
  for (const key of ['figure.label.section-cavity-top', 'figure.label.section-cavity-side']) assert.ok(t[key], `${lang} ${key}`)
72
61
  }
73
62
  })
74
-
75
- test('맞은편 벽의 숫자와 손잡이가 겹치지 않는다: 숫자는 단면 바깥, 좁은 속에서는 손잡이가 비킨다', () => {
76
- has(drawing, /const tx = a === h \? ox \+ sign \* \(half\[a\] \+ 5\) : ox,\s*ty = a === v \? oy \+ sign \* \(half\[a\] \+ \(low \? 5 : 13\)\) : oy \+ 4/, 'past the outer edge on its own side')
77
- has(drawing, /const aside = room < 16 \? sign \* 9 : 0/, 'two handles close together step apart')
78
- })
@@ -64,8 +64,6 @@ test('a proposal that stops no clip keeps every clip', async () => {
64
64
  })
65
65
 
66
66
  test('the AI never agrees for the person: no consent on any proposal path, and none carried in a step', async () => {
67
- for (const file of ['../client/modeller/ai-proposal.ts', '../client/pages/figure-modeller-page.ts'])
68
- assert.ok(!/withConsent|dropClips/.test(read(file)), `${file} gives no consent`)
69
67
  /* Consent written into the steps or beside them by the model is not consent: the proposal is still refused. */
70
68
  const s = createFigureProductSession(store)
71
69
  await s.open('door')
@@ -76,8 +74,7 @@ test('the AI never agrees for the person: no consent on any proposal path, and n
76
74
  assert.equal(out.verdict, 'refused')
77
75
  assert.equal(out.errorCode, 'CLIPS_DROPPED', 'refused for the clip it would stop: the consent it carries is read as nothing')
78
76
  assert.deepEqual(clipsOf(s), ['swing'])
79
- const page = read('../client/pages/figure-modeller-page.ts')
80
- assert.ok(page.includes('if (takeFigureOffer(this.session, detail, {'), 'the page takes an offer by the handler tested here')
77
+ /* The page takes an offer by this same handler: product-page.check.mjs sends it one that would stop a clip. */
81
78
  })
82
79
 
83
80
  /* Designer 2026-09-27: not calling it is not forbidding it -- an AI proposal made inside a person's consent, still refused. */
@@ -100,5 +97,4 @@ test('an AI proposal made while a person\'s consent is held is refused; clips, f
100
97
  assert.equal(s.read().revision, revision)
101
98
  assert.equal(s.read().dirty, false)
102
99
  assert.deepEqual(clipsOf(s), ['swing'])
103
- assert.ok(!/origin: 'ai'[^\n]*consented\(\)/.test(read('../client/modeller/product-session.ts')), 'the AI path passes no consent')
104
100
  })
@@ -75,11 +75,6 @@ test('a consent is used by one edit only, on the version and figure it was given
75
75
  })
76
76
 
77
77
  test('the modeller asks on every path an edit can stop a clip, in every language', () => {
78
- const editor = read('../client/modeller/editor.ts')
79
- for (const path of ['() => this.run(action)', '() => this.linkCommand(action, from)', '() => this.removePart(id, refitRoom)'])
80
- assert.ok(editor.includes(`if (this.askClipLoss(error, ${path})) return`), path)
81
- assert.ok(editor.includes('if (!now || now.context !== asked.context || now.revision !== asked.revision) {'), 'what was asked about is checked after the answer')
82
- assert.ok(editor.includes('asked.session.withConsent({ dropClips: clips.map(c => c.id), context: asked.context, revision: asked.revision }, again)'))
83
78
  for (const lang of ['en', 'ja', 'ko', 'ms', 'zh', 'es']) {
84
79
  const t = JSON.parse(read(`../translations/${lang}.json`))
85
80
  for (const key of ['figure.text.clips-would-go', 'figure.text.clip-gone', 'figure.text.clip-changed', 'figure.button.let-clips-go', 'figure.text.clips-consent-stale']) assert.ok(t[key], `${lang} ${key}`)
@@ -55,14 +55,7 @@ test('키를 그 순간에 더하고 빼며, 쓰면 figure-model 이 받는다
55
55
  })
56
56
 
57
57
  test('움직임 탭에 서고, 막대는 입력 한 묶음 · 나머지는 부품 편집으로 편집기에 닿는다', () => {
58
- const editor = read('../client/modeller/editor.ts')
59
- const start = editor.indexOf('<figure-clips')
60
- const block = editor.slice(start, editor.indexOf('</figure-clips>', start))
61
- assert.ok(start >= 0)
62
- assert.ok(block.includes('@states=${(e: CustomEvent) => this.run(() => this.session!.apply(e.detail.changes))}'))
63
- assert.ok(block.includes('@part-edit=${(e: CustomEvent) => this.run(() => this.session!.edit(e.detail.edit))}'))
64
58
  const clips = read('../client/modeller/figure-clips.ts')
65
- assert.ok(clips.includes("@change=${(e: Event) => this.scrub(clip, number(e))}"), '막대는 놓을 때 한 번')
66
59
  const keys = [...clips.matchAll(/'(figure\.[a-z.-]*[a-z])'/g)].map(m => m[1])
67
60
  for (const lang of ['ko', 'en', 'ja', 'zh', 'ms', 'es']) {
68
61
  const table = JSON.parse(read(`../translations/${lang}.json`))
@@ -75,25 +75,7 @@ test('the face it would meet is lit where attaching reads it, just off the face'
75
75
  assert.equal(faceHighlightOf(undefined), null)
76
76
  })
77
77
 
78
- test('the editor shows it only for the figure, version and part it was worked out for, and clears it when the form lets go', () => {
79
- const editor = read('../client/modeller/editor.ts')
80
- assert.ok(editor.includes('if (!p || !view || p.context !== view.context || p.revision !== view.revision || p.part !== this.selected || this.middle !== \'edit\' || this.tab !== \'properties\') return undefined'))
81
- assert.ok(editor.includes('this.preview = { ...held, refused: this.said(error) }'), 'refused: no earlier result stays')
82
- assert.ok(editor.includes('.ghost=${this.previewShown().ghost}') && editor.includes('.highlight=${this.previewShown().highlight}'))
83
- const links = read('../client/modeller/figure-links.ts')
84
- assert.ok(links.includes("this.dispatchEvent(new CustomEvent('preview-command', { detail: this.candidate ?? null, bubbles: true, composed: true }))"))
85
- assert.ok(links.includes("if (this.told && this.told !== 'null') this.dispatchEvent(new CustomEvent('preview-command', { detail: null, bubbles: true, composed: true }))"), 'leaving the panel clears it')
86
- assert.ok(links.includes("if (this.section !== 'shape' && !this.told) return"), 'the sections without the form do not clear it')
87
- assert.ok(links.includes('<button act ?disabled=${this.locked || !action} @click=${() => action && this.command(action)}>'), 'the button applies the command the preview showed')
88
- })
89
-
90
78
  test('the details fold, and what they hold that is not the default stays said -- nothing for the default', () => {
91
- const links = read('../client/modeller/figure-links.ts')
92
- assert.ok(links.includes('<details fasten-detail>'))
93
- assert.ok(links.includes("${detail.length ? html` · <span fasten-summary>${detail.join(' · ')}</span>` : nothing}"))
94
- /* The way and the place on the face are the drawing's: not said again in the summary, nor chosen again in the details. */
95
- assert.ok(!links.includes('fasten-summary-edge') && !links.includes('fasten-summary-off-centre') && !links.includes("'figure.label.facing-flush'"))
96
- assert.ok(links.includes('({ axis }) => !view || !view.axes.includes(axis) || !!f.alignInput?.[axis] || hasInstanceSize(asset, axis)'), 'a row only where the drawing cannot set it')
97
79
  for (const lang of ['en', 'ja', 'ko', 'ms', 'zh', 'es']) {
98
80
  const t = JSON.parse(read(`../translations/${lang}.json`))
99
81
  for (const key of ['preview-refused', 'preview-stays', 'preview-moves', 'preview-turns-named', 'preview-turns-slanted', 'preview-carries', 'preview-copies', 'preview-clips', 'preview-face-unknown', 'target-would-loop', 'fasten-summary-gap', 'fasten-summary-twist'])
@@ -102,15 +84,6 @@ test('the details fold, and what they hold that is not the default stays said --
102
84
  }
103
85
  })
104
86
 
105
- /* ADR-0097 §5.5 (Q2): the list stays flat; a part says what it is fastened to, beside its name. */
106
- test('the part list names what each part is fastened to, without reordering or nesting it', () => {
107
- const editor = read('../client/modeller/editor.ts')
108
- assert.ok(editor.includes("'plane' in j.body0 ? i18next.t('figure.label.mounting-plane') : `${partLabelOf(j.body0.part)} · ${i18next.t(endPlaceKeyOf(j.body0))}`"))
109
- assert.ok(editor.includes('...(fastened.has(id) ? { fastenedTo: fastened.get(id)! } : {})'))
110
- const parts = read('../client/modeller/figure-parts.ts')
111
- assert.ok(parts.includes('<small fastened-to title=${i18next.t(\'figure.label.fastened-to\')}>${part.fastenedTo}</small>'))
112
- })
113
-
114
87
  /* Detaching is seen before it is done: "it stays where it is" only when the copy shows it does. */
115
88
  test('detaching says what it would do: stays, what stays fastened to it, the clips it would stop', async () => {
116
89
  const s = createFigureProductSession(store)
@@ -118,25 +91,12 @@ test('detaching says what it would do: stays, what stays fastened to it, the cli
118
91
  const seen = s.previewCommand({ kind: 'detach', part: 'lamp' } as never)
119
92
  assert.equal(seen.moves.length, 0, 'the lamp stays where it stands')
120
93
  assert.equal(s.read().revision, 0)
121
- const editor = read('../client/modeller/editor.ts')
122
- assert.ok(editor.includes("const own = seen.moves.some(m => m.body === part)"))
123
- /* Staying where it is is as expected: nothing is said then (user 2026-10-01); a move, a child kept, a clip lost are. */
124
- assert.ok(editor.includes(': /* It stays where it is: as expected, so nothing is said (user 2026-10-01). */ []'), 'nothing said when it stays')
125
- const links = read('../client/modeller/figure-links.ts')
126
- assert.equal(links.match(/\$\{this\.renderDetachWords\(\)\}/g)?.length, 3, 'beside every button that detaches (a part at an origin has its own)')
127
94
  for (const lang of ['en', 'ja', 'ko', 'ms', 'zh', 'es']) {
128
95
  const t = JSON.parse(read(`../translations/${lang}.json`))
129
96
  for (const key of ['detach-moves', 'detach-keeps-children', 'detach-clips', 'detach-refused']) assert.ok(t[`figure.text.${key}`], `${lang} ${key}`)
130
97
  }
131
98
  })
132
99
 
133
- /* A panel made with a part already chosen loads that part's fastening: "editing the fastening it has" over an empty target was wrong. */
134
- test('the fastening form opens on the part\'s own fastening however the panel came to be', () => {
135
- const links = read('../client/modeller/figure-links.ts')
136
- assert.ok(links.includes('if (this.asset && this.partId && this.partId !== this.loadedFor) {'))
137
- assert.ok(!links.includes("changed.get('partId') !== undefined"), 'not only when the part changes after the panel was made')
138
- })
139
-
140
100
  /* ADR-0097 §5.5: a row is said as what it makes, read as drawn; where it cannot be made, why. */
141
101
  test('a row is seen before it is made, read as figure-model draws it -- and said where it cannot be made', async () => {
142
102
  const { rowWordsOf } = await import('../client/modeller/command-preview.js')
@@ -155,15 +115,6 @@ test('a row is seen before it is made, read as figure-model draws it -- and said
155
115
  /* A set count is spread over the length: said as drawn, 970 apart, nothing left at the ends. */
156
116
  const three = figureRowOf(applyFigureAuthoring(a, { kind: 'make-repeat', part: 'roller', axis: 'x', pitch: 1, count: 3 }), 'roller')
157
117
  assert.deepEqual(rowWordsOf(three, say), ['roller 3개 · X 방향 · pitch 970 mm (중심 간격)', '가운데 정렬 · 양 끝에 0 mm씩 남음'])
158
- const links = read('../client/modeller/figure-links.ts')
159
- assert.ok(links.includes("<p hint row-blocked>${i18next.t(`figure.text.row-blocked-${blocked ?? 'unfastened'}`, { part: this.partId })}</p>"), 'not hidden: why')
160
- /* Folded, its head the section's: opened, it says why; open by itself only while it is worked. */
161
- assert.ok(links.includes("const head = html`<summary><h3>${i18next.t('figure.label.repeat')}</h3></summary>`"))
162
- assert.ok(links.includes("<section row><details row-fold>") && links.includes("<details row-fold ?open=${this.working === 'row'}>"))
163
- assert.ok(links.includes("this.candidate = this.working === 'row' && this.rowCandidate ? { action: this.rowCandidate } : this.fastenCandidate"), 'the form being worked on is the one seen')
164
- const editor = read('../client/modeller/editor.ts')
165
- assert.ok(editor.includes("const row = candidate.action.kind === 'make-repeat' ? figureRowOf(seen.asset, this.selected, this.shown()!.inputs) : undefined"))
166
- assert.ok(editor.includes('<ul row-reading>${rowWordsOf(this.rowNow(shown)'), 'a row made says the same, as it stands')
167
118
  for (const lang of ['en', 'ja', 'ko', 'ms', 'zh', 'es']) {
168
119
  const t = JSON.parse(read(`../translations/${lang}.json`))
169
120
  for (const key of ['row-count-pitch', 'row-count-uneven', 'row-ends-centred', 'row-ends', 'row-ends-unknown', 'row-companions', 'row-count-spreads', 'row-blocked-unfastened', 'row-blocked-on-plane', 'row-blocked-holds-parts'])
@@ -197,8 +148,6 @@ test('a published pitch typed into its field is one edit, undone once', async ()
197
148
  s.undo()
198
149
  assert.equal(count(), 10, 'one undo, and it is back')
199
150
  assert.equal(s.read().fields.find(f => f.id === 'pitch')?.value, 200)
200
- const editor = read('../client/modeller/editor.ts')
201
- assert.ok(editor.includes("this.run(() => this.session!.apply([{ kind: 'set-design', input: from, unit: 'mm', value: pitch }]))"), 'the field sends one change, on change -- not per key')
202
151
  })
203
152
 
204
153
  /* ADR-0097 §5.5: a motion is named by its parts, and where it takes the part is drawn at both ends of its range. */
@@ -222,24 +171,12 @@ test('a motion is named by its parts, and drawn at both ends of its range -- onl
222
171
  const slide = motionEndsOf(held, 'gate')!
223
172
  const line = slide.ghost.find(g => g.id === '__target-face:slide')!
224
173
  assert.equal(Math.round((line.shape as unknown as { extrusion: { section: { size: { x: number } } } }).extrusion.section.size.x), 300, 'the line it travels, its whole stroke')
225
- const editor = read('../client/modeller/editor.ts')
226
- assert.ok(editor.includes("if (!p && this.tab === 'motion' && this.middle === 'edit') return { ghost: this.motionEnds()?.ghost ?? [], highlight: null }"))
227
- const links = read('../client/modeller/figure-links.ts')
228
- assert.ok(links.includes("i18next.t('figure.text.motion-summary', {"))
229
- assert.ok(links.includes("new CustomEvent('go-fasten', { bubbles: true, composed: true })"), 'a part fastened to nothing is taken to fastening')
230
174
  assert.equal(say('figure.text.motion-summary', { part: 'leaf', against: 'frame', kind: '회전', axis: 'Y' }), 'leaf · frame 기준 · Y축 회전')
231
175
  assert.match(say('figure.text.motion-ends-shown', { min: '0', max: '90', unit: 'deg' }), /그 사이는 확인하지 않습니다/)
232
176
  })
233
177
 
234
178
  /* ADR-0097 §5.5, Q3: past the face's edge is said beside the fastening, with the way to change it; nothing moves. */
235
179
  test('past the edge of its face is said beside the fastening, with the way to change alignment and gap', () => {
236
- const editor = read('../client/modeller/editor.ts')
237
- assert.ok(editor.includes("if (own?.state === 'out') words.push(say('figure.text.overhang-out', { ...where(own), sides: sides(own) }))"))
238
- assert.ok(editor.includes("else if (own?.state === 'possible') words.push(say('figure.text.overhang-possible', where(own)))"))
239
- assert.ok(editor.includes("else if (own?.state === 'unknown') words.push(say('figure.text.overhang-unknown', where(own)))"))
240
- const links = read('../client/modeller/figure-links.ts')
241
- assert.ok(links.includes("<button act @click=${() => this.showWhere()}><md-icon>tune</md-icon>${i18next.t('figure.button.fix-alignment')}</button>"))
242
- assert.ok(links.includes("shadowRoot?.querySelector<HTMLElement>('div[where]')"), 'to the face in the drawing, where the place is dragged or keyed')
243
180
  assert.equal(say('figure.text.overhang-out', { on: 'top', face: '윗면', sides: say('figure.text.overhang-side-high', { axis: 'X', mm: '100' }) }), 'top의 윗면 가장자리 밖으로 나와 있습니다: X 높은 쪽 100 mm. 모델의 점유 공간과는 따로 봅니다.')
244
181
  for (const lang of ['en', 'ja', 'ko', 'ms', 'zh', 'es']) {
245
182
  const t = JSON.parse(read(`../translations/${lang}.json`))
@@ -284,11 +221,6 @@ test('a moving part is made into a row, and a row given or relieved of a motion
284
221
 
285
222
  /* Designer 2026-09-27: wider than its face is not hidden -- a fact in the relation's details, off to one side said. */
286
223
  test('a part wider than its face is said in the relation, with its numbers and whether it sits to one side', () => {
287
- const editor = read('../client/modeller/editor.ts')
288
- assert.ok(editor.includes("if (own?.state === 'larger') {"))
289
- assert.ok(editor.includes("if (own.lopsided.length) note.push(say('figure.text.overhang-lopsided'"))
290
- const links = read('../client/modeller/figure-links.ts')
291
- assert.ok(links.includes("${this.overhangNote.length ? html`<p hint overhang-note>${this.overhangNote.join(' ')}</p>` : nothing}"), 'in the details, not the warning')
292
224
  assert.equal(say('figure.text.overhang-larger', { on: 'leg', face: '윗면', sides: 'X 낮은 쪽 270 mm, X 높은 쪽 870 mm' }), 'leg의 윗면보다 넓어 가장자리 밖으로 나와 있습니다: X 낮은 쪽 270 mm, X 높은 쪽 870 mm.')
293
225
  for (const lang of ['en', 'ja', 'ko', 'ms', 'zh', 'es']) {
294
226
  const t = JSON.parse(read(`../translations/${lang}.json`))
@@ -51,16 +51,7 @@ test('지금 자세를 기본으로 — 보고 있는 자세가 시작 자세와
51
51
  })
52
52
 
53
53
  test('편집기에 닿는다 — 값 바꾸기는 입력으로, 이름 · 범위 · 시간 · 기본 자세는 부품 편집으로', () => {
54
- const editor = read('../client/modeller/editor.ts')
55
- const start = editor.indexOf('<figure-controls')
56
- const block = editor.slice(start, editor.indexOf('</figure-controls>', start))
57
- assert.ok(start >= 0)
58
- assert.ok(block.includes("@state=${(e: CustomEvent) => this.run(() => this.session!.apply([{ kind: 'set-state'"))
59
- assert.ok(block.includes('@part-edit=${(e: CustomEvent) => this.run(() => this.session!.edit(e.detail.edit))}'))
60
- assert.ok(editor.includes("const design = view.fields.filter(i => i.role !== 'state')"), '조작값은 설계 값 칸에 두 번 나오지 않는다')
61
-
62
54
  const controls = read('../client/modeller/figure-controls.ts')
63
- assert.ok(controls.includes("@change=${(e: Event) => this.try(c, number(e))}"), '슬라이더는 놓을 때 한 번 — 끄는 동안 기록이 쌓이지 않는다')
64
55
  const keys = [...controls.matchAll(/'(figure\.[a-z.-]*[a-z])'/g)].map(m => m[1])
65
56
  for (const lang of ['ko', 'en', 'ja', 'zh', 'ms', 'es']) {
66
57
  const table = JSON.parse(read(`../translations/${lang}.json`))
@@ -1,31 +1,12 @@
1
1
  import { test } from 'node:test'
2
2
  import assert from 'node:assert/strict'
3
- import { readFileSync } from 'node:fs'
4
- import { fileURLToPath } from 'node:url'
5
3
 
6
- const dock = readFileSync(fileURLToPath(new URL('../client/viewparts/figure-detail-dock.ts', import.meta.url)), 'utf8')
7
-
8
- test('opening the editor from a catalogue preview closes the preview first', () => {
9
- const footer = dock.slice(dock.indexOf('<footer>'), dock.indexOf('</footer>'))
10
- const hide = footer.indexOf('hideDetail()')
11
- const go = footer.indexOf('navigate(modellerPathOf(saved))')
12
- assert.ok(go > 0, 'the edit button opens the modeller')
13
- assert.ok(hide > 0 && hide < go, 'the edit button hides the preview before it navigates')
14
- })
15
-
16
- test('the preview scrolls with the shared scrollbar styles', () => {
17
- assert.match(dock, /import \{ ScrollbarStyles \} from '@operato\/styles'/)
18
- assert.match(dock, /static styles = \[ScrollbarStyles, css`/)
19
- })
4
+ import { leftOpeningPage } from '../client/viewparts/detail-route.ts'
20
5
 
6
+ /* The dock itself -- closed before the modeller opens, closed on leaving the catalogue -- is checked in product-page.check.mjs. */
21
7
  test('leaving the catalogue by any route closes the preview', () => {
22
- const open = dock.slice(dock.indexOf('export function openFigureDetail'))
23
- assert.ok(open.includes('openedOn = (store.getState() as { route?: { page?: string } }).route?.page'), 'opening remembers the page')
24
- assert.ok(dock.includes('leftOpeningPage(openedOn, state.route?.page))) hideDetail()'), 'a route change away from it hides the preview')
25
- const body = dock.slice(dock.indexOf('export function leftOpeningPage'))
26
- const left = new Function('opened', 'current', body.slice(body.indexOf('{') + 1, body.indexOf('\n}')))
27
- assert.equal(left('figure-list', 'figure-list'), false)
28
- assert.equal(left('figure-list', 'figure-modeller'), true)
29
- assert.equal(left('figure-list', 'board-list'), true)
30
- assert.equal(left(undefined, 'figure-modeller'), false)
8
+ assert.equal(leftOpeningPage('figure-list', 'figure-list'), false)
9
+ assert.equal(leftOpeningPage('figure-list', 'figure-modeller'), true)
10
+ assert.equal(leftOpeningPage('figure-list', 'board-list'), true)
11
+ assert.equal(leftOpeningPage(undefined, 'figure-modeller'), false)
31
12
  })
@@ -3,20 +3,16 @@
3
3
  * 관계에만 · 편집한 그 자리에서 묻는다).
4
4
  *
5
5
  * 커널 거절이 실어 온 것으로 줄을 짓고, 다 고른 뒤에만 remap 을 내며, 그 remap 을 붙인 같은 명령을 커널이 받는지 잰다.
6
- * 편집기·단면 편집 창·단면 칸이 거절을 그 자리에 붙들어 두는지는 소스로 잰다.
6
+ * 편집기·단면 편집 창·단면 칸이 거절을 그 자리에 붙들어 두는지는 브라우저 점검(test/browser/section.check.mjs)이 잰다.
7
7
  */
8
8
  import { test } from 'node:test'
9
9
  import assert from 'node:assert/strict'
10
- import { readFileSync } from 'node:fs'
11
- import { fileURLToPath } from 'node:url'
12
10
 
13
11
  import { addFigureBox, applyFigureAuthoring, createFigureAsset } from '@hatiolab/figure-model'
14
12
  import type { FigureAsset, FigureFaceMappingNeed } from '@hatiolab/figure-model'
15
13
 
16
14
  import { DETACH, faceMappingRowsOf, remapOf, rowKey } from '../client/modeller/face-mapping.ts'
17
15
 
18
- const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
19
- const has = (source: string, re: RegExp, message?: string) => assert.ok(re.test(source), message ?? `missing: ${re}`)
20
16
 
21
17
  /** A tote with a floor inside and a divider standing on it. */
22
18
  function tote(): FigureAsset {
@@ -63,18 +59,3 @@ test('nothing goes until every row is chosen, and the chosen remap is what the k
63
59
  const freed = applyFigureAuthoring(asset, { ...open, remap: remapOf(rows, { [rowKey(rows[0]!)]: DETACH })! })
64
60
  assert.ok(!(freed.joints ?? []).some(j => j.id === 'divider'))
65
61
  })
66
-
67
- test('the choice is asked where the edit was made, and the same edit goes again with it', () => {
68
- const editor = read('../client/modeller/editor.ts')
69
- const sectionEditor = read('../client/modeller/figure-section-editor.ts')
70
- const panel = read('../client/modeller/figure-section-panel.ts')
71
- const mapping = read('../client/modeller/figure-face-mapping.ts')
72
- has(editor, /code === 'FEATURE_MAPPING_REQUIRED' && need\?\.lost\?\.length\) \{\s*this\.faceMapping = \{ action, need, from \}/, 'the refusal is held, not only said')
73
- has(editor, /linkCommand\(\{ \.\.\.held\.action, remap \}/, 'the same edit, with where things go')
74
- has(editor, /if \(!this\.linkCommand\(e\.detail\.action, 'section-editor'\)\) e\.detail\.held = true/, 'the section editor stays open while held')
75
- has(editor, /this\.selected = id \? ownerOf\(id\) : id\s*this\.faceMapping = undefined/, 'another part chosen lets the held edit go')
76
- has(sectionEditor, /if \(!detail\.held\) this\.close\(\)/)
77
- has(sectionEditor, /this\.faceMapping \? html`<figure-face-mapping/)
78
- has(panel, /this\.faceMapping \? html`<figure-face-mapping/)
79
- has(mapping, /\?disabled=\$\{!remap\}/, 'apply is off until every row is chosen')
80
- })
@@ -10,32 +10,9 @@ import { readFileSync } from 'node:fs'
10
10
  import { fileURLToPath } from 'node:url'
11
11
 
12
12
  const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
13
- const has = (source: string, re: RegExp, message?: string) => assert.ok(re.test(source), message ?? `missing: ${re}`)
14
- const form = read('../client/modeller/figure-links.ts')
15
- const drawing = read('../client/modeller/figure-fasten-drawing.ts')
16
13
  const ko = JSON.parse(read('../translations/ko.json')) as Record<string, string>
17
14
  const say = (key: string, params: Record<string, string>) => Object.entries(params).reduce((s, [k, v]) => s.replace(`{${k}}`, v), ko[key] ?? key)
18
15
 
19
- test('부품의 면·맞닿기 고르기 칸이 없고, 그 자리에 그림이 있다', () => {
20
- assert.ok(!form.includes("i18next.t('figure.label.my-face')"), 'no list of the part\'s own faces')
21
- assert.ok(!form.includes("i18next.t('figure.label.facing')"), 'no facing list')
22
- has(form, /<figure-fasten-drawing[\s\S]*?\.modes=\$\{f\.toPlane \? \(\['meet'\] as FastenMode\[\]\)/, 'the plane is set on only')
23
- has(form, /turnsInPlace\(asset, this\.partId\) \? \(\['centre'\] as FastenMode\[\]\) : \[\]/, 'centre on it only for a part that turns')
24
- has(form, /const derived = f\.toPlane \|\| f\.target \? myFaceFor\(this\.revision!, this\.partId, aim, mode\) : undefined/, 'the part\'s face follows the target and how')
25
- })
26
-
27
- test('상대 목록에 배치 기준의 면이 이름으로 있다: 바닥 · 천장 · 벽 · 중심', () => {
28
- has(form, /const planeWord = i18next\.t\('figure\.label\.resize-place-' \+ \(asset\.placement \?\? 'floor'\)\)/)
29
- has(form, /<option value=\$\{PLANE\} \?selected=\$\{f\.toPlane\}>\$\{planeWord\}<\/option>/)
30
- })
31
-
32
- test('놓으면: 이미 붙은 부품은 한 번 고쳐지고, 새 부품은 붙이기를 기다린다', () => {
33
- has(form, /private async onFastenRelease\(held: boolean\) \{\s*if \(!held\) return\s*await this\.updateComplete\s*if \(this\.fastenCandidate\) this\.command\(this\.fastenCandidate\.action\)/)
34
- has(drawing, /private drop\(\) \{[\s\S]*?this\.released\(\)\s*\}/, 'a drag ends in one release')
35
- has(drawing, /this\.change\(\{ targetFace: f \}\)\s*this\.released\(\)/, 'a face pressed is one change')
36
- has(drawing, /this\.change\(\{ mode: m \}\)\s*this\.released\(\)/, 'a way pressed is one change')
37
- })
38
-
39
16
  test('화면 문구는 결과와 예외만 — 한 줄 요약, 가장자리 밖, 닿지 않음', () => {
40
17
  assert.equal(say('figure.text.fasten-sentence-meet', { part: 'lid', face: '아랫면', to: 'bin의 윗면' }), 'lid의 아랫면을 bin의 윗면에 맞닿게')
41
18
  assert.equal(say('figure.text.fasten-sentence-centre', { part: 'roller', face: '', to: 'frame의 앞면' }), 'roller의 가운데를 frame의 앞면 위에')
@@ -38,26 +38,13 @@ test('오른쪽 칸은 한 벌의 규격 — 요소마다 제 제목 · 칸 ·
38
38
  }
39
39
  })
40
40
 
41
- test('긴 설명은 제목 옆 「?」 로 연다 — 늘 펼쳐 두지 않는다', () => {
42
- for (const [file, key] of [
43
- ['figure-links', 'figure.text.motion-explained'],
44
- ['figure-links', 'figure.text.place-explained'],
45
- ['figure-links', 'figure.text.fastening-explained'],
46
- ['figure-clips', 'figure.text.clips-explained']
47
- ]) {
48
- const source = readFileSync(fileURLToPath(new URL(`../client/modeller/${file}.ts`, import.meta.url)), 'utf8')
49
- assert.ok(source.includes(`this.help.button('${key}')`) && source.includes(`this.help.hint('${key}')`), `${file}: ${key}`)
50
- assert.ok(!source.includes(`<p hint>\${i18next.t('${key}')}</p>`), `${file}: ${key} 가 늘 펼쳐져 있다`)
51
- }
52
- })
41
+ /* The long explanations opened from their 「?」 are the browser check's (test/browser/parts.check.mjs). */
53
42
 
54
43
  test('되풀이한 부품은 되풀이의 이름과 몇 번째인지로 읽는다 — 날 id 를 보이지 않는다', () => {
55
44
  assert.equal(partLabelOf('["roller.repeat",0,"roller.value"]'), 'roller 1')
56
45
  assert.equal(partLabelOf('["roller.repeat",2,"mark.placed"]'), 'mark 3', '줄의 부품에 붙은 것은 제 이름으로 (ADR-0093, 2026-09-26)')
57
46
  assert.equal(partLabelOf('deck'), 'deck', '보통 부품은 제 이름')
58
47
  assert.equal(partLabelOf('[not json'), '[not json')
59
- const parts = readFileSync(fileURLToPath(new URL('../client/modeller/figure-parts.ts', import.meta.url)), 'utf8')
60
- assert.ok(parts.includes('>${partLabelOf(part.id)}'), '목록이 이 이름으로 그린다')
61
48
  })
62
49
 
63
50
  test('되풀이한 부품의 속성 이름 칸도 사람의 이름이고, 몇 개 중 몇 번째인지 말한다 — 날 id 를 보이지 않는다', () => {
@@ -67,9 +54,6 @@ test('되풀이한 부품의 속성 이름 칸도 사람의 이름이고, 몇
67
54
  const carried = geometry.concat(Array.from({ length: 16 }, (_, i) => ({ id: JSON.stringify(['roller.repeat', i, 'mark.placed']) })))
68
55
  assert.deepEqual(repeatOf('["roller.repeat",14,"roller.value"]', carried), { name: 'roller', count: 16, index: 15 }, '롤러에 붙은 것은 롤러 수에 안 든다')
69
56
  assert.deepEqual(repeatOf('["roller.repeat",2,"mark.placed"]', carried), { name: 'mark', count: 16, index: 3 })
70
- const editor = readFileSync(fileURLToPath(new URL('../client/modeller/editor.ts', import.meta.url)), 'utf8')
71
- assert.ok(editor.includes('.value=${partLabelOf(this.selected)}'), '사용자 2026-09-24: 「부품 이름 ???」')
72
- assert.ok(editor.includes('?disabled=${unavailable || !!repeatOf(this.selected, view.geometry)?.index}'), '복사본에는 바꿀 제 이름이 없고, 줄의 부품 자신은 이름을 바꿀 수 있다(walk 1, 2026-09-26)')
73
57
  assert.equal(repeatOf('roller', geometry)?.index, 0, '줄의 부품 자신은 몇 번째가 아니다')
74
58
  })
75
59
 
@@ -78,24 +62,12 @@ test('편집기는 저작한 부품 단위로 고르고 보인다 — 되풀이
78
62
  assert.equal(ownerOf('deck'), 'deck')
79
63
  assert.equal(ownerOf('["roller.repeat",2,"mark.placed"]'), 'mark', '롤러에 붙은 표시의 복사본을 누르면 그 표시를 고른다')
80
64
  assert.deepEqual(repeatOf('roller', [{ id: '["roller.repeat",0,"roller.value"]' }, { id: '["roller.repeat",1,"roller.value"]' }, { id: 'deck' }]), { name: 'roller', count: 2, index: 0 })
81
- const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
82
- const canvas = read('../client/modeller/canvas.ts')
83
- assert.ok(canvas.includes('.filter(([name]) => ownerOf(name) === this.selected)'), '사용자 2026-09-24: 「why repeatable parts are not selected?」')
84
- const editor = read('../client/modeller/editor.ts')
85
- assert.ok(editor.includes('this.selected = id ? ownerOf(id) : id'), '캔버스에서 복사본을 누르면 그 되풀이를 고른다')
86
- assert.ok(editor.includes('.parts=${this.partRows(view.geometry, asset)}'), '목록은 저작한 부품 한 줄씩')
87
- assert.ok(editor.includes("this.session!.command({ kind: 'set-repeat', part: this.selected, pitch })"), 'pitch 는 명령으로 정한다')
88
- const parts = read('../client/modeller/figure-parts.ts')
89
- assert.ok(parts.includes('?disabled=${this.locked || full}'), '등급 한도에 이르면 팔레트가 잠긴다')
65
+ /* Every copy selected on the canvas, the pitch set by the row and the palette locked at the level's limit are the browser
66
+ check's (test/browser/parts.check.mjs); a copy clicked picks its part, one row a part (modeller.check.mjs). */
90
67
  })
91
68
 
92
69
  test('pitch 는 한 곳에서 하나의 값이고, 늘 set-repeat 로 바꿔 최대 길이가 따라온다', () => {
93
- const editor = readFileSync(fileURLToPath(new URL('../client/modeller/editor.ts', import.meta.url)), 'utf8')
94
- assert.ok(editor.includes("groupFields(partFields, this.selected)") && editor.includes("const partFields = own.filter("), '사용자 2026-09-24: 되풀이의 간격과 형상 치수의 repeat · pitch 가 같은 값으로 두 번 떴다')
95
- assert.ok(editor.includes("!i.id.startsWith(this.selected + '.repeat.')"))
96
- const pitch = editor.slice(editor.indexOf(' private setPitch('), editor.indexOf(' /** Choose a part,'))
97
- assert.ok(pitch.includes("this.session!.command({ kind: 'set-repeat', part: this.selected, pitch })"), '늘 명령으로 — 최대 길이가 따라온다')
98
- assert.ok(!pitch.includes('changeField'))
70
+ /* Offered once and set by the row in one step: the browser check's (test/browser/parts.check.mjs). */
99
71
  for (const lang of ['ko', 'en', 'ja', 'zh', 'ms', 'es']) {
100
72
  const table = JSON.parse(readFileSync(fileURLToPath(new URL(`../translations/${lang}.json`, import.meta.url)), 'utf8'))
101
73
  /* The term stays pitch; what it measures is said beside it (ADR-0097 §5.5: centre to centre). */
@@ -109,6 +81,7 @@ test('기즈모로 끈, 규칙이 자리를 정하는 부품은 규칙을 지킨
109
81
  const moved = editor.slice(editor.indexOf(' private transformed('), editor.indexOf(' /** 맞추기'))
110
82
  assert.ok(moved.includes("kind: 'nudge-part',"), '사용자 2026-09-24: 「기즈모 컨트롤도 전혀 작동이 안되는군」')
111
83
  assert.ok(moved.includes('...(Object.keys(grow).length ? { grow } : {})'), '크기 끌기도 배율로 — 「되는 듯하다가 다시 원래 크기로 돌아간다」')
112
- assert.ok(moved.includes("'figure.text.gizmo-joint-places'") && moved.includes("'figure.text.gizmo-fastening-places'"))
84
+ /* Which of the two places it is said from the refusal's details (refusals.test.ts makes both with the kernel, ADR-0127). */
85
+ assert.ok(moved.includes('placedRefusalOf(error, partLabelOf(id)'), moved)
113
86
  })
114
87