@things-factory/figure-ui 10.1.147 → 10.1.150

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (210) hide show
  1. package/client/bootstrap.ts +10 -2
  2. package/client/graphql/index.ts +20 -0
  3. package/client/modeller/canvas.ts +6 -2
  4. package/client/modeller/carrier-declaration.ts +63 -0
  5. package/client/modeller/editor.ts +62 -27
  6. package/client/modeller/figure-carrier.ts +117 -0
  7. package/client/modeller/figure-cavity-drawing.ts +3 -9
  8. package/client/modeller/figure-first-save.ts +4 -7
  9. package/client/modeller/figure-history-panel.ts +2 -3
  10. package/client/modeller/figure-links.ts +46 -60
  11. package/client/modeller/figure-material-panel.ts +4 -14
  12. package/client/modeller/figure-mesh-choice.ts +68 -0
  13. package/client/modeller/figure-parts.ts +23 -1
  14. package/client/modeller/figure-preview.ts +2 -2
  15. package/client/modeller/figure-properties.ts +5 -3
  16. package/client/modeller/figure-section-editor.ts +12 -33
  17. package/client/modeller/figure-section-panel.ts +6 -7
  18. package/client/modeller/figure-slot-layout-panel.ts +208 -0
  19. package/client/modeller/figure-verdict.ts +21 -31
  20. package/client/modeller/first-save.ts +23 -0
  21. package/client/modeller/links.ts +52 -1
  22. package/client/modeller/material-edit.ts +38 -0
  23. package/client/modeller/maturity.ts +28 -0
  24. package/client/modeller/mesh-import.ts +77 -0
  25. package/client/modeller/modeller-route.ts +9 -0
  26. package/client/modeller/mounting-part.ts +15 -0
  27. package/client/modeller/newest-answer.ts +21 -0
  28. package/client/modeller/preview-model.ts +5 -0
  29. package/client/modeller/refusals.ts +168 -82
  30. package/client/modeller/section-controls.ts +25 -1
  31. package/client/modeller/section-drawing.ts +43 -1
  32. package/client/modeller/slot-layout-host.ts +140 -0
  33. package/client/pages/figure-modeller-page.ts +6 -5
  34. package/client/viewparts/detail-route.ts +7 -0
  35. package/client/viewparts/figure-detail-dock.ts +1 -3
  36. package/dist-client/bootstrap.d.ts +1 -1
  37. package/dist-client/bootstrap.js +9 -2
  38. package/dist-client/bootstrap.js.map +1 -1
  39. package/dist-client/graphql/index.d.ts +5 -0
  40. package/dist-client/graphql/index.js +19 -0
  41. package/dist-client/graphql/index.js.map +1 -1
  42. package/dist-client/modeller/canvas.d.ts +2 -0
  43. package/dist-client/modeller/canvas.js +10 -2
  44. package/dist-client/modeller/canvas.js.map +1 -1
  45. package/dist-client/modeller/carrier-declaration.d.ts +31 -0
  46. package/dist-client/modeller/carrier-declaration.js +53 -0
  47. package/dist-client/modeller/carrier-declaration.js.map +1 -0
  48. package/dist-client/modeller/editor.d.ts +13 -0
  49. package/dist-client/modeller/editor.js +62 -27
  50. package/dist-client/modeller/editor.js.map +1 -1
  51. package/dist-client/modeller/figure-carrier.d.ts +26 -0
  52. package/dist-client/modeller/figure-carrier.js +123 -0
  53. package/dist-client/modeller/figure-carrier.js.map +1 -0
  54. package/dist-client/modeller/figure-cavity-drawing.js +3 -11
  55. package/dist-client/modeller/figure-cavity-drawing.js.map +1 -1
  56. package/dist-client/modeller/figure-first-save.d.ts +4 -0
  57. package/dist-client/modeller/figure-first-save.js +5 -8
  58. package/dist-client/modeller/figure-first-save.js.map +1 -1
  59. package/dist-client/modeller/figure-history-panel.js +2 -4
  60. package/dist-client/modeller/figure-history-panel.js.map +1 -1
  61. package/dist-client/modeller/figure-links.d.ts +4 -1
  62. package/dist-client/modeller/figure-links.js +47 -57
  63. package/dist-client/modeller/figure-links.js.map +1 -1
  64. package/dist-client/modeller/figure-material-panel.d.ts +0 -1
  65. package/dist-client/modeller/figure-material-panel.js +4 -14
  66. package/dist-client/modeller/figure-material-panel.js.map +1 -1
  67. package/dist-client/modeller/figure-mesh-choice.d.ts +16 -0
  68. package/dist-client/modeller/figure-mesh-choice.js +75 -0
  69. package/dist-client/modeller/figure-mesh-choice.js.map +1 -0
  70. package/dist-client/modeller/figure-parts.d.ts +1 -1
  71. package/dist-client/modeller/figure-parts.js +22 -0
  72. package/dist-client/modeller/figure-parts.js.map +1 -1
  73. package/dist-client/modeller/figure-preview.js +2 -2
  74. package/dist-client/modeller/figure-preview.js.map +1 -1
  75. package/dist-client/modeller/figure-properties.d.ts +2 -1
  76. package/dist-client/modeller/figure-properties.js +5 -3
  77. package/dist-client/modeller/figure-properties.js.map +1 -1
  78. package/dist-client/modeller/figure-section-editor.d.ts +2 -2
  79. package/dist-client/modeller/figure-section-editor.js +12 -33
  80. package/dist-client/modeller/figure-section-editor.js.map +1 -1
  81. package/dist-client/modeller/figure-section-panel.js +6 -7
  82. package/dist-client/modeller/figure-section-panel.js.map +1 -1
  83. package/dist-client/modeller/figure-slot-layout-panel.d.ts +36 -0
  84. package/dist-client/modeller/figure-slot-layout-panel.js +198 -0
  85. package/dist-client/modeller/figure-slot-layout-panel.js.map +1 -0
  86. package/dist-client/modeller/figure-verdict.d.ts +1 -1
  87. package/dist-client/modeller/figure-verdict.js +19 -37
  88. package/dist-client/modeller/figure-verdict.js.map +1 -1
  89. package/dist-client/modeller/first-save.d.ts +10 -0
  90. package/dist-client/modeller/first-save.js +16 -0
  91. package/dist-client/modeller/first-save.js.map +1 -1
  92. package/dist-client/modeller/links.d.ts +26 -1
  93. package/dist-client/modeller/links.js +43 -0
  94. package/dist-client/modeller/links.js.map +1 -1
  95. package/dist-client/modeller/material-edit.d.ts +11 -0
  96. package/dist-client/modeller/material-edit.js +28 -0
  97. package/dist-client/modeller/material-edit.js.map +1 -0
  98. package/dist-client/modeller/maturity.d.ts +15 -0
  99. package/dist-client/modeller/maturity.js +23 -0
  100. package/dist-client/modeller/maturity.js.map +1 -1
  101. package/dist-client/modeller/mesh-import.d.ts +47 -0
  102. package/dist-client/modeller/mesh-import.js +47 -0
  103. package/dist-client/modeller/mesh-import.js.map +1 -0
  104. package/dist-client/modeller/modeller-route.d.ts +5 -0
  105. package/dist-client/modeller/modeller-route.js +9 -0
  106. package/dist-client/modeller/modeller-route.js.map +1 -1
  107. package/dist-client/modeller/mounting-part.d.ts +14 -0
  108. package/dist-client/modeller/mounting-part.js +10 -0
  109. package/dist-client/modeller/mounting-part.js.map +1 -0
  110. package/dist-client/modeller/newest-answer.d.ts +12 -0
  111. package/dist-client/modeller/newest-answer.js +21 -0
  112. package/dist-client/modeller/newest-answer.js.map +1 -0
  113. package/dist-client/modeller/preview-model.d.ts +2 -0
  114. package/dist-client/modeller/preview-model.js +4 -0
  115. package/dist-client/modeller/preview-model.js.map +1 -1
  116. package/dist-client/modeller/refusals.d.ts +26 -5
  117. package/dist-client/modeller/refusals.js +184 -86
  118. package/dist-client/modeller/refusals.js.map +1 -1
  119. package/dist-client/modeller/section-controls.d.ts +17 -1
  120. package/dist-client/modeller/section-controls.js +18 -0
  121. package/dist-client/modeller/section-controls.js.map +1 -1
  122. package/dist-client/modeller/section-drawing.d.ts +24 -1
  123. package/dist-client/modeller/section-drawing.js +42 -0
  124. package/dist-client/modeller/section-drawing.js.map +1 -1
  125. package/dist-client/modeller/slot-layout-host.d.ts +81 -0
  126. package/dist-client/modeller/slot-layout-host.js +105 -0
  127. package/dist-client/modeller/slot-layout-host.js.map +1 -0
  128. package/dist-client/pages/figure-modeller-page.js +5 -4
  129. package/dist-client/pages/figure-modeller-page.js.map +1 -1
  130. package/dist-client/tsconfig.tsbuildinfo +1 -1
  131. package/dist-client/viewparts/detail-route.d.ts +5 -0
  132. package/dist-client/viewparts/detail-route.js +8 -0
  133. package/dist-client/viewparts/detail-route.js.map +1 -0
  134. package/dist-client/viewparts/figure-detail-dock.d.ts +0 -1
  135. package/dist-client/viewparts/figure-detail-dock.js +1 -3
  136. package/dist-client/viewparts/figure-detail-dock.js.map +1 -1
  137. package/package.json +9 -8
  138. package/test/align.test.ts +0 -9
  139. package/test/browser/assistant-stand-in.js +32 -0
  140. package/test/browser/build.check.mjs +77 -0
  141. package/test/browser/build.mjs +31 -6
  142. package/test/browser/editing-page.js +224 -0
  143. package/test/browser/editing.check.mjs +550 -0
  144. package/test/browser/links-page.js +113 -0
  145. package/test/browser/links.check.mjs +568 -0
  146. package/test/browser/modeller.check.mjs +21 -0
  147. package/test/browser/parts-page.js +120 -0
  148. package/test/browser/parts.check.mjs +328 -0
  149. package/test/browser/places-mesh-page.js +36 -0
  150. package/test/browser/places-mesh.check.mjs +239 -0
  151. package/test/browser/product-page.check.mjs +682 -0
  152. package/test/browser/product-page.js +276 -0
  153. package/test/browser/screen-page.js +112 -0
  154. package/test/browser/screen-product-page.js +62 -0
  155. package/test/browser/screen.check.mjs +1003 -0
  156. package/test/browser/section-page.js +256 -0
  157. package/test/browser/section.check.mjs +719 -0
  158. package/test/browser/serve.mjs +20 -12
  159. package/test/carrier-declaration.test.ts +69 -0
  160. package/test/cavity-drawing.test.ts +2 -18
  161. package/test/clip-ai-proposal.test.ts +1 -5
  162. package/test/clip-consent.test.ts +0 -5
  163. package/test/clips.test.ts +0 -7
  164. package/test/command-preview.test.ts +0 -68
  165. package/test/controls.test.ts +0 -9
  166. package/test/detail-dock.test.ts +6 -25
  167. package/test/face-mapping.test.ts +1 -20
  168. package/test/fasten-drawing.test.ts +0 -23
  169. package/test/fields.test.ts +6 -33
  170. package/test/figure-assistant-surface.test.ts +32 -38
  171. package/test/first-minute.test.ts +3 -55
  172. package/test/first-save-suggest.test.ts +11 -8
  173. package/test/first-save.test.ts +4 -26
  174. package/test/gizmo.test.ts +0 -25
  175. package/test/joint-fan.test.ts +0 -10
  176. package/test/keys.test.ts +0 -7
  177. package/test/layout.test.ts +4 -73
  178. package/test/links.test.ts +48 -67
  179. package/test/live.test.ts +0 -34
  180. package/test/material.test.ts +17 -26
  181. package/test/maturity.test.ts +13 -16
  182. package/test/mesh-import.test.ts +76 -0
  183. package/test/modeller-route.test.ts +3 -19
  184. package/test/package-download.test.ts +6 -7
  185. package/test/parts-wiring.test.ts +9 -51
  186. package/test/preview.test.ts +5 -46
  187. package/test/properties.test.ts +11 -26
  188. package/test/proposal-record.test.ts +2 -19
  189. package/test/recipes.test.ts +0 -15
  190. package/test/refusals.test.ts +257 -151
  191. package/test/release-entry.test.ts +1 -9
  192. package/test/release-panel.test.ts +1 -22
  193. package/test/resize-with-placement.test.ts +0 -32
  194. package/test/revert-choice.test.ts +1 -22
  195. package/test/revert-keeping.test.ts +1 -12
  196. package/test/save-landed.test.ts +16 -14
  197. package/test/scene-canvas.test.ts +2 -13
  198. package/test/section-editor.test.ts +30 -83
  199. package/test/section-panel.test.ts +22 -17
  200. package/test/slot-layout-host.test.ts +117 -0
  201. package/test/theme.test.ts +5 -9
  202. package/test/verdict.test.ts +44 -41
  203. package/test/view.test.ts +5 -12
  204. package/translations/en.json +61 -1
  205. package/translations/es.json +61 -1
  206. package/translations/ja.json +61 -1
  207. package/translations/ko.json +61 -1
  208. package/translations/ms.json +61 -1
  209. package/translations/zh.json +61 -1
  210. package/test/thumbnail.test.ts +0 -29
