@things-factory/figure-ui 10.1.169 → 10.1.171

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 (105) hide show
  1. package/client/modeller/affected-parts.ts +11 -0
  2. package/client/modeller/canvas.ts +15 -1
  3. package/client/modeller/editor-styles.ts +98 -6
  4. package/client/modeller/editor.ts +212 -53
  5. package/client/modeller/fasten-geometry.ts +19 -0
  6. package/client/modeller/figure-fasten-drawing.ts +4 -3
  7. package/client/modeller/figure-links.ts +80 -37
  8. package/client/modeller/figure-material-panel.ts +20 -1
  9. package/client/modeller/figure-preview.ts +121 -8
  10. package/client/modeller/figure-properties.ts +1 -2
  11. package/client/modeller/figure-section-panel.ts +5 -0
  12. package/client/modeller/figure-verdict.ts +9 -2
  13. package/client/modeller/gate-findings.ts +12 -4
  14. package/client/modeller/group-menu.ts +57 -0
  15. package/client/modeller/links.ts +22 -1
  16. package/client/modeller/modeller-keys.ts +6 -0
  17. package/client/modeller/preview-size.ts +32 -0
  18. package/client/modeller/stage-model.ts +11 -0
  19. package/client/pages/figure-modeller-page.ts +8 -2
  20. package/dist-client/modeller/affected-parts.d.ts +6 -0
  21. package/dist-client/modeller/affected-parts.js +12 -0
  22. package/dist-client/modeller/affected-parts.js.map +1 -0
  23. package/dist-client/modeller/canvas.d.ts +3 -0
  24. package/dist-client/modeller/canvas.js +12 -1
  25. package/dist-client/modeller/canvas.js.map +1 -1
  26. package/dist-client/modeller/clips.d.ts +1 -1
  27. package/dist-client/modeller/editor-styles.js +98 -6
  28. package/dist-client/modeller/editor-styles.js.map +1 -1
  29. package/dist-client/modeller/editor.d.ts +42 -1
  30. package/dist-client/modeller/editor.js +220 -51
  31. package/dist-client/modeller/editor.js.map +1 -1
  32. package/dist-client/modeller/fasten-geometry.d.ts +27 -0
  33. package/dist-client/modeller/fasten-geometry.js +14 -0
  34. package/dist-client/modeller/fasten-geometry.js.map +1 -1
  35. package/dist-client/modeller/figure-fasten-drawing.js +4 -3
  36. package/dist-client/modeller/figure-fasten-drawing.js.map +1 -1
  37. package/dist-client/modeller/figure-links.d.ts +2 -0
  38. package/dist-client/modeller/figure-links.js +84 -38
  39. package/dist-client/modeller/figure-links.js.map +1 -1
  40. package/dist-client/modeller/figure-material-panel.d.ts +7 -0
  41. package/dist-client/modeller/figure-material-panel.js +24 -1
  42. package/dist-client/modeller/figure-material-panel.js.map +1 -1
  43. package/dist-client/modeller/figure-preview.d.ts +12 -0
  44. package/dist-client/modeller/figure-preview.js +120 -5
  45. package/dist-client/modeller/figure-preview.js.map +1 -1
  46. package/dist-client/modeller/figure-properties.js +1 -2
  47. package/dist-client/modeller/figure-properties.js.map +1 -1
  48. package/dist-client/modeller/figure-section-panel.js +5 -0
  49. package/dist-client/modeller/figure-section-panel.js.map +1 -1
  50. package/dist-client/modeller/figure-verdict.d.ts +1 -0
  51. package/dist-client/modeller/figure-verdict.js +8 -2
  52. package/dist-client/modeller/figure-verdict.js.map +1 -1
  53. package/dist-client/modeller/gate-findings.d.ts +3 -1
  54. package/dist-client/modeller/gate-findings.js +8 -4
  55. package/dist-client/modeller/gate-findings.js.map +1 -1
  56. package/dist-client/modeller/group-menu.d.ts +18 -0
  57. package/dist-client/modeller/group-menu.js +52 -0
  58. package/dist-client/modeller/group-menu.js.map +1 -0
  59. package/dist-client/modeller/links.d.ts +5 -0
  60. package/dist-client/modeller/links.js +21 -1
  61. package/dist-client/modeller/links.js.map +1 -1
  62. package/dist-client/modeller/modeller-keys.js +7 -0
  63. package/dist-client/modeller/modeller-keys.js.map +1 -1
  64. package/dist-client/modeller/preview-size.d.ts +23 -0
  65. package/dist-client/modeller/preview-size.js +32 -0
  66. package/dist-client/modeller/preview-size.js.map +1 -0
  67. package/dist-client/modeller/stage-model.d.ts +11 -0
  68. package/dist-client/modeller/stage-model.js +11 -0
  69. package/dist-client/modeller/stage-model.js.map +1 -1
  70. package/dist-client/pages/figure-modeller-page.d.ts +3 -0
  71. package/dist-client/pages/figure-modeller-page.js +11 -2
  72. package/dist-client/pages/figure-modeller-page.js.map +1 -1
  73. package/dist-client/route.d.ts +1 -1
  74. package/dist-client/tsconfig.tsbuildinfo +1 -1
  75. package/package.json +8 -8
  76. package/test/browser/editing-page.js +3 -1
  77. package/test/browser/editing.check.mjs +2 -1
  78. package/test/browser/links-page.js +1 -0
  79. package/test/browser/links.check.mjs +20 -5
  80. package/test/browser/modeller.check.mjs +9 -1
  81. package/test/browser/more-open.js +11 -0
  82. package/test/browser/parts.check.mjs +11 -1
  83. package/test/browser/places-mesh.check.mjs +9 -3
  84. package/test/browser/product-page.check.mjs +9 -4
  85. package/test/browser/props.check.mjs +224 -0
  86. package/test/browser/screen-page.js +18 -0
  87. package/test/browser/screen.check.mjs +172 -29
  88. package/test/browser/section-page.js +1 -0
  89. package/test/browser/section.check.mjs +6 -0
  90. package/test/command-preview.test.ts +2 -2
  91. package/test/fasten-geometry.test.ts +15 -0
  92. package/test/fields.test.ts +4 -2
  93. package/test/first-minute.test.ts +15 -2
  94. package/test/layout.test.ts +1 -1
  95. package/test/preview-size.test.ts +33 -0
  96. package/test/refusals.test.ts +1 -1
  97. package/test/resize-with-placement.test.ts +1 -1
  98. package/test/stage-model.test.ts +11 -1
  99. package/test/verdict.test.ts +7 -0
  100. package/translations/en.json +37 -27
  101. package/translations/es.json +18 -8
  102. package/translations/ja.json +19 -9
  103. package/translations/ko.json +50 -40
  104. package/translations/ms.json +19 -9
  105. package/translations/zh.json +19 -9
@@ -82,6 +82,12 @@ const tabOn = () => read(() => screenCheck.editor().shadowRoot.querySelector('bu
82
82
  const pressBar = action => press(action, a => screenCheck.editor().shadowRoot.querySelector('ox-canvas-bar').shadowRoot.querySelector(`button[data-action=${a}]`), action)
83
83
  const pressTab = name => press(name, name => screenCheck.editor().shadowRoot.querySelector(`button[tab][aria-label="${name}"]`), name)
84
84
  const TAB = name => `figure.label.tab-short-${name}`
85
+ /** The view settings or the release history, opened from the bar over the work (they were tabs once); left open if open. */
86
+ async function openOverlay(which) {
87
+ if (await read(w => screenCheck.editor().shadowRoot.querySelector(`ox-canvas-bar button[bar-overlay=${w}]`)?.getAttribute('aria-pressed') === 'true', which)) return
88
+ await press(`the ${which} button`, w => screenCheck.editor().shadowRoot.querySelector(`ox-canvas-bar button[bar-overlay=${w}]`), which)
89
+ await until(`${which} open`, w => screenCheck.editor().shadowRoot.querySelector(`div[bar-overlay-panel=${w}]`)?.getBoundingClientRect().height > 0, which)
90
+ }
85
91
  /** A row of the parts list, by the part's name. */
86
92
  const pressRow = id => press(`the ${id} row`, id => screenCheck.deep('span[label]').find(s => s.getAttribute('title') === id), id)
87
93
 
@@ -185,13 +191,13 @@ describe('the frame: parts · canvas · one side of seven tabs, and the bar over
185
191
  assert.equal(await read(() => getComputedStyle(screenCheck.editor()).containerType), 'inline-size')
186
192
  })
187
193
 
188
- test('the side\'s seven tabs, in order, each saying its name under its icon -- the name it is read by', async () => {
194
+ test('the side\'s four tabs, in order, each saying its name under its icon -- the name it is read by', async () => {
189
195
  const tabs = await read(() =>
190
196
  [...screenCheck.editor().shadowRoot.querySelectorAll('aside button[tab]')].map(b => ({ label: b.getAttribute('aria-label'), shows: b.querySelector('span[tab-label]')?.textContent.trim() }))
191
197
  )
192
198
  assert.deepEqual(
193
199
  tabs.map(t => t.label),
194
- ['properties', 'capability', 'motion', 'data', 'verdict', 'view', 'history'].map(TAB)
200
+ ['properties', 'motion', 'data', 'verdict'].map(TAB)
195
201
  )
196
202
  for (const tab of tabs) assert.equal(tab.shows, tab.label, `${tab.label} shows its name`)
197
203
  assert.equal(await read(() => screenCheck.editor().shadowRoot.querySelector('aside').firstElementChild.matches('div[tabs]')), true, 'the tabs are the side\'s first row')
@@ -200,12 +206,12 @@ describe('the frame: parts · canvas · one side of seven tabs, and the bar over
200
206
  test('a tab shows its panel and hides the others without taking them down -- the verdict keeps what it holds', async () => {
201
207
  const panels = () =>
202
208
  read(() => [...screenCheck.editor().shadowRoot.querySelectorAll('aside > div[panel]')].map(p => ({ seen: p.getBoundingClientRect().height > 0, connected: p.isConnected, verdict: !!p.querySelector('slot[name=verdict]') })))
203
- const tabs = ['properties', 'capability', 'motion', 'data', 'verdict', 'view', 'history']
209
+ const tabs = ['properties', 'motion', 'data', 'verdict']
204
210
  for (const [at, tab] of tabs.entries()) {
205
211
  await pressTab(TAB(tab))
206
212
  await until(`${tab} on`, name => screenCheck.editor().shadowRoot.querySelector('button[tab][on]')?.getAttribute('aria-label') === name, TAB(tab))
207
213
  const now = await panels()
208
- assert.equal(now.length, 7, 'seven panels, all standing')
214
+ assert.equal(now.length, 4, 'four panels, all standing')
209
215
  assert.deepEqual(now.map(p => p.seen), tabs.map((_, i) => i === at), `${tab}: its panel seen, the rest hidden`)
210
216
  assert.ok(now.every(p => p.connected))
211
217
  assert.ok(now[tabs.indexOf('verdict')].verdict, 'the verdict panel holds the verdict slot whichever tab is on')
@@ -323,7 +329,7 @@ describe('the editor is drawn in the shell\'s tokens (2026-09-27)', () => {
323
329
  describe('a part chosen: each tab in its place, the figure-wide tabs give way, the list\'s root goes back', () => {
324
330
  before(() => mount('arm'))
325
331
 
326
- test('properties hold the part\'s shape and material, capability its places, motion its motion and controls -- each under the part\'s head', async () => {
332
+ test('properties hold the part\'s size, place, material and, folded, its places; motion its motion and controls -- each under the part\'s head', async () => {
327
333
  await pressRow('upper-arm')
328
334
  await until('upper-arm chosen', () => screenCheck.editor().selected === 'upper-arm')
329
335
  const holds = () =>
@@ -341,13 +347,18 @@ describe('a part chosen: each tab in its place, the figure-wide tabs give way, t
341
347
  await pressTab(TAB('properties'))
342
348
  let now = await holds()
343
349
  assert.ok(now.subject, 'the part\'s head')
344
- assert.ok(now.links.includes('figure.label.resize-intent') && now.links.includes('figure.label.placement'), `the shape's links: ${now.links}`)
350
+ assert.ok(now.links.includes('figure.label.group-size') && now.links.includes('figure.label.group-place'), `the shape's links: ${now.links}`)
345
351
  assert.ok(now.material, 'its material')
346
- await pressTab(TAB('capability'))
347
- now = await holds()
348
- assert.ok(now.subject, 'the part\'s head')
349
- assert.ok(now.links.length === 1 && now.links[0].startsWith('figure.label.places-on-part'), `the places on the part: ${now.links}`)
350
- assert.ok(now.capability, 'its capabilities')
352
+ /* What it does is a folded group of the properties now (chief architect 2026-10-16), its places inside. */
353
+ await press('the capability fold', () => screenCheck.shownPanel().querySelector('section[capability] > details > summary'))
354
+ const places = await read(() => {
355
+ const fold = screenCheck.shownPanel().querySelector('section[capability] > details')
356
+ const links = fold.querySelector('figure-links')
357
+ return { open: fold.open, heads: links ? [...links.shadowRoot.querySelectorAll('section > h3')].map(h => screenCheck.ownText(h)) : [], capability: !!fold.querySelector('figure-capability-panel') }
358
+ })
359
+ assert.ok(places.open, 'the fold opened')
360
+ assert.ok(places.heads.length === 1 && places.heads[0].startsWith('figure.label.places-on-part'), `the places on the part: ${places.heads}`)
361
+ assert.ok(places.capability, 'its capabilities')
351
362
  await pressTab(TAB('motion'))
352
363
  now = await holds()
353
364
  assert.ok(now.subject, 'the part\'s head')
@@ -355,8 +366,8 @@ describe('a part chosen: each tab in its place, the figure-wide tabs give way, t
355
366
  assert.ok(now.controls > 0, 'the controls that pose it')
356
367
  })
357
368
 
358
- test('choosing a part from the verdict, view or history tab opens its properties; from a part\'s own tab, that tab stays', async () => {
359
- for (const tab of ['verdict', 'view', 'history']) {
369
+ test('choosing a part from the verdict tab opens its properties; from a part\'s own tab, that tab stays', async () => {
370
+ for (const tab of ['verdict']) {
360
371
  await pressTab(TAB(tab))
361
372
  await pressRow((await selected()) === 'forearm' ? 'elbow' : 'forearm')
362
373
  await until(`from ${tab}, the properties`, () => screenCheck.editor().shadowRoot.querySelector('button[tab][on]')?.getAttribute('aria-label') === 'figure.label.tab-short-properties')
@@ -563,8 +574,8 @@ describe('the view tab: the workbench\'s settings, in the board\'s own names, ke
563
574
  assert.equal(now.pixelRatio, await read(() => Math.min(3, (window.devicePixelRatio || 1) * 2)))
564
575
  })
565
576
 
566
- test('the coordinate ground switched off in the tab: the board follows, the browser keeps it, the figure is untouched', async () => {
567
- await pressTab(TAB('view'))
577
+ test('the coordinate ground switched off in the view menu: the board follows, the browser keeps it, the figure is untouched', async () => {
578
+ await openOverlay('view')
568
579
  assert.ok((await read(() => screenCheck.deep('figure-settings')[0].shadowRoot.textContent)).includes('figure.text.view-settings-are-not-saved'), 'it says these are not saved')
569
580
  const before = { revision: await revision(), source: await source(), grid: (await board()).grid }
570
581
  assert.equal(before.grid, true)
@@ -640,7 +651,7 @@ describe('the preview: registered as a board registers it, posed and played, the
640
651
  const now = await clip()
641
652
  assert.equal(now.shown, now.value.toFixed(2))
642
653
  const given = await read(() => screenCheck.deep('figure-preview')[0].scene.root.components.map(c => c.state.clips?.cycle))
643
- assert.equal(given.length, 8, 'eight instances')
654
+ assert.equal(given.length, 1, 'the one figure the preview stands')
644
655
  assert.ok(given.every(v => v === now.value), `each given the speed shown: ${given}`)
645
656
  })
646
657
 
@@ -714,12 +725,65 @@ describe('the preview of a figure that does not declare every size', () => {
714
725
  await mount('tower')
715
726
  await press('the preview mode', () => screenCheck.deep('button[mode]').find(b => b.title === 'figure.text.mode-preview-explained'))
716
727
  await until('the legend', () => !!screenCheck.deep('figure-preview')[0]?.shadowRoot?.querySelector('div[legend]'))
728
+ /* The eight ways side by side are a choice in the legend; the one figure stands first. */
729
+ await press('the eight sizes', () => screenCheck.deep('figure-preview')[0].shadowRoot.querySelector('button[way=eight]'))
730
+ await until('the eight', () => screenCheck.deep('figure-preview')[0].scene?.root?.components?.length === 8)
717
731
  const clamped = await read(() => [...screenCheck.deep('figure-preview')[0].shadowRoot.querySelectorAll('div[legend] small[clamp]')].map(s => s.textContent.trim()))
718
732
  assert.ok(clamped.length > 0 && clamped.every(c => c.startsWith('figure.text.preview-clamped')), `said: ${clamped}`)
719
733
  await boardsUp()
720
734
  })
721
735
  })
722
736
 
737
+ describe('the camera follows a size typed far from the view (first 15 minutes, 2026-10-16)', () => {
738
+ test('a width typed ten times larger fits the view again; a little wider leaves the camera where it is', async () => {
739
+ await mount('box')
740
+ await pressRow('box')
741
+ await read(() => {
742
+ const canvas = screenCheck.deep('figure-canvas')[0]
743
+ const fit = canvas.fit.bind(canvas)
744
+ window.fits = 0
745
+ canvas.fit = () => (window.fits++, fit())
746
+ })
747
+ const typeWidth = async mm => {
748
+ const width = await element('the width field', () => screenCheck.deep('input').find(i => i.getAttribute('aria-label') === 'box.size.x'))
749
+ await width.click({ clickCount: 3 })
750
+ await page().keyboard.type(String(mm))
751
+ await page().keyboard.press('Enter')
752
+ await until(`the width is ${mm}`, mm => probe.value('box.size.x') === mm, mm)
753
+ /* The camera judges once the figure has stood still (REFIT_SETTLE_MS, 400 ms). */
754
+ await new Promise(done => setTimeout(done, 700))
755
+ }
756
+ await typeWidth(500)
757
+ assert.equal(await read(() => window.fits), 0, 'a little wider: the camera stays')
758
+ await typeWidth(4000)
759
+ await until('the view fitted to it', () => window.fits >= 1)
760
+ await boardsUp()
761
+ })
762
+ })
763
+
764
+ describe('the preview stands one figure the author stretches (first 15 minutes, 2026-10-16)', () => {
765
+ const preview = () => screenCheck.deep('figure-preview')[0]
766
+ test('a slider per axis the figure lets change; the figure grows in place and its size is said beside it', async () => {
767
+ await mount('stretch')
768
+ await press('the preview mode', () => screenCheck.deep('button[mode]').find(b => b.title === 'figure.text.mode-preview-explained'))
769
+ await until('one figure, its sizes', () => screenCheck.deep('figure-preview')[0]?.scene?.root?.components?.length === 1 && !!screenCheck.deep('figure-preview')[0].shadowRoot.querySelector('div[sizes] input'))
770
+ assert.equal(await read(() => screenCheck.deep('figure-preview')[0].shadowRoot.querySelectorAll('div[sizes] input').length), 3, 'each axis the figure declares a range for')
771
+ await read(() => {
772
+ const slider = screenCheck.deep('figure-preview')[0].shadowRoot.querySelector('div[sizes] input')
773
+ slider.value = '6000'
774
+ slider.dispatchEvent(new Event('input'))
775
+ slider.dispatchEvent(new Event('change'))
776
+ })
777
+ /* The box is the space the figure takes at 6 m: the frame and its 100 mm padding either side. */
778
+ await until('the figure 6 m wide, in its space', () => Math.round(screenCheck.deep('figure-preview')[0].scene.root.components[0].model.width) === 6200)
779
+ const said = await read(() => screenCheck.deep('figure-preview')[0].shadowRoot.querySelector('div[sizes] p[said]').textContent.trim())
780
+ assert.ok(said.startsWith('6,000 × '), said)
781
+ const box = await read(() => { const m = screenCheck.deep('figure-preview')[0].scene.root.components[0].model; return m.left + m.width / 2 })
782
+ assert.equal(Math.round(box), await read(() => screenCheck.deep('figure-preview')[0].board.width / 2), 'grown about the board\'s centre')
783
+ await boardsUp()
784
+ })
785
+ })
786
+
723
787
  describe('a part\'s size row: a rule\'s numbers only where a rule sets every side', () => {
724
788
  /* The size row of the part chosen, and how many of its cells a rule holds. */
725
789
  const sizeRow = () =>
@@ -809,11 +873,14 @@ describe('the fastening form says whether it changes the fastening the part has
809
873
  assert.equal(await revision(), before, 'nothing done until it is pressed')
810
874
  })
811
875
 
812
- test('a part already fastened: no 「새로 붙이기」, no 붙이기 -- another target is taken at once, in one step', async () => {
876
+ test('a part already fastened: said in one line with 「바꾸기」; opened, no 「새로 붙이기」, no 붙이기 -- another target is taken at once, in one step', async () => {
813
877
  await mount('arm')
814
878
  await pressRow('forearm')
815
879
  await until('forearm chosen', () => screenCheck.editor().selected === 'forearm')
816
880
  await until('the sentence', () => screenCheck.deep('p[fasten-sentence]').length === 1)
881
+ assert.equal(await read(() => screenCheck.deep('figure-fasten-drawing').length + screenCheck.deep('select').filter(s => s.closest('section[fastening]')).length), 0, 'one line: no drawing, no target to choose')
882
+ await press('change the fastening', () => screenCheck.deep('button[change-fastening]')[0])
883
+ await until('the drawing', () => screenCheck.deep('figure-fasten-drawing').length === 1)
817
884
  assert.equal(await read(() => screenCheck.deep('p[fasten-mode]').length), 0, 'the one it has is not said to be new')
818
885
  assert.equal(await fastenButton(), 0, 'no 붙이기')
819
886
  await until('the fastening seen before it is done: its face lit on the canvas', () => !!screenCheck.deep('figure-canvas')[0].highlight)
@@ -852,6 +919,71 @@ describe('the modeller page: the editor with the page\'s figure panel, verdict a
852
919
  assert.deepEqual(asides, [true], 'the editor\'s side, and no other')
853
920
  })
854
921
 
922
+ /*
923
+ * The side's buttons (user 2026-10-16: 「버튼 박스도 과하게 사용되어서」): a word button stands as tall as the fields beside
924
+ * it, whichever panel drew it, and taking something back -- detach, clear, back to fixed, remove -- draws no box.
925
+ */
926
+ test('the side panel\'s word buttons are one height with its fields, and taking back draws no box', async () => {
927
+ await open('fig-stack', 'stack')
928
+ await pressTab(TAB('properties'))
929
+ const buttons = () =>
930
+ read(() => {
931
+ const all = []
932
+ const walk = root => {
933
+ for (const e of root.querySelectorAll('*')) {
934
+ if (e.shadowRoot) walk(e.shadowRoot)
935
+ if (e.localName === 'slot') for (const a of e.assignedElements({ flatten: true })) all.push(a, ...a.querySelectorAll('*')), a.shadowRoot && walk(a.shadowRoot)
936
+ all.push(e)
937
+ }
938
+ }
939
+ walk(screenCheck.shownPanel())
940
+ const field = [...new Set(all)].find(e => e.localName === 'input' && e.type !== 'checkbox' && e.type !== 'range' && e.getBoundingClientRect().height > 0)
941
+ return {
942
+ field: Math.round(field.getBoundingClientRect().height),
943
+ words: [...new Set(all)]
944
+ .filter(e => e.localName === 'button' && (e.hasAttribute('act') || e.hasAttribute('quiet') || e.parentElement?.hasAttribute('tools')))
945
+ .filter(e => e.getBoundingClientRect().height > 0)
946
+ .map(e => {
947
+ const s = getComputedStyle(e)
948
+ return { kind: e.hasAttribute('quiet') ? 'quiet' : 'boxed', height: Math.round(e.getBoundingClientRect().height), border: s.borderTopStyle === 'none' ? 0 : parseFloat(s.borderTopWidth) }
949
+ })
950
+ }
951
+ })
952
+ const seen = []
953
+ for (const part of [null, 'base', 'top']) {
954
+ if (part) await pressRow(part), await until(`${part} chosen`, id => screenCheck.editor().selected === id, part)
955
+ const { field, words } = await buttons()
956
+ assert.ok(words.length > 0, `${part ?? 'the figure'}: buttons to measure`)
957
+ for (const one of words) assert.equal(one.height, field, `${part ?? 'the figure'}: a ${one.kind} button as tall as a field`)
958
+ for (const one of words.filter(w => w.kind === 'quiet')) assert.equal(one.border, 0, `${part ?? 'the figure'}: taking back draws no box`)
959
+ seen.push(...words.map(w => w.kind))
960
+ }
961
+ assert.ok(seen.includes('quiet'), 'a taking-back button was measured')
962
+ /* The tests after this one read the figure the first opened. */
963
+ await open('fig-a')
964
+ })
965
+
966
+ /* First 15 minutes (2026-10-16): release was refused for a volume never declared, said only at the foot of the verdict tab. */
967
+ test('what blocks release is said beside 「발행」, with the way out at hand there', async () => {
968
+ const answer = blocked => read(blocked => product.answer('inspectFigureDraft', () => ({ __typename: 'FigureInspection', blocked, findings: blocked ? [alone.finding('occupancy-undeclared')] : [] })), blocked)
969
+ const bar = () => read(() => [...screenCheck.editor().shadowRoot.querySelectorAll('ox-canvas-bar > button[slot=commit]')].map(b => (b.hasAttribute('bar-overlay') ? 'history' : b.hasAttribute('release-blocked') ? 'blocked' : b.hasAttribute('publish') ? 'publish' : 'save')))
970
+ await answer(true)
971
+ await open('fig-block', 'two', 'probe-block')
972
+ await until('said beside 발행', () => !!screenCheck.editor().shadowRoot.querySelector('ox-canvas-bar button[release-blocked]'))
973
+ assert.deepEqual(await bar(), ['history', 'blocked', 'publish', 'save'])
974
+ await press('what blocks release', () => screenCheck.editor().shadowRoot.querySelector('ox-canvas-bar button[release-blocked]'))
975
+ await until('the list under the bar', () => !!screenCheck.editor().shadowRoot.querySelector('div[release-blockers] li[finding]'))
976
+ assert.equal(await read(() => !!screenCheck.editor().session.revisionNow().asset.occupancy), false)
977
+ await answer(false)
978
+ await press('fit to the parts', () => screenCheck.editor().shadowRoot.querySelector('div[release-blockers] button[act]'))
979
+ await until('the space declared, in one step', () => !!screenCheck.editor().session.revisionNow().asset.occupancy)
980
+ await until('nothing blocks: the word is gone', () => !screenCheck.editor().shadowRoot.querySelector('ox-canvas-bar button[release-blocked]'))
981
+ assert.deepEqual(await bar(), ['history', 'publish', 'save'])
982
+ await read(() => product.answer('inspectFigureDraft'))
983
+ await saved()
984
+ await open('fig-a')
985
+ })
986
+
855
987
  test('the figure\'s type is the first field of the figure panel, under its heading', async () => {
856
988
  await pressTab(TAB('properties'))
857
989
  const first = await read(() => {
@@ -870,14 +1002,21 @@ describe('the modeller page: the editor with the page\'s figure panel, verdict a
870
1002
  assert.deepEqual(first, { heading: 'figure.label.group-figure', assigned: true, order: [true, true] })
871
1003
  })
872
1004
 
873
- test('the verdict and history tabs show the page\'s verdict and release history', async () => {
874
- for (const [tab, host] of [['verdict', 'figure-verdict'], ['history', 'figure-history-panel']]) {
875
- await pressTab(TAB(tab))
876
- const shown = await read(() => screenCheck.shownPanel().querySelector('slot')?.assignedElements().map(e => ({ tag: e.localName, height: Math.round(e.getBoundingClientRect().height) })))
877
- assert.equal(shown?.length, 1, tab)
878
- assert.equal(shown[0].tag, host)
879
- assert.ok(shown[0].height > 0, `${host} is shown`)
880
- }
1005
+ test('the verdict tab shows the page\'s verdict, and the bar\'s history button its release history', async () => {
1006
+ const assigned = within =>
1007
+ read(w => (w ? screenCheck.editor().shadowRoot.querySelector(w) : screenCheck.shownPanel()).querySelector('slot')?.assignedElements().map(e => ({ tag: e.localName, height: Math.round(e.getBoundingClientRect().height) })), within)
1008
+ await pressTab(TAB('verdict'))
1009
+ let shown = await assigned(null)
1010
+ assert.equal(shown?.length, 1, 'verdict')
1011
+ assert.equal(shown[0].tag, 'figure-verdict')
1012
+ assert.ok(shown[0].height > 0, 'figure-verdict is shown')
1013
+ await openOverlay('history')
1014
+ shown = await assigned('div[bar-overlay-panel=history]')
1015
+ assert.equal(shown?.length, 1, 'history')
1016
+ assert.equal(shown[0].tag, 'figure-history-panel')
1017
+ assert.ok(shown[0].height > 0, 'figure-history-panel is shown')
1018
+ /* Closed again: it lies over the side, and the checks after this press there. */
1019
+ await press('the history button', () => screenCheck.editor().shadowRoot.querySelector('ox-canvas-bar button[bar-overlay=history]'))
881
1020
  })
882
1021
 
883
1022
  test('the verdict tab\'s count is the report\'s, measured by the verdict', async () => {
@@ -946,7 +1085,7 @@ describe('the modeller page: the editor with the page\'s figure panel, verdict a
946
1085
  await read(finding => product.answer('inspectFigure', () => ({ __typename: 'FigureInspection', blocked: true, findings: [finding] })), await read(() => alone.finding('occupancy-undeclared')))
947
1086
  try {
948
1087
  await open('fig-empty', 'empty')
949
- await pressTab(TAB('history'))
1088
+ await openOverlay('history')
950
1089
  await until('offered', () => !!screenCheck.deep('figure-history-panel')[0]?.shadowRoot?.querySelector('button[fit-base]'))
951
1090
  await press('fit to the parts', () => screenCheck.deep('figure-history-panel')[0].shadowRoot.querySelector('button[fit-base]'))
952
1091
  await until('said', () => screenCheck.deep('p[fit-error]')[0]?.textContent.trim() === 'figure.text.fit-needs-parts')
@@ -956,8 +1095,8 @@ describe('the modeller page: the editor with the page\'s figure panel, verdict a
956
1095
  }
957
1096
  })
958
1097
 
959
- test('the view set in the tab holds for the next figure opened: its board is stood up with it', async () => {
960
- await pressTab(TAB('view'))
1098
+ test('the view set in the bar\'s menu holds for the next figure opened: its board is stood up with it', async () => {
1099
+ await openOverlay('view')
961
1100
  const colour = await element('the sky colour', () => screenCheck.setting('figure.label.sky-color').querySelector('input[type=color]'))
962
1101
  /* A colour picker is the browser's own window; its value is set as the picker sets it, and changed. */
963
1102
  await colour.evaluate(input => {
@@ -968,9 +1107,11 @@ describe('the modeller page: the editor with the page\'s figure panel, verdict a
968
1107
  try {
969
1108
  assert.equal(await read(() => screenCheck.deep('figure-canvas')[0].scene.root.state.skyColor), '#336699', 'the new board\'s sky')
970
1109
  } finally {
971
- await pressTab(TAB('view'))
1110
+ await openOverlay('view')
972
1111
  await press('reset the view', () => screenCheck.deep('figure-settings')[0].shadowRoot.querySelector('button[reset]'))
973
1112
  await until('the view as it was', () => screenCheck.deep('figure-canvas')[0].scene.root.state.skyColor === '#7c7c7c')
1113
+ /* Closed again: it lies over the side, and the next check presses there. */
1114
+ await press('the view button', () => screenCheck.editor().shadowRoot.querySelector('ox-canvas-bar button[bar-overlay=view]'))
974
1115
  }
975
1116
  })
976
1117
 
@@ -978,6 +1119,8 @@ describe('the modeller page: the editor with the page\'s figure panel, verdict a
978
1119
  await open('fig-p', 'arm-clip')
979
1120
  await pressRow('upper-arm')
980
1121
  await until('upper-arm chosen', () => screenCheck.editor().selected === 'upper-arm')
1122
+ /* The part's panel fits the side now; its places opened make it long enough to scroll. */
1123
+ await press('the capability fold', () => screenCheck.shownPanel().querySelector('section[capability] > details:not([open]) > summary'))
981
1124
  const at = await read(() => {
982
1125
  const r = screenCheck.shownPanel().getBoundingClientRect()
983
1126
  return { x: r.left + r.width / 2, y: r.top + r.height / 2 }
@@ -3,6 +3,7 @@
3
3
  * editor, the section panel, the inside's drawing, the fastening drawing and the face choice are tried on, and what the
4
4
  * check reads of them. Only reading here: every change the check makes goes through the mouse and the keyboard.
5
5
  */
6
+ import './more-open.js'
6
7
  import './modeller-page.js'
7
8
  import { html, render } from 'lit'
8
9
  import { setOpenPopupImplementation } from '@operato/popup'
@@ -524,10 +524,13 @@ describe('the inside\'s drawing in the 단면 group', () => {
524
524
  describe('the fastening drawing in the fastening form', () => {
525
525
  const labels = () => read(() => probe.fastenForm().map(s => s.label))
526
526
  const target = () => read(() => probe.fastenForm().find(s => s.label === 'figure.label.target-part'))
527
+ /* A fastening the part has is said in one line; 「바꾸기」 opens the drawing (chief architect 2026-10-16). */
528
+ const change = () => press('button[change-fastening]')
527
529
 
528
530
  test('the part\'s own face and the facing are not asked: the target, its face and how are drawn', async () => {
529
531
  await mount('arm')
530
532
  await pick('forearm')
533
+ await change()
531
534
  const asked = await labels()
532
535
  assert.ok(asked.includes('figure.label.target-part'))
533
536
  assert.ok(!asked.includes('figure.label.my-face') && !asked.includes('figure.label.facing'), `asked: ${asked}`)
@@ -570,6 +573,7 @@ describe('the fastening drawing in the fastening form', () => {
570
573
  test('fastened to the plane: the plane is named by the placement, chosen, and set on only', async () => {
571
574
  await mount('rollers')
572
575
  await pick('bed')
576
+ await change()
573
577
  const list = await target()
574
578
  assert.equal(list.value, '__plane')
575
579
  assert.equal(list.options.find(o => o.value === '__plane').text, 'figure.label.resize-place-floor')
@@ -578,8 +582,10 @@ describe('the fastening drawing in the fastening form', () => {
578
582
 
579
583
  test('a part that turns in place may have its centre on the face; a part that does not, not', async () => {
580
584
  await pick('roller')
585
+ await change()
581
586
  assert.deepEqual((await read(() => probe.fastenDrawing())).modes, ['meet', 'flush', 'centre'])
582
587
  await pick('mark')
588
+ await change()
583
589
  assert.deepEqual((await read(() => probe.fastenDrawing())).modes, ['meet', 'flush'])
584
590
  })
585
591
 
@@ -110,11 +110,11 @@ test('a row is seen before it is made, read as figure-model draws it -- and said
110
110
  assert.equal(rowBlockOf(a, 'roller'), undefined)
111
111
  const seen = previewFigureCommand(a, { kind: 'make-repeat', part: 'roller', axis: 'x', pitch: 200 } as never)
112
112
  const held: HeldPreview = { context: 'c', revision: 0, key: 'k', geometry: seen.geometry, moves: seen.moves, added: seen.added, row: figureRowOf(seen.asset, 'roller') }
113
- assert.deepEqual(previewWordsOf(held, 'roller', say), ['roller 10개 · X 방향 · pitch 200 mm (중심 간격)', '가운데 정렬 · 양 끝에 70 mm씩 남음'])
113
+ assert.deepEqual(previewWordsOf(held, 'roller', say), ['roller 10개 · X 방향 · 피치 200 mm', '가운데 정렬 · 양 끝에 70 mm씩 남음'])
114
114
  assert.equal(ghostPartsOf(held).length, 10, 'the copies it would draw, translucent')
115
115
  /* A set count is spread over the length: said as drawn, 970 apart, nothing left at the ends. */
116
116
  const three = figureRowOf(applyFigureAuthoring(a, { kind: 'make-repeat', part: 'roller', axis: 'x', pitch: 1, count: 3 }), 'roller')
117
- assert.deepEqual(rowWordsOf(three, say), ['roller 3개 · X 방향 · pitch 970 mm (중심 간격)', '가운데 정렬 · 양 끝에 0 mm씩 남음'])
117
+ assert.deepEqual(rowWordsOf(three, say), ['roller 3개 · X 방향 · 피치 970 mm', '가운데 정렬 · 양 끝에 0 mm씩 남음'])
118
118
  for (const lang of ['en', 'ja', 'ko', 'ms', 'zh', 'es']) {
119
119
  const t = JSON.parse(read(`../translations/${lang}.json`))
120
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'])
@@ -78,3 +78,18 @@ test('면은 서 있는 대로 보인다: 앞면은 X 가로·Y 위로, 옆면
78
78
  assert.deepEqual(screenOf(['y', 'z']), { h: 'z', v: 'y', sign: { x: 1, y: -1, z: 1 } })
79
79
  assert.deepEqual(screenOf(['x', 'z']), { h: 'x', v: 'z', sign: { x: 1, y: 1, z: 1 } })
80
80
  })
81
+
82
+ test('the drawing\'s two names do not lie over each other (user 2026-10-16: 「roller」 on its bar, the face\'s name cut by it)', async () => {
83
+ const { fastenLabelsOf } = await import('../client/modeller/fasten-geometry.ts')
84
+ const face = { x: 20, y: 60, w: 300, h: 90 }
85
+ /* A thin roller across the face: its name goes under it, the face's above the face. */
86
+ const thin = fastenLabelsOf(face, { x: 165, y: 63, w: 6, h: 84 }, 'roller', 180)
87
+ assert.deepEqual(thin.face, { x: 20, y: 55 })
88
+ assert.equal(thin.part.inside, false)
89
+ assert.ok(thin.part.y > 63 + 84 || thin.part.y < 63, 'outside the part')
90
+ /* A wide block: its name in it. */
91
+ const wide = fastenLabelsOf(face, { x: 120, y: 80, w: 100, h: 40 }, 'lid', 180)
92
+ assert.deepEqual(wide.part, { x: 170, y: 104, inside: true })
93
+ /* A face that fills the drawing: its name inside its top edge. */
94
+ assert.deepEqual(fastenLabelsOf({ x: 4, y: 4, w: 300, h: 170 }, { x: 100, y: 60, w: 100, h: 40 }, 'lid', 180).face, { x: 8, y: 18 })
95
+ })
@@ -70,8 +70,10 @@ test('pitch 는 한 곳에서 하나의 값이고, 늘 set-repeat 로 바꿔 최
70
70
  /* Offered once and set by the row in one step: the browser check's (test/browser/parts.check.mjs). */
71
71
  for (const lang of ['ko', 'en', 'ja', 'zh', 'ms', 'es']) {
72
72
  const table = JSON.parse(readFileSync(fileURLToPath(new URL(`../translations/${lang}.json`, import.meta.url)), 'utf8'))
73
- /* The term stays pitch; what it measures is said beside it (ADR-0097 §5.5: centre to centre). */
74
- assert.ok(table['figure.label.repeat-gap'].startsWith('pitch') && table['figure.label.repeat-gap'].length > 'pitch'.length, lang)
73
+ /* The term stays pitch -- in Korean written as the trade writes it, 피치 (user 2026-10-16: no English names on the
74
+ panel); what it measures is said beside it (ADR-0097 §5.5: centre to centre). */
75
+ const term = lang === 'ko' ? '피치' : 'pitch'
76
+ assert.ok(table['figure.label.repeat-gap'].startsWith(term) && table['figure.label.repeat-gap'].length > term.length, lang)
75
77
  assert.ok(['{length}', '{count}', '{longest}', '{limit}'].every(k => table['figure.text.repeat-what-pitch-gives']?.includes(k)), lang)
76
78
  }
77
79
  })
@@ -2,7 +2,7 @@ import { test } from 'node:test'
2
2
  import assert from 'node:assert/strict'
3
3
  import { readFileSync } from 'node:fs'
4
4
  import { addFigureBox, applyFigureAuthoring, applyFigureProposal, createFigureAsset, planFigureResize, proposeFigureOccupancy } from '@hatiolab/figure-model'
5
- import { partSummaryOf } from '../client/modeller/links.js'
5
+ import { followWordsOf, partSummaryOf } from '../client/modeller/links.js'
6
6
 
7
7
  /*
8
8
  * ADR-0097 B, the first minute (designer 2026-09-27, U04 · U05 · U09 · U11): a new figure asks what to start with; a
@@ -22,7 +22,10 @@ test('a selected part at the head: its kind, and what its size follows only when
22
22
  /* As usual -- a size of its own, loose: nothing but what it is. */
23
23
  assert.deepEqual(partSummaryOf(a, 'box', say), { kind: 'figure.label.primitive-cube', size: '' })
24
24
  const only = applyFigureProposal(a, planFigureResize(a, { part: 'box', axes: ['x'] }).steps)
25
- assert.equal(partSummaryOf(only, 'box', sayKo).size, '너비는 모델 크기를 따라 바뀝니다.')
25
+ assert.equal(partSummaryOf(only, 'box', sayKo).size, '너비는 도형 크기를 따라 바뀝니다.')
26
+ /* Every side following the figure's own: one sentence, not one per side (user 2026-10-16). */
27
+ const every = applyFigureProposal(a, planFigureResize(a, { part: 'box', axes: ['x', 'y', 'z'] }).steps)
28
+ assert.equal(partSummaryOf(every, 'box', sayKo).size, '너비 · 높이 · 깊이는 도형 크기를 따라 바뀝니다.')
26
29
  /* Another part's measure, said by name -- not 「규칙을 따름」. */
27
30
  let two = addFigureBox(a, { id: 'bed', size: { x: 800, y: 50, z: 400 }, position: { x: 0, y: 25, z: 0 } })
28
31
  two = applyFigureAuthoring(two, { kind: 'link-dimension', part: 'box', axis: 'y', source: { of: 'part', part: 'bed', axis: 'x' } } as never)
@@ -55,3 +58,13 @@ test('the fastening form says whether it edits the current relation or fastens a
55
58
  assert.equal(ko['figure.text.fasten-new'], '새로 붙이기')
56
59
  assert.equal(ko['figure.button.fasten-update'], undefined)
57
60
  })
61
+
62
+ test('a measure that follows something says what, where the row once said 「다른 규칙에 연결됨」 (user 2026-10-16)', () => {
63
+ const ko = JSON.parse(read('../translations/ko.json')) as Record<string, string>
64
+ const sayKo = (key: string, params: Record<string, string> = {}) => Object.entries(params).reduce((t, [k, v]) => t.split(`{${k}}`).join(v), ko[key] ?? key)
65
+ assert.equal(followWordsOf({ of: 'part', part: 'bed', axis: 'x', times: 1 } as never, sayKo), 'bed의 X')
66
+ assert.equal(followWordsOf({ of: 'instance', axis: 'y', times: 0.5 } as never, sayKo), '도형 Y 크기에 비례')
67
+ assert.equal(followWordsOf({ of: 'instance', axis: 'z', times: 1 } as never, sayKo), '도형 Z 크기와 같게')
68
+ assert.ok(!Object.values(ko).some(v => /\((floor|ceiling|center|wall|inverted|space)\)/.test(v)), 'no placement keeps its English name in brackets')
69
+ assert.ok(!/AGV_LIFTER|대문자/.test(ko['figure.text.type-name-will-not-release']!), 'the identifier rule is the format\'s: lower case and hyphens')
70
+ })
@@ -32,7 +32,7 @@ test('맞추기는 글자 단추로 — 옮기는 넷(가운데 X·Z, 면에 대
32
32
  const ko = wordsOf('ko') as Record<string, string>
33
33
  assert.deepEqual(
34
34
  ['center-x', 'center-z', 'align-to-floor', 'snap', 'mirror-x'].map(k => ko[`figure.button.${k}`]),
35
- ['X 가운데로', 'Z 가운데로', '바닥에 내려놓기', '눈금에 맞추기', 'X 반대편에']
35
+ ['X 가운데로', 'Z 가운데로', '바닥에 내려놓기', '눈금에 정렬', 'X 반대편에']
36
36
  )
37
37
  assert.ok(!ko['figure.text.center-on-x']!.includes('기준 상자'), 'says what the centre is')
38
38
  })
@@ -0,0 +1,33 @@
1
+ /*
2
+ * The preview's one figure, stretched (first 15 minutes, 2026-10-16): the author drags the width and reads what the
3
+ * figure is there -- its size and how many rollers its row holds -- beside the figure that grew.
4
+ */
5
+ import { test } from 'node:test'
6
+ import assert from 'node:assert/strict'
7
+ import { readFileSync } from 'node:fs'
8
+ import { addFigureBox, addFigurePrimitive, applyFigureAuthoring, createFigureAsset, type FigureAsset } from '@hatiolab/figure-model'
9
+
10
+ import { previewSizeWordsOf, rowCountsAt } from '../client/modeller/preview-size.ts'
11
+
12
+ /** A 2 m frame that follows the figure's width, with a roller every 200 mm along its top. */
13
+ function conveyor(): FigureAsset {
14
+ let asset = addFigureBox(createFigureAsset('Conveyor'), { id: 'frame', size: { x: 2000, y: 100, z: 600 }, position: { x: 0, y: 50, z: 0 } })
15
+ asset = addFigurePrimitive(asset, { id: 'roller', kind: 'cylinder', dimensions: { radius: 30, length: 560 }, position: { x: 0, y: 0, z: 0 }, rotation: { x: 90, y: 0, z: 0 } })
16
+ asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'roller', face: 'bottom', to: { part: 'frame', face: 'top' } })
17
+ asset = applyFigureAuthoring(asset, { kind: 'follow-size', part: 'frame', axes: ['x'] })
18
+ return applyFigureAuthoring(asset, { kind: 'make-repeat', part: 'roller', axis: 'x', pitch: 200 })
19
+ }
20
+
21
+ test('a row\'s count at each size tried is figure-model\'s reading of what it draws there', () => {
22
+ const asset = conveyor()
23
+ const at = (x: number) => rowCountsAt(asset, { x, y: 160, z: 600 })
24
+ assert.deepEqual(at(2000), [{ part: 'roller', count: 10 }])
25
+ assert.deepEqual(at(6000), [{ part: 'roller', count: 30 }], 'three times as long, three times as many')
26
+ assert.deepEqual(rowCountsAt(addFigureBox(createFigureAsset('Plain'), { id: 'box', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 } }), { x: 100, y: 100, z: 100 }), [], 'no row, nothing counted')
27
+ })
28
+
29
+ test('the line beside the figure: its size in millimetres, then each row\'s count', () => {
30
+ const ko = JSON.parse(readFileSync(new URL('../translations/ko.json', import.meta.url), 'utf8')) as Record<string, string>
31
+ const say = (key: string, params: Record<string, string> = {}) => Object.entries(params).reduce((t, [k, v]) => t.split(`{${k}}`).join(v), ko[key] ?? key)
32
+ assert.equal(previewSizeWordsOf(conveyor(), { x: 6000, y: 160, z: 600 }, say), '6,000 × 160 × 600 mm · roller 30개')
33
+ })
@@ -149,7 +149,7 @@ test('배치를 바꿀 수 없는 까닭 — 평면에 붙은 부품과 새 평
149
149
  })
150
150
  const refused = refusalOf(() => act(a, { kind: 'declare-occupancy', proposal: fm.proposeFigureOccupancy(a), placement: 'wall' }))
151
151
  const said = placementRefusalOf(refused, say)!
152
- assert.ok(said.includes('post (bottom)') && said.includes('벽 기반') && said.includes('back'), said)
152
+ assert.ok(said.includes('post (bottom)') && said.includes('벽') && said.includes('back'), said)
153
153
  assert.equal(editRefusalOf(refused, say), said, '편집 거절의 말도 같다')
154
154
  assert.equal(placementRefusalOf(refusalOf(() => act(box(), declaring({ min: 0, max: 4000 }))), say), null)
155
155
  /* The placement list sends its command through the page: said there in words, and the list back on the placement it has (product-page.check.mjs). */
@@ -94,7 +94,7 @@ test('fresh, width only, and all set back to fixed are three different answers',
94
94
  assert.deepEqual(resizeAxesOf(following, 'box'), { axes: ['y'], basis: 'chosen' })
95
95
  assert.deepEqual(resizeAxesOf(following, 'box', ['x']), { axes: ['x', 'y'], basis: 'chosen' })
96
96
  const ko = JSON.parse(readFileSync(new URL('../translations/ko.json', import.meta.url), 'utf-8'))
97
- assert.equal(ko['figure.button.follow-size'], '모델 크기에 맞추기')
97
+ assert.equal(ko['figure.button.follow-size'], '도형 크기에 맞추기')
98
98
  })
99
99
 
100
100
  test('a part off the floor is not moved by choosing its size response', () => {
@@ -9,7 +9,7 @@ import assert from 'node:assert/strict'
9
9
  import { addFigureBox, compileFigureAsset, createFigureAsset } from '@hatiolab/figure-model'
10
10
 
11
11
  import { drawableFigureParts } from '../client/modeller/renderer.ts'
12
- import { boardFor } from '../client/modeller/stage-model.ts'
12
+ import { boardFor, needsRefit } from '../client/modeller/stage-model.ts'
13
13
 
14
14
  const partsOf = (height: number) =>
15
15
  drawableFigureParts(
@@ -23,3 +23,13 @@ test('the board keeps the height it had: a part that shrinks back does not stand
23
23
  assert.equal(after.depth, tall.depth)
24
24
  assert.ok(boardFor(partsOf(3000), tall).depth > tall.depth, 'it still grows')
25
25
  })
26
+
27
+ test('a size typed far from the view brings the camera to it; a nudge does not (first 15 minutes, 2026-10-16)', () => {
28
+ const parts = (x: number, y: number, z: number) =>
29
+ drawableFigureParts(compileFigureAsset(addFigureBox(createFigureAsset('B'), { id: 'box', size: { x, y, z }, position: { x: 0, y: y / 2, z: 0 } })).evaluate().geometry)
30
+ const fitted = boardFor(parts(100, 100, 100))
31
+ assert.equal(needsRefit(fitted, boardFor(parts(3000, 800, 600))), true, 'a 100 mm box typed to 3 m')
32
+ assert.equal(needsRefit(fitted, boardFor(parts(120, 100, 100))), false, 'a little wider stays in view')
33
+ assert.equal(needsRefit(boardFor(parts(3000, 800, 600)), boardFor(parts(100, 100, 100))), true, 'shrunk far: fitted again')
34
+ assert.equal(needsRefit(undefined, boardFor(parts(3000, 800, 600))), false, 'never fitted: the first standing fits it')
35
+ })
@@ -92,3 +92,10 @@ test('감춘 부품도 「부품에 맞추기」가 잴 수 있는 부품이다'
92
92
  assert.equal(verdictMeasureOf(asset).bodies, 2, '그려지는 하나와 감춘 하나')
93
93
  assert.equal(verdictMeasureOf(createFigureAsset('Empty')).bodies, 0, '부품이 없으면 잴 것이 없다')
94
94
  })
95
+
96
+ test('a finding\'s affected parts are the figure\'s part names, not the format\'s words (user 2026-10-16: 「occupancy · roller-row-1-roller」)', async () => {
97
+ const { affectedPartsOf } = await import('../client/modeller/affected-parts.ts')
98
+ assert.deepEqual(affectedPartsOf('occupancy · roller-row-1-roller', ['box', 'roller-row-1-roller']), ['roller-row-1-roller'])
99
+ assert.deepEqual(affectedPartsOf('occupancy', ['box']), [], 'nothing of the figure: no line of affected parts')
100
+ assert.deepEqual(affectedPartsOf('box · lid', ['box', 'lid']), ['box', 'lid'])
101
+ })