@@ -1,58 +1,52 @@
1
1
  /*
2
2
  * The modeller's AI - the framework dock talks about the figure on screen (design/ai/shared-dock.md §3).
3
3
  *
4
- * What this holds: ① a saved figure provides a surface anchored on its row ② the draft is read at the moment of
5
- * sending, not when the surface was provided ③ an unsaved figure provides nothing - no conversation without a row
6
- * ④ the page takes the surface back when it goes inactive or leaves. The page pulls in the shell and things-scene,
7
- * so this reads its source.
4
+ * What the page does with the dock -- ① a saved figure provides a surface anchored on its row ② the draft is read at the
5
+ * moment of sending ③ an unsaved figure provides nothing ④ the surface is taken back when the page goes inactive or
6
+ * leaves ⑤ a proposal is taken through the page, and a new figure's opens a new figure -- is checked on the page itself
7
+ * in test/browser/product-page.check.mjs. Here: the offer the page takes it by, and the words every outcome is said in.
8
8
  */
9
9
  import assert from 'node:assert/strict'
10
10
  import { readFileSync } from 'node:fs'
11
11
  import { fileURLToPath } from 'node:url'
12
12
  import { test } from 'node:test'
13
+ import { addFigureBox, createFigureAsset, serializeFigureAsset } from '@hatiolab/figure-model'
13
14
 
14
- const source = readFileSync(fileURLToPath(new URL('../client/pages/figure-modeller-page.ts', import.meta.url)), 'utf8')
15
+ import { createFigureProductSession } from '../client/modeller/product-session.js'
16
+ import { takeFigureOffer, type FigureApplyDetail } from '../client/modeller/ai-proposal.js'
15
17
 
16
- test('a saved figure provides a surface anchored on its row', () => {
17
- assert.match(source, /provideAssistantSurface\(this, \{/)
18
- assert.match(source, /subject: 'figure'/)
19
- assert.match(source, /anchor: \(\) => \(\{ anchorType: 'figure', anchorId: id \}\)/)
20
- })
21
-
22
- test('the draft is read when the message is sent, as host context the figure tools read', () => {
23
- assert.match(source, /hostContext: \(\) => \(\{[\s\S]*?figureId: id,[\s\S]*?draftSource: JSON\.parse\(this\.session\.source\(\)\),[\s\S]*?context: this\.session\.read\(\)\.context,\s*revision: this\.session\.read\(\)\.revision,/)
24
- })
25
-
26
- test('an unsaved figure provides nothing', () => {
27
- assert.match(source, /if \(!view\.id\) \{\s*this\.withdrawAssistant\(\)/)
28
- })
29
-
30
- test('the surface is taken back when the page goes inactive or leaves', () => {
31
- const withdraws = source.match(/this\.withdrawAssistant\(\)/g) ?? []
32
- assert.ok(withdraws.length >= 3, 'unsaved, inactive and disconnected each withdraw')
33
- assert.doesNotMatch(source, /figure-ai-context/, 'the old window-event channel is gone')
34
- })
18
+ const say = (key: string) => key
19
+ const K1 = '11111111-1111-4111-8111-111111111111'
20
+ const lid = [{ kind: 'add-part', part: { id: 'lid', kind: 'box', dimensions: { width: 100, height: 10, depth: 100 } } }] as never[]
35
21
 
36
- test('the dock applies a proposal through the page, on the version the model saw', () => {
37
- assert.match(source, /window\.addEventListener\('figure-apply', this\.applyAI\)/)
38
- /* The page takes it by `takeFigureOffer` (ai-proposal.ts), tested there on the session itself. */
39
- assert.match(source, /takeFigureOffer\(this\.session, detail, \{/)
40
- const offer = readFileSync(fileURLToPath(new URL('../client/modeller/ai-proposal.ts', import.meta.url)), 'utf8')
41
- assert.match(offer, /session\.applyProposal\(detail\.steps, detail\.revision!, detail\.context!, detail\.key\)/)
42
- assert.match(source, /this\.takeNewFigureProposal\(\)/, 'a new figure opened for a proposal takes its steps')
22
+ test('the dock applies a proposal through the page, on the version the model saw', async () => {
23
+ const source = serializeFigureAsset(addFigureBox(createFigureAsset('A'), { id: 'body', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 } }))
24
+ const session = createFigureProductSession({ load: async (id: string) => ({ id, source }), save: async (q: { id?: string; source: string }) => ({ id: q.id ?? 'a', source: q.source }) })
25
+ await session.open('a')
26
+ const seen = session.read()
27
+ /* The person edits meanwhile: the version the model saw is gone, and the proposal is refused, not put on top. */
28
+ session.command({ kind: 'rename', name: 'B' } as never)
29
+ const stale: FigureApplyDetail = { key: K1, steps: lid, create: false, context: seen.context, revision: seen.revision }
30
+ assert.equal(takeFigureOffer(session, stale, { onSavedFigure: true, say }), false)
31
+ assert.deepEqual({ applied: stale.applied, code: stale.code }, { applied: false, code: 'REVISION_CONFLICT' })
32
+ assert.deepEqual(session.proposalsInEffect(), [])
33
+ /* On the version now: applied as one step, under the proposal's key. */
34
+ const now = session.read()
35
+ const fresh: FigureApplyDetail = { key: K1, steps: lid, create: false, context: now.context, revision: now.revision }
36
+ assert.equal(takeFigureOffer(session, fresh, { onSavedFigure: true, say }), true)
37
+ assert.equal(fresh.applied, true)
38
+ assert.equal(session.read().revision, now.revision! + 1)
39
+ assert.deepEqual(session.proposalsInEffect(), [K1])
40
+ /* A new figure's proposal is not for a page on a saved figure: left untaken, so the handler opens a new one. */
41
+ const create: FigureApplyDetail = { key: K1, steps: lid, create: true }
42
+ assert.equal(takeFigureOffer(session, create, { onSavedFigure: true, say }), false)
43
+ assert.equal(create.applied, undefined)
43
44
  })
44
45
 
45
46
  test('a figure proposal is answered by its handler, in words, never left without one', () => {
46
- const handler = readFileSync(fileURLToPath(new URL('../client/figure-assistant.ts', import.meta.url)), 'utf8')
47
- assert.match(handler, /registerProposalHandler\(FIGURE_CANDIDATE_TOOL,/)
48
- assert.match(handler, /export const FIGURE_CANDIDATE_TOOL = 'proposeFigureCandidate'/)
49
- assert.match(handler, /runFigureProposal\(/)
50
- assert.match(handler, /context\.notify\(/)
51
47
  for (const lang of ['en', 'ko', 'ja', 'zh', 'ms', 'es']) {
52
48
  const words = JSON.parse(readFileSync(fileURLToPath(new URL(`../translations/${lang}.json`, import.meta.url)), 'utf8'))
53
49
  for (const key of ['applied', 'stale', 'refused', 'needs-modeller', 'not-opened', 'new-figure-unsaved'])
54
50
  assert.ok(words[`figure.text.proposal-${key}`], `${lang} has no words for proposal-${key}`)
55
51
  }
56
- const index = readFileSync(fileURLToPath(new URL('../client/index.ts', import.meta.url)), 'utf8')
57
- assert.match(index, /figure-assistant\.js/)
58
52
  })
@@ -31,78 +31,26 @@ test('a selected part at the head: its kind, and what its size follows only when
31
31
  assert.ok(!/규칙을 따름|규칙으로 결정됨|규칙이 정하는|도형을 따라감|크기 고정|붙임 없음/.test(JSON.stringify(ko)), 'the words nobody knew are gone')
32
32
  })
33
33
 
34
- test('a new figure asks what to start with -- a shape, a recipe, an AI draft where offered -- and adds nothing by itself', () => {
35
- const editor = read('../client/modeller/editor.ts')
36
- const start = editor.slice(editor.indexOf(' private renderStart() {'), editor.indexOf(' /**\n * Which part the panel speaks of'))
37
- assert.ok(start.includes("['box', 'cylinder', 'plate']") && start.includes('@click=${() => this.add(kind)}'), 'a shape, on the author\'s click')
38
- assert.ok(start.includes('this.catalogOpen = true'), 'a recipe')
39
- assert.ok(start.includes('${this.aiDraft') && start.includes("new CustomEvent('ask-ai-draft'"), 'an AI draft, only where the page offers it')
40
- assert.ok(editor.includes('${view.geometry.length === 0 && !unavailable ? this.renderStart() : nothing}'), 'only while the figure is empty')
41
- assert.ok(editor.includes('<summary>${i18next.t(\'figure.label.figure-info-later\')}</summary>'), 'the figure\'s own settings wait, folded')
42
- const page = read('../client/pages/figure-modeller-page.ts')
43
- assert.ok(page.includes("aiDraft @ask-ai-draft=${() => askAssistant({ draft: i18next.t('figure.text.ai-draft-request') })}"), 'the app puts a draft request in the dock, not sent')
44
- const parts = read('../client/modeller/figure-parts.ts')
45
- assert.ok(parts.includes('if (!b || (!this.parts.length && !b.level)) return nothing'), 'the detail level comes after the first part (U09)')
46
- })
47
-
48
34
  test('the side tabs say their names, in every language', () => {
49
- const editor = read('../client/modeller/editor.ts')
50
- assert.ok(editor.includes("const name = i18next.t('figure.label.tab-short-' + tab)"))
51
- assert.ok(editor.includes('aria-label=${name}'), 'its accessible name is the name it shows')
52
- assert.ok(editor.includes('<span tab-label aria-hidden="true">${name}</span>'))
35
+ /* The name under each icon, and its accessible name, are the browser check's (test/browser/screen.check.mjs). */
53
36
  for (const lang of ['ko', 'en', 'ja', 'zh', 'ms', 'es']) {
54
37
  const table = JSON.parse(read(`../translations/${lang}.json`))
55
38
  for (const tab of ['properties', 'capability', 'motion', 'verdict', 'view', 'history']) assert.ok(table[`figure.label.tab-short-${tab}`], `${lang} ${tab}`)
56
39
  }
57
40
  })
58
41
 
59
- test('narrow, the parts list folds for the canvas, said once, and stays open once the author opens it', () => {
60
- const editor = read('../client/modeller/editor.ts')
61
- const fit = editor.slice(editor.indexOf(' private fitPanesTo(width: number) {'), editor.indexOf(' /**\n * Refusals and notices'))
62
- assert.ok(fit.includes('if (this.foldedForWidth && !panes.foldStart) this.keptOpenByAuthor = true'), 'opened by the author, left open')
63
- assert.ok(fit.includes("notify({ level: 'info', message: i18next.t('figure.text.parts-panel-folded-narrow') })"), 'said')
64
- assert.ok(fit.includes('if (this.foldedForWidth && panes.foldStart) panes.foldStart = false'), 'wide again, opened again')
65
- assert.ok(editor.includes('private static readonly FOLD_START_UNDER = 1100'))
66
- })
67
-
68
- test('a palette part\'s own sizes are its shape\'s fields, not a rule\'s: no "set by a rule" size row for them', () => {
69
- const editor = read('../client/modeller/editor.ts')
70
- assert.ok(editor.includes("sizeRuled: (['x', 'y', 'z'] as const).every(axis => followOf(shown.asset, ownerOf(this.selected), axis).of !== 'own')"))
71
- assert.ok(editor.includes('if (!held?.sizeRuled ||'))
72
- })
73
-
74
42
  /* Designer 2026-09-27: the placement is chosen before any part, as the figure's own setting -- no room, no part made up. */
75
43
  test('the start card and the figure panel choose the placement as the figure\'s own setting', async () => {
76
44
  const { createFigureAsset, applyFigureAuthoring } = await import('@hatiolab/figure-model')
77
45
  const hung = applyFigureAuthoring(createFigureAsset('New'), { kind: 'set-placement', placement: 'ceiling' })
78
46
  assert.equal(hung.placement, 'ceiling')
79
47
  assert.equal(hung.occupancy, undefined)
80
- const editor = read('../client/modeller/editor.ts')
81
- assert.ok(editor.includes("this.linkCommand({ kind: 'set-placement', placement: one })"), 'on the start card')
82
- const properties = read('../client/modeller/figure-properties.ts')
83
- assert.ok(properties.includes("this.command({ kind: 'set-placement', placement: select.value as Placement })"), 'in the figure panel')
84
- assert.ok(!properties.includes('?disabled=${!facts.parts.length}\n .value=${live(current)}'), 'not waiting for a part')
48
+ /* Chosen on the start card and in the figure panel, before any part: the browser check's (test/browser/screen.check.mjs). */
85
49
  })
86
50
 
87
51
  /* B.1 and the designer's items (2026-09-27): each target's panel is found as it was left; the fastening form says which it is. */
88
- test('the panel remembers its scroll and folds per model and part, and forgets them for another model', () => {
89
- const editor = read('../client/modeller/editor.ts')
90
- assert.ok(editor.includes("if (panel) this.panelMemory.set(was || '@model', { top: panel.scrollTop, open: this.foldsOf(panel).map(d => d.open) })"))
91
- assert.ok(editor.includes('panel.scrollTop = kept?.top ?? 0'))
92
- assert.ok(editor.includes('this.panelMemory.clear()'), 'another model starts clean')
93
- assert.ok(editor.includes("position: sticky;"), 'the path stays in view whatever the scroll')
94
- })
95
-
96
52
  test('the fastening form says whether it edits the current relation or fastens anew', () => {
97
- const links = read('../client/modeller/figure-links.ts')
98
- /* Anew is said; the one it has is said once, by the sentence over the drawing (user 2026-10-01: no third line). */
99
- assert.ok(links.includes("${current ? nothing : html`<p fasten-mode>${editing}</p>`}"))
100
- assert.ok(links.includes('<p fasten-sentence>'))
101
- /* A fastening the part has is changed as soon as something is chosen (2026-10-02): 붙이기 only for a new one. */
102
- assert.ok(links.includes("${held\n ? nothing\n : html`<button act ?disabled=${this.locked || !action} @click=${() => action && this.command(action)}>"))
103
- assert.ok(links.includes('if (value) settle()'), 'another target taken at once')
104
- assert.ok(links.includes("action && !held && this.working === 'fasten' ? this.renderPreviewWords() : nothing"), 'no words for a command no button sends')
105
- assert.ok(links.includes('const settle = () => this.onFastenRelease(!!held)'))
53
+ /* Anew is said, the one it has changed at once, 붙이기 only for a new one: the browser check's (test/browser/screen.check.mjs). Here, the words. */
106
54
  const ko = JSON.parse(read('../translations/ko.json'))
107
55
  assert.equal(ko['figure.text.fasten-new'], '새로 붙이기')
108
56
  assert.equal(ko['figure.button.fasten-update'], undefined)
@@ -8,7 +8,7 @@ import { readFileSync } from 'node:fs'
8
8
  import { fileURLToPath } from 'node:url'
9
9
  import { isPlaceholderType, TYPE_LENGTH, TYPE_PATTERN } from '@hatiolab/figure-model'
10
10
 
11
- import { suggestTypeOf, releasableType } from '../client/modeller/first-save.js'
11
+ import { offeredTypeOf, suggestTypeOf, releasableType } from '../client/modeller/first-save.js'
12
12
 
13
13
  const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
14
14
 
@@ -27,14 +27,17 @@ test('an identifier is offered for any name: plain letters as they are, anything
27
27
  assert.equal(suggestTypeOf('선반', ['fig-000000'], () => draws[n++ % draws.length]!), 'fig-iiiiii')
28
28
  })
29
29
 
30
+ /* The question itself -- the name first and focused, a retry on the identifier answered -- is checked in product-page.check.mjs. */
30
31
  test('the first save asks the name first; the identifier offered stays through retries, and renaming does not change it', () => {
31
- const popup = read('../client/modeller/figure-first-save.ts')
32
- assert.ok(popup.indexOf('<label for="name">') < popup.indexOf('<label for="type">'), 'the name first')
33
- assert.ok(/id="name"\s+autofocus/.test(popup), 'the name has the focus')
34
- assert.ok(!popup.includes('this.type = this.suggested = suggestTypeOf(this.name, this.taken)\n }}'), 'typing a name does not redraw it')
35
- assert.ok(popup.includes('if (!this.type || offeredAndTaken) this.type = this.suggested = suggestTypeOf(this.name, this.taken)'), 'offered again only when empty, or offered and taken')
36
- const page = read('../client/pages/figure-modeller-page.ts')
37
- assert.ok(page.includes('if (answer) this.row = { ...this.row, type: answer.type }'), 'a retry opens on the identifier answered before')
32
+ /* None yet: one is offered, from the name. */
33
+ assert.deepEqual(offeredTypeOf('', 'Lift table', []), { type: 'lift-table', suggested: 'lift-table' })
34
+ /* Offered before and still free, or typed by the author: kept as it is. */
35
+ assert.deepEqual(offeredTypeOf('fig-abc123', 'Lift table', []), { type: 'fig-abc123', suggested: 'fig-abc123' })
36
+ assert.deepEqual(offeredTypeOf('my-lift', 'Other name', []), { type: 'my-lift' })
37
+ /* Offered and found taken: offered again. Typed and taken: kept as typed -- its refusal is said when saving. */
38
+ assert.deepEqual(offeredTypeOf('fig-abc123', 'Lift table', ['fig-abc123']), { type: 'lift-table', suggested: 'lift-table' })
39
+ assert.deepEqual(offeredTypeOf('my-lift', 'Lift table', ['my-lift']), { type: 'my-lift' })
40
+ assert.deepEqual(offeredTypeOf('fig-abc123', '선반', ['fig-abc123'], () => 0), { type: 'fig-000000', suggested: 'fig-000000' })
38
41
  for (const lang of ['en', 'ja', 'ko', 'ms', 'zh', 'es']) {
39
42
  const t = JSON.parse(read(`../translations/${lang}.json`))
40
43
  assert.ok(t['figure.label.figure-identifier'] && t['figure.text.identifier-suggested'], lang)
@@ -66,29 +66,7 @@ test('타입을 주지 않으면 싣지 않는다 — 서버가 임시 이름을
66
66
  assert.equal('type' in requests[0]!, false)
67
67
  })
68
68
 
69
- test('저장 전에 적은 타입은 처음 저장의 물음에 실려 온다 — 칸이 막혀 있어 적어도 사라졌다 (09-25)', async () => {
70
- const { readFileSync } = await import('node:fs')
71
- const page = readFileSync(new URL('../client/pages/figure-modeller-page.ts', import.meta.url), 'utf8')
72
- const field = page.slice(page.indexOf('<label slot="type" type-field>'), page.indexOf('</label>', page.indexOf('<label slot="type" type-field>')))
73
- assert.doesNotMatch(field, /disabled=\$\{!view\.id\}/, '저장 전에도 칸에 적을 수 있다')
74
- const rename = page.slice(page.indexOf('private renameType'), page.indexOf('private fitToParts'))
75
- assert.match(rename, /if \(!id\) \{\s*this\.row = \{ \.\.\.this\.row, type: wanted \|\| undefined \}/, '저장 전에는 행 대신 화면이 타입을 쥔다')
76
- assert.match(page, /<figure-first-save \.type=\$\{this\.row\.type \?\? ''\}/, '물음은 그 타입으로 열린다')
77
- })
78
-
79
- test('처음 저장한 도형은 주소가 제 것이 된다 — 모델러를 다시 열면 새 도형이 열린다 (2026-09-25)', async () => {
80
- const { readFileSync } = await import('node:fs')
81
- const page = readFileSync(new URL('../client/pages/figure-modeller-page.ts', import.meta.url), 'utf8')
82
- const changed = page.slice(page.indexOf('private changed = () => {'), page.indexOf('private async readRow'))
83
- assert.match(changed, /if \(view\.id && !this\.routeId && !view\.saving\) \{\s*this\.routeId = view\.id\s*navigate\(modellerPathOf\(\{ id: view\.id \}\), true\)/, '저장으로 id 를 얻으면 그 주소로 바꾼다(기록은 덮어쓴다)')
84
- /* The early return that kept the old figure reads the route id; it now matches only the figure the address names. */
85
- assert.match(page, /if \(this\.session\.read\(\)\.context && id === this\.routeId && !changes\.active\) return/)
86
- })
87
-
88
- test('다른 도형을 열면 행을 먼저 비운다 — 앞 도형의 타입이 칸에 남지 않는다 (C2, 2026-09-25)', async () => {
89
- const { readFileSync } = await import('node:fs')
90
- const page = readFileSync(new URL('../client/pages/figure-modeller-page.ts', import.meta.url), 'utf8')
91
- const updated = page.slice(page.indexOf('async pageUpdated'), page.indexOf('private askFirstSave'))
92
- const opening = updated.slice(updated.indexOf('if (id) {'), updated.indexOf('this.session.open(id'))
93
- assert.match(opening, /this\.row = \{ version: 0 \}/, '열기 전에 행을 비운다')
94
- })
69
+ /*
70
+ On the page -- a type written before the first save carried into its question, the address made the figure's own,
71
+ the row emptied before another figure opens -- see test/browser/product-page.check.mjs.
72
+ */
@@ -81,15 +81,10 @@ test('크기는 배율만큼 치수를 바꾸고 1mm 아래로 가지 않는다'
81
81
  test('끈 것은 세션의 한 걸음이다 — 한 번의 되돌리기로 돌아간다', () => {
82
82
  const workspace = createFigureEditorWorkspace() as unknown as Record<string, Function>
83
83
  assert.ok(typeof workspace.apply === 'function')
84
- const editor = read('../client/modeller/editor.ts')
85
- const moved = editor.slice(editor.indexOf(' private transformed('), editor.indexOf(' /** 맞추기'))
86
- assert.ok(moved.includes('this.run(() => this.session!.apply(changes))'), 'one apply, one step')
87
- assert.ok(moved.includes("kind: 'nudge-part',"), 'a nudge is one command, one step')
88
84
  })
89
85
 
90
86
  test('캔버스 — 기즈모 · W E R · 스냅은 씬의 편집 모드가 한다', () => {
91
87
  const canvas = read('../client/modeller/canvas.ts')
92
- assert.match(canvas, /mode: 1\n/)
93
88
  assert.doesNotMatch(canvas, /TransformControls|OrbitControls/)
94
89
  })
95
90
 
@@ -110,14 +105,6 @@ test('떼어 낸 부품은 다시 옮길 수 있다 — 움직임이 있는 부
110
105
  assert.equal(gizmoReachOf(slid as never, 'lid').translate, false, 'not its own numbers: a drag on a joint edits the joint (joint-drag)')
111
106
  })
112
107
 
113
- test('끄는 동안은 모아 두고 놓을 때 한 번 적는다 — 크기 기즈모가 편집기와 싸우지 않는다', () => {
114
- const canvas = read('../client/modeller/canvas.ts')
115
- const watch = canvas.slice(canvas.indexOf(' private watchScene()'), canvas.indexOf(' /** Components the gizmo changed'))
116
- assert.ok(watch.includes('this.pending.set(name, origin!)') && watch.includes('if (!this.pointerDown) this.commitSoon(80)'), '사용자 2026-09-24: 「기즈모 컨트롤 중에 scale은 제대로 작동하지 않는군」')
117
- assert.ok(!watch.includes("new CustomEvent('part-transformed'"), '움직일 때마다 알리지 않는다')
118
- assert.ok(canvas.includes('private onPointerUp = () => {') && canvas.includes('this.commitSoon(0)'))
119
- })
120
-
121
108
 
122
109
  test('원기둥 · 구도 크기 기즈모가 먹는다 — 단면은 원으로', async () => {
123
110
  const { addFigurePrimitive, createFigureAsset } = await import('@hatiolab/figure-model')
@@ -167,22 +154,10 @@ test('크기 칸과 손잡이 — 배치가 지키는 면을 지킨다, 커널
167
154
  let hung = addFigureBox(createFigureAsset('Lamp'), { id: 'lamp', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: -50, z: 0 } })
168
155
  hung = applyFigureAuthoring(hung, { kind: 'declare-occupancy', proposal: proposeFigureOccupancy(hung), placement: 'ceiling' })
169
156
  assert.equal(centreY(grow(hung, 'lamp', gizmoReachOf(hung, 'lamp').sizes![1], 300), 'lamp') + 300 / 2, 0, '천장 도형: 윗면이 천장에')
170
- const editor = read('../client/modeller/editor.ts')
171
- assert.ok(editor.includes('figureSizeEditChanges(this.shown()!.asset, part, [change])'), '숫자 칸이 부른다')
172
- assert.ok(editor.includes('figureSizeEditChanges(asset, ownerOf(id), written)'), '손잡이도 부른다')
173
- assert.ok(!read('../client/modeller/gizmo.ts').includes('keepBottomChanges'), '화면의 제 산수는 없다')
174
157
  })
175
158
 
176
159
  /* 손잡이는 키를 몰라도 찾는다 — 도구 막대에 옮기기 · 돌리기 · 크기, 제 숫자로 놓인 부품에만 (09-26). */
177
160
  test('도구 막대의 옮기기 · 돌리기 · 크기 — 키와 같은 길, 되쓸 수 있는 것만', () => {
178
- const editor = read('../client/modeller/editor.ts')
179
- const bar = editor.slice(editor.indexOf('return html`<ox-canvas-bar'), editor.indexOf('</ox-canvas-bar>'))
180
- assert.ok(bar.includes("this.renderGizmoTools(!unavailable && this.middle === 'edit')"), '편집 중에만')
181
- const tools = editor.slice(editor.indexOf(' private renderGizmoTools('), editor.indexOf(" /** The modeller's keys"))
182
- for (const [mode, key, can] of [['translate', 'W', 'translate'], ['rotate', 'E', 'rotate'], ['scale', 'R', 'scale']])
183
- assert.ok(tools.includes(`mode: '${mode}'`) && tools.includes(`key: '${key}'`) && tools.includes(`can: !!reach?.${can}`), mode)
184
- assert.ok(tools.includes('@click=${() => this.useGizmo(tool.mode)}'))
185
- assert.match(editor, /action === 'scale'\) canvas && this\.useGizmo\(action\)/, '키도 같은 길로 — 눌린 단추가 맞다')
186
161
  const ko = JSON.parse(read('../translations/ko.json'))
187
162
  assert.deepEqual(['translate', 'rotate', 'scale'].map(m => ko['figure.button.gizmo-' + m]), ['옮기기', '돌리기', '크기 바꾸기'])
188
163
  })
@@ -16,8 +16,6 @@ import { revisionOf } from './revision-of.ts'
16
16
  import { liveOverSession } from '../client/modeller/live.ts'
17
17
  import { moving } from './joints.ts'
18
18
 
19
- const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
20
-
21
19
  function arm(): FigureAsset {
22
20
  let asset = createFigureAsset('Probe')
23
21
  asset = addFigureBox(asset, { id: 'base', size: { x: 200, y: 100, z: 200 }, position: { x: 0, y: 50, z: 0 } })
@@ -84,14 +82,6 @@ test('no fan for a part that does not turn, nor for a slide -- a fan in a guesse
84
82
  assert.equal(jointFanOf(revisionOf(slider), 'block'), null)
85
83
  })
86
84
 
87
- test('the editor draws it for the selected part; the canvas never lets it be picked', () => {
88
- const editor = read('../client/modeller/editor.ts')
89
- assert.ok(editor.includes('.fan=${this.fanOf(shown, live)}'))
90
- const canvas = read('../client/modeller/canvas.ts')
91
- /* The fan, and what a command would do, are shown and never picked (ADR-0097 §5.5). */
92
- assert.ok(canvas.includes('.map(c => ({ ...c, capturable: false, locked: true }))'))
93
- })
94
-
95
85
  /*
96
86
  * The fan stands where the joint is drawn: the pose set with the controls (the version's inputs), and while a control is
97
87
  * dragged, the pose in hand -- the same as a version where that pose was set (user 2026-09-24: the arm jumped on release).
package/test/keys.test.ts CHANGED
@@ -3,8 +3,6 @@
3
3
  */
4
4
  import { test } from 'node:test'
5
5
  import assert from 'node:assert/strict'
6
- import { readFileSync } from 'node:fs'
7
- import { fileURLToPath } from 'node:url'
8
6
 
9
7
  import { keyActionOf } from '../client/modeller/keys.ts'
10
8
 
@@ -30,11 +28,6 @@ test('칸에 치는 키는 칸의 것 — 이름 칸의 Backspace 는 글자를
30
28
  assert.equal(keyActionOf({ key: 'Delete', target: { tagName: 'CANVAS' } }, 'arm'), 'remove')
31
29
  })
32
30
 
33
- test('편집기가 창의 키를 듣고, 떼어지면 놓는다', () => {
34
- const editor = readFileSync(fileURLToPath(new URL('../client/modeller/editor.ts', import.meta.url)), 'utf8')
35
- assert.ok(editor.includes("window.addEventListener('keydown', this.onKey, true)") && editor.includes("window.removeEventListener('keydown', this.onKey, true)"))
36
- })
37
-
38
31
  test('W · E · R 은 gizmo 의 모드 — 부품 목록에서 골라 포커스가 캔버스 밖에 있어도', () => {
39
32
  assert.equal(keyActionOf({ key: 'w' }, 'arm'), 'translate')
40
33
  assert.equal(keyActionOf({ key: 'E' }, 'arm'), 'rotate')
@@ -2,7 +2,7 @@
2
2
  * **모델러의 구성 — 세 칸과 오른쪽 탭** (사용자 2026-09-24).
3
3
  *
4
4
  * 부품 목록 · 캔버스 · 오른쪽 칸(아이콘 탭 여섯). 전에는 부품 속성이 캔버스 아래 좁은 칸에 끼어 있었다.
5
- * 이 시험은 소스에서 그 구성을 확인한다.
5
+ * 그 구성은 브라우저 검사(`test/browser/screen.check.mjs`)가 확인하고, 여기서는 화면의 말을 본다.
6
6
  */
7
7
  import { test } from 'node:test'
8
8
  import assert from 'node:assert/strict'
@@ -13,57 +13,8 @@ const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.m
13
13
  const editor = read('../client/modeller/editor.ts')
14
14
  const renderBody = editor.slice(editor.indexOf(' render() {'), editor.indexOf(' private renderField('))
15
15
 
16
- test('세 칸 — 부품 목록 · 캔버스 · 오른쪽 칸, 캔버스 아래에는 아무것도 없다', () => {
17
- /* The three panes are ox-three-pane's (ADR-0060 decision 8): parts in start, the canvas in the middle, the side in end. */
18
- const main = renderBody.slice(renderBody.indexOf('<ox-three-pane'), renderBody.indexOf('</ox-three-pane>'))
19
- const order = ['<figure-parts', '<figure-canvas', '<aside slot="end">'].map(tag => main.indexOf(tag))
20
- assert.match(main, /<figure-parts\s+slot="start"/, '부품 목록은 왼쪽 칸')
21
- assert.ok(order.every(i => i >= 0) && order[0]! < order[1]! && order[1]! < order[2]!, '부품 · 캔버스 · 오른쪽 칸 순서')
22
- assert.equal((main.match(/<aside/g) ?? []).length, 1, '오른쪽 칸은 하나 — 페이지가 따로 칸을 두지 않는다')
23
- assert.doesNotMatch(read('../client/pages/figure-modeller-page.ts'), /<aside/)
24
- })
25
-
26
- test('오른쪽 칸의 탭 여섯: 속성 · 능력 · 움직임 · 판정 · 환경 · 이력, 판정의 개수는 탭에 남는다', () => {
27
- const tabs = [...renderBody.matchAll(/this\.renderTab\('([a-z]+)'/g)].map(m => m[1])
28
- assert.deepEqual(tabs, ['properties', 'capability', 'motion', 'verdict', 'view', 'history'])
29
- for (const tab of tabs) assert.ok(renderBody.includes(`?off=\${tab !== '${tab}'}`), `${tab} 는 감추되 떼지 않는다`)
30
- })
31
-
32
- test('부품을 고르면 — 도형 전체의 탭(판정 · 이력)은 그 부품의 속성으로 넘어가고, 부품 탭은 새 부품을 보여 준다', () => {
33
- const choose = editor.slice(editor.indexOf(' private choose('), editor.indexOf(' private asset()'))
34
- assert.match(choose, /this\.selected = id/)
35
- assert.match(choose, /if \(id && \(this\.tab === 'verdict' \|\| this\.tab === 'view' \|\| this\.tab === 'history'\)\) this\.tab = 'properties'/)
36
- for (const from of ['@select-part=${(e: CustomEvent) => this.choose(e.detail.id)}', '@part-selected=${(e: CustomEvent<string>) => this.choose(e.detail)}'])
37
- assert.ok(renderBody.includes(from), from)
38
- })
39
-
40
- /* B.1 (designer 2026-09-27), amended by the user 2026-10-01: no path -- the list and the canvas show the choice; the list's root goes back. */
41
- test('패널 머리에는 경로가 없고 부품의 세 줄만 — 모델 설정은 목록 루트의 한 동작으로 연다', () => {
42
- assert.ok((renderBody.match(/\$\{this\.renderSubject\(\)\}/g) ?? []).length >= 3)
43
- const subject = editor.slice(editor.indexOf(' private renderSubject()'), editor.indexOf(' private showModelSettings()'))
44
- assert.ok(!subject.includes('<nav path'), 'no path')
45
- assert.ok(subject.includes('if (!summary) return nothing'), 'nothing chosen: no head')
46
- const show = editor.slice(editor.indexOf(' private showModelSettings()'), editor.indexOf(' render() {'))
47
- assert.ok(show.includes("this.selected = ''") && show.includes("this.tab = 'properties'"), 'the model settings, on the properties tab')
48
- assert.ok(!/session!?\.(command|apply|edit|undo)/.test(show), 'moving changes no data and no history')
49
- assert.ok(editor.includes('@show-model-settings=${() => this.showModelSettings()}'), 'the list root, the same move')
50
- })
51
-
52
- test('탭마다 제자리 — 속성엔 부품의 모양 · 재질, 자리와 능력엔 사다리와 자리, 움직임엔 움직임 · 조작값', () => {
53
- const panel = (tab: string) => {
54
- const start = renderBody.indexOf(`?off=\${tab !== '${tab}'}`)
55
- return renderBody.slice(start, renderBody.indexOf('<div panel', start + 1))
56
- }
57
- assert.ok(panel('properties').includes("this.renderLinks(unavailable, 'shape')") && panel('properties').includes('<figure-material-panel'))
58
- assert.ok(panel('capability').includes("this.renderLinks(unavailable, 'places')") && panel('capability').includes('<figure-capability-panel'))
59
- assert.ok(panel('motion').includes("this.renderLinks(unavailable, 'motion')") && panel('motion').includes('this.renderControls('))
60
- })
61
-
62
16
  test('띠 — 칸 접기 · 되돌리기 · 전체 보기 · 저장 상태 · 저장, 말은 번역에서', () => {
63
- /* The bar is ox-canvas-bar: folds at its edges (it folds the frame it is given), undo · redo · fit in the suite's places. */
64
- const bar = renderBody.slice(renderBody.indexOf('<ox-canvas-bar'), renderBody.indexOf('</ox-canvas-bar>'))
65
- for (const piece of ['.pane=${this.panes}', 'history', 'fit', 'undo-shortcut="Ctrl+Z"', 'redo-shortcut="Ctrl+Y"', 'session!.undo()', 'session!.redo()', '.fit()', 'role="status"', '@click=${this.save}'])
66
- assert.ok(bar.includes(piece), piece)
17
+ /* The bar itself -- its folds, undo · redo · fit, the status and save -- is the browser check's (test/browser/screen.check.mjs). */
67
18
  assert.doesNotMatch(renderBody, /[가-힣]/, '편집기의 화면 말에 한국어가 박혀 있지 않다')
68
19
  const keys = [...editor.matchAll(/'(figure\.[a-z.-]*[a-z])'/g)].map(m => m[1])
69
20
  for (const lang of ['ko', 'en', 'ja', 'zh', 'ms', 'es']) {
@@ -72,24 +23,9 @@ test('띠 — 칸 접기 · 되돌리기 · 전체 보기 · 저장 상태 ·
72
23
  }
73
24
  })
74
25
 
75
- test('탭 아이콘은 오른쪽 칸 맨 위 — 타입 칸은 속성 탭 안의 첫 칸', () => {
76
- const aside = renderBody.slice(renderBody.indexOf('<aside slot="end">'), renderBody.indexOf('</aside>'))
77
- assert.match(aside, /^<aside slot="end">\s*<div tabs>/, '탭이 첫째')
78
- const properties = aside.slice(aside.indexOf("?off=${tab !== 'properties'}"), aside.indexOf("?off=${tab !== 'capability'}"))
79
- assert.ok(properties.includes('<slot name="figure"></slot>'))
80
- const figure = read('../client/modeller/figure-properties.ts')
81
- assert.match(figure, /<h3>\$\{i18next\.t\('figure\.label\.group-figure'\)\}<\/h3>\s*<slot name="type"><\/slot>/, '타입은 「도형」 절의 첫 칸')
82
- const page = read('../client/pages/figure-modeller-page.ts')
83
- assert.ok(page.includes('<label slot="type" type-field>'))
84
- })
85
-
86
26
  /* User 2026-10-01: 「맞추기 아이콘 … 무엇을 하는 것인지 모르겠다」 -- words, and what moves the part apart from what makes one. */
87
27
  test('맞추기는 글자 단추로 — 옮기는 넷(가운데 X·Z, 면에 대기, 눈금)과 새 부품을 만드는 대칭 복사를 다른 줄에 둔다', () => {
88
- const tools = editor.slice(editor.indexOf(' private renderTools('), editor.indexOf(" /**\n * A part's name."))
89
- assert.ok(!tools.includes('<md-icon>'), 'no icon-only buttons')
90
- assert.ok(tools.includes("i18next.t('figure.label.tools')") && tools.includes("i18next.t('figure.label.mirror-copy')"), 'two rows, each named')
91
- const plane = editor.slice(editor.indexOf(' private renderPlaneAlign('), editor.indexOf(' private alignToPlane()'))
92
- assert.ok(!plane.includes('<md-icon>') && plane.includes('figure.button.align-to-${placement}'))
28
+ /* The two rows and their buttons in words are the browser check's (test/browser/screen.check.mjs); here, the words. */
93
29
  const ko = JSON.parse(readFileSync(fileURLToPath(new URL('../translations/ko.json', import.meta.url)), 'utf8')) as Record<string, string>
94
30
  assert.deepEqual(
95
31
  ['center-x', 'center-z', 'align-to-floor', 'snap', 'mirror-x'].map(k => ko[`figure.button.${k}`]),
@@ -99,12 +35,7 @@ test('맞추기는 글자 단추로 — 옮기는 넷(가운데 X·Z, 면에 대
99
35
  })
100
36
 
101
37
  test('불러오는 동안 빈 무대에 「도형을 추가해…」가 깜빡이지 않고, 좁은 화면 알림은 이 브라우저에서 한 번', () => {
102
- const canvas = read('../client/modeller/canvas.ts')
103
- /* A new figure's start card says how to begin; an empty stage otherwise is one loading, or with every part hidden. */
104
- assert.ok(canvas.includes('return html`<div stage></div>`'))
105
- assert.ok(!canvas.includes('add-a-shape-to-start') && !editor.includes('startShown'))
38
+ /* A new figure's start card says how to begin; an empty stage otherwise is one loading, or with every part hidden. The bare stage and the notice said once are the browser check's (test/browser/screen.check.mjs). */
106
39
  for (const lang of ['ko', 'en', 'ja', 'zh', 'ms', 'es'])
107
40
  assert.equal((JSON.parse(read(`../translations/${lang}.json`)) as Record<string, string>)['figure.text.add-a-shape-to-start'], undefined, lang)
108
- assert.ok(editor.includes("if (!foldSaid()) notify({ level: 'info', message: i18next.t('figure.text.parts-panel-folded-narrow') })"))
109
- assert.ok(/function foldSaid\(\): boolean \{\s*try \{\s*if \(localStorage\.getItem\('figure-fold-said'\)\) return true\s*localStorage\.setItem\('figure-fold-said', '1'\)\s*\} catch/.test(editor), 'storage refused: said, as before')
110
41
  })