@things-factory/figure-ui 10.1.168 → 10.1.170
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.
- package/client/figure-assistant.ts +10 -1
- package/client/modeller/affected-parts.ts +11 -0
- package/client/modeller/ai-proposal.ts +40 -9
- package/client/modeller/canvas.ts +15 -1
- package/client/modeller/editor-styles.ts +98 -6
- package/client/modeller/editor.ts +212 -53
- package/client/modeller/fasten-geometry.ts +19 -0
- package/client/modeller/figure-fasten-drawing.ts +4 -3
- package/client/modeller/figure-links.ts +80 -37
- package/client/modeller/figure-material-panel.ts +20 -1
- package/client/modeller/figure-preview.ts +117 -8
- package/client/modeller/figure-properties.ts +1 -2
- package/client/modeller/figure-section-panel.ts +5 -0
- package/client/modeller/figure-verdict.ts +9 -2
- package/client/modeller/gate-findings.ts +12 -4
- package/client/modeller/group-menu.ts +57 -0
- package/client/modeller/links.ts +22 -1
- package/client/modeller/modeller-keys.ts +6 -0
- package/client/modeller/preview-size.ts +32 -0
- package/client/modeller/product-session.ts +23 -9
- package/client/modeller/refusals.ts +14 -0
- package/client/modeller/stage-model.ts +11 -0
- package/client/pages/figure-modeller-page.ts +15 -6
- package/dist-client/figure-assistant.d.ts +6 -0
- package/dist-client/figure-assistant.js +9 -1
- package/dist-client/figure-assistant.js.map +1 -1
- package/dist-client/modeller/affected-parts.d.ts +6 -0
- package/dist-client/modeller/affected-parts.js +12 -0
- package/dist-client/modeller/affected-parts.js.map +1 -0
- package/dist-client/modeller/ai-proposal.d.ts +15 -5
- package/dist-client/modeller/ai-proposal.js +39 -10
- package/dist-client/modeller/ai-proposal.js.map +1 -1
- package/dist-client/modeller/canvas.d.ts +3 -0
- package/dist-client/modeller/canvas.js +12 -1
- package/dist-client/modeller/canvas.js.map +1 -1
- package/dist-client/modeller/editor-styles.js +98 -6
- package/dist-client/modeller/editor-styles.js.map +1 -1
- package/dist-client/modeller/editor.d.ts +42 -1
- package/dist-client/modeller/editor.js +220 -51
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/fasten-geometry.d.ts +27 -0
- package/dist-client/modeller/fasten-geometry.js +14 -0
- package/dist-client/modeller/fasten-geometry.js.map +1 -1
- package/dist-client/modeller/figure-fasten-drawing.js +4 -3
- package/dist-client/modeller/figure-fasten-drawing.js.map +1 -1
- package/dist-client/modeller/figure-links.d.ts +3 -1
- package/dist-client/modeller/figure-links.js +84 -38
- package/dist-client/modeller/figure-links.js.map +1 -1
- package/dist-client/modeller/figure-material-panel.d.ts +8 -1
- package/dist-client/modeller/figure-material-panel.js +24 -1
- package/dist-client/modeller/figure-material-panel.js.map +1 -1
- package/dist-client/modeller/figure-preview.d.ts +12 -0
- package/dist-client/modeller/figure-preview.js +116 -5
- package/dist-client/modeller/figure-preview.js.map +1 -1
- package/dist-client/modeller/figure-properties.js +1 -2
- package/dist-client/modeller/figure-properties.js.map +1 -1
- package/dist-client/modeller/figure-section-panel.d.ts +1 -1
- package/dist-client/modeller/figure-section-panel.js +5 -0
- package/dist-client/modeller/figure-section-panel.js.map +1 -1
- package/dist-client/modeller/figure-verdict.d.ts +1 -0
- package/dist-client/modeller/figure-verdict.js +8 -2
- package/dist-client/modeller/figure-verdict.js.map +1 -1
- package/dist-client/modeller/gate-findings.d.ts +3 -1
- package/dist-client/modeller/gate-findings.js +8 -4
- package/dist-client/modeller/gate-findings.js.map +1 -1
- package/dist-client/modeller/group-menu.d.ts +18 -0
- package/dist-client/modeller/group-menu.js +52 -0
- package/dist-client/modeller/group-menu.js.map +1 -0
- package/dist-client/modeller/links.d.ts +5 -0
- package/dist-client/modeller/links.js +21 -1
- package/dist-client/modeller/links.js.map +1 -1
- package/dist-client/modeller/modeller-keys.js +7 -0
- package/dist-client/modeller/modeller-keys.js.map +1 -1
- package/dist-client/modeller/preview-size.d.ts +23 -0
- package/dist-client/modeller/preview-size.js +32 -0
- package/dist-client/modeller/preview-size.js.map +1 -0
- package/dist-client/modeller/product-session.d.ts +0 -1
- package/dist-client/modeller/product-session.js +22 -9
- package/dist-client/modeller/product-session.js.map +1 -1
- package/dist-client/modeller/refusals.d.ts +1 -5
- package/dist-client/modeller/refusals.js +14 -1
- package/dist-client/modeller/refusals.js.map +1 -1
- package/dist-client/modeller/stage-model.d.ts +11 -0
- package/dist-client/modeller/stage-model.js +11 -0
- package/dist-client/modeller/stage-model.js.map +1 -1
- package/dist-client/pages/figure-modeller-page.d.ts +3 -0
- package/dist-client/pages/figure-modeller-page.js +18 -5
- package/dist-client/pages/figure-modeller-page.js.map +1 -1
- package/dist-client/route.d.ts +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +8 -8
- package/test/ai-proposal.test.ts +43 -4
- package/test/browser/editing-page.js +3 -1
- package/test/browser/editing.check.mjs +2 -1
- package/test/browser/links-page.js +1 -0
- package/test/browser/links.check.mjs +20 -5
- package/test/browser/modeller.check.mjs +6 -0
- package/test/browser/more-open.js +11 -0
- package/test/browser/parts.check.mjs +11 -1
- package/test/browser/places-mesh.check.mjs +9 -3
- package/test/browser/product-page.check.mjs +9 -4
- package/test/browser/props.check.mjs +224 -0
- package/test/browser/screen-page.js +17 -0
- package/test/browser/screen.check.mjs +171 -29
- package/test/browser/section-page.js +1 -0
- package/test/browser/section.check.mjs +6 -0
- package/test/command-preview.test.ts +2 -2
- package/test/fasten-geometry.test.ts +15 -0
- package/test/fields.test.ts +4 -2
- package/test/figure-assistant-surface.test.ts +38 -0
- package/test/first-minute.test.ts +15 -2
- package/test/layout.test.ts +1 -1
- package/test/preview-size.test.ts +33 -0
- package/test/refusals.test.ts +1 -1
- package/test/resize-with-placement.test.ts +1 -1
- package/test/stage-model.test.ts +11 -1
- package/test/verdict.test.ts +7 -0
- package/translations/en.json +43 -27
- package/translations/es.json +24 -8
- package/translations/ja.json +25 -9
- package/translations/ko.json +56 -40
- package/translations/ms.json +25 -9
- package/translations/zh.json +25 -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
|
|
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', '
|
|
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', '
|
|
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,
|
|
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
|
|
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.
|
|
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
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
|
359
|
-
for (const tab of ['verdict'
|
|
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
|
|
567
|
-
await
|
|
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,
|
|
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,64 @@ 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
|
+
await until('the figure 6 m wide', () => Math.round(screenCheck.deep('figure-preview')[0].scene.root.components[0].model.width) === 6000)
|
|
778
|
+
const said = await read(() => screenCheck.deep('figure-preview')[0].shadowRoot.querySelector('div[sizes] p[said]').textContent.trim())
|
|
779
|
+
assert.ok(said.startsWith('6,000 × '), said)
|
|
780
|
+
const box = await read(() => { const m = screenCheck.deep('figure-preview')[0].scene.root.components[0].model; return m.left + m.width / 2 })
|
|
781
|
+
assert.equal(Math.round(box), await read(() => screenCheck.deep('figure-preview')[0].board.width / 2), 'grown about the board\'s centre')
|
|
782
|
+
await boardsUp()
|
|
783
|
+
})
|
|
784
|
+
})
|
|
785
|
+
|
|
723
786
|
describe('a part\'s size row: a rule\'s numbers only where a rule sets every side', () => {
|
|
724
787
|
/* The size row of the part chosen, and how many of its cells a rule holds. */
|
|
725
788
|
const sizeRow = () =>
|
|
@@ -809,11 +872,14 @@ describe('the fastening form says whether it changes the fastening the part has
|
|
|
809
872
|
assert.equal(await revision(), before, 'nothing done until it is pressed')
|
|
810
873
|
})
|
|
811
874
|
|
|
812
|
-
test('a part already fastened: no 「새로 붙이기」, no 붙이기 -- another target is taken at once, in one step', async () => {
|
|
875
|
+
test('a part already fastened: said in one line with 「바꾸기」; opened, no 「새로 붙이기」, no 붙이기 -- another target is taken at once, in one step', async () => {
|
|
813
876
|
await mount('arm')
|
|
814
877
|
await pressRow('forearm')
|
|
815
878
|
await until('forearm chosen', () => screenCheck.editor().selected === 'forearm')
|
|
816
879
|
await until('the sentence', () => screenCheck.deep('p[fasten-sentence]').length === 1)
|
|
880
|
+
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')
|
|
881
|
+
await press('change the fastening', () => screenCheck.deep('button[change-fastening]')[0])
|
|
882
|
+
await until('the drawing', () => screenCheck.deep('figure-fasten-drawing').length === 1)
|
|
817
883
|
assert.equal(await read(() => screenCheck.deep('p[fasten-mode]').length), 0, 'the one it has is not said to be new')
|
|
818
884
|
assert.equal(await fastenButton(), 0, 'no 붙이기')
|
|
819
885
|
await until('the fastening seen before it is done: its face lit on the canvas', () => !!screenCheck.deep('figure-canvas')[0].highlight)
|
|
@@ -852,6 +918,71 @@ describe('the modeller page: the editor with the page\'s figure panel, verdict a
|
|
|
852
918
|
assert.deepEqual(asides, [true], 'the editor\'s side, and no other')
|
|
853
919
|
})
|
|
854
920
|
|
|
921
|
+
/*
|
|
922
|
+
* The side's buttons (user 2026-10-16: 「버튼 박스도 과하게 사용되어서」): a word button stands as tall as the fields beside
|
|
923
|
+
* it, whichever panel drew it, and taking something back -- detach, clear, back to fixed, remove -- draws no box.
|
|
924
|
+
*/
|
|
925
|
+
test('the side panel\'s word buttons are one height with its fields, and taking back draws no box', async () => {
|
|
926
|
+
await open('fig-stack', 'stack')
|
|
927
|
+
await pressTab(TAB('properties'))
|
|
928
|
+
const buttons = () =>
|
|
929
|
+
read(() => {
|
|
930
|
+
const all = []
|
|
931
|
+
const walk = root => {
|
|
932
|
+
for (const e of root.querySelectorAll('*')) {
|
|
933
|
+
if (e.shadowRoot) walk(e.shadowRoot)
|
|
934
|
+
if (e.localName === 'slot') for (const a of e.assignedElements({ flatten: true })) all.push(a, ...a.querySelectorAll('*')), a.shadowRoot && walk(a.shadowRoot)
|
|
935
|
+
all.push(e)
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
walk(screenCheck.shownPanel())
|
|
939
|
+
const field = [...new Set(all)].find(e => e.localName === 'input' && e.type !== 'checkbox' && e.type !== 'range' && e.getBoundingClientRect().height > 0)
|
|
940
|
+
return {
|
|
941
|
+
field: Math.round(field.getBoundingClientRect().height),
|
|
942
|
+
words: [...new Set(all)]
|
|
943
|
+
.filter(e => e.localName === 'button' && (e.hasAttribute('act') || e.hasAttribute('quiet') || e.parentElement?.hasAttribute('tools')))
|
|
944
|
+
.filter(e => e.getBoundingClientRect().height > 0)
|
|
945
|
+
.map(e => {
|
|
946
|
+
const s = getComputedStyle(e)
|
|
947
|
+
return { kind: e.hasAttribute('quiet') ? 'quiet' : 'boxed', height: Math.round(e.getBoundingClientRect().height), border: s.borderTopStyle === 'none' ? 0 : parseFloat(s.borderTopWidth) }
|
|
948
|
+
})
|
|
949
|
+
}
|
|
950
|
+
})
|
|
951
|
+
const seen = []
|
|
952
|
+
for (const part of [null, 'base', 'top']) {
|
|
953
|
+
if (part) await pressRow(part), await until(`${part} chosen`, id => screenCheck.editor().selected === id, part)
|
|
954
|
+
const { field, words } = await buttons()
|
|
955
|
+
assert.ok(words.length > 0, `${part ?? 'the figure'}: buttons to measure`)
|
|
956
|
+
for (const one of words) assert.equal(one.height, field, `${part ?? 'the figure'}: a ${one.kind} button as tall as a field`)
|
|
957
|
+
for (const one of words.filter(w => w.kind === 'quiet')) assert.equal(one.border, 0, `${part ?? 'the figure'}: taking back draws no box`)
|
|
958
|
+
seen.push(...words.map(w => w.kind))
|
|
959
|
+
}
|
|
960
|
+
assert.ok(seen.includes('quiet'), 'a taking-back button was measured')
|
|
961
|
+
/* The tests after this one read the figure the first opened. */
|
|
962
|
+
await open('fig-a')
|
|
963
|
+
})
|
|
964
|
+
|
|
965
|
+
/* First 15 minutes (2026-10-16): release was refused for a volume never declared, said only at the foot of the verdict tab. */
|
|
966
|
+
test('what blocks release is said beside 「발행」, with the way out at hand there', async () => {
|
|
967
|
+
const answer = blocked => read(blocked => product.answer('inspectFigureDraft', () => ({ __typename: 'FigureInspection', blocked, findings: blocked ? [alone.finding('occupancy-undeclared')] : [] })), blocked)
|
|
968
|
+
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')))
|
|
969
|
+
await answer(true)
|
|
970
|
+
await open('fig-block', 'two', 'probe-block')
|
|
971
|
+
await until('said beside 발행', () => !!screenCheck.editor().shadowRoot.querySelector('ox-canvas-bar button[release-blocked]'))
|
|
972
|
+
assert.deepEqual(await bar(), ['history', 'blocked', 'publish', 'save'])
|
|
973
|
+
await press('what blocks release', () => screenCheck.editor().shadowRoot.querySelector('ox-canvas-bar button[release-blocked]'))
|
|
974
|
+
await until('the list under the bar', () => !!screenCheck.editor().shadowRoot.querySelector('div[release-blockers] li[finding]'))
|
|
975
|
+
assert.equal(await read(() => !!screenCheck.editor().session.revisionNow().asset.occupancy), false)
|
|
976
|
+
await answer(false)
|
|
977
|
+
await press('fit to the parts', () => screenCheck.editor().shadowRoot.querySelector('div[release-blockers] button[act]'))
|
|
978
|
+
await until('the space declared, in one step', () => !!screenCheck.editor().session.revisionNow().asset.occupancy)
|
|
979
|
+
await until('nothing blocks: the word is gone', () => !screenCheck.editor().shadowRoot.querySelector('ox-canvas-bar button[release-blocked]'))
|
|
980
|
+
assert.deepEqual(await bar(), ['history', 'publish', 'save'])
|
|
981
|
+
await read(() => product.answer('inspectFigureDraft'))
|
|
982
|
+
await saved()
|
|
983
|
+
await open('fig-a')
|
|
984
|
+
})
|
|
985
|
+
|
|
855
986
|
test('the figure\'s type is the first field of the figure panel, under its heading', async () => {
|
|
856
987
|
await pressTab(TAB('properties'))
|
|
857
988
|
const first = await read(() => {
|
|
@@ -870,14 +1001,21 @@ describe('the modeller page: the editor with the page\'s figure panel, verdict a
|
|
|
870
1001
|
assert.deepEqual(first, { heading: 'figure.label.group-figure', assigned: true, order: [true, true] })
|
|
871
1002
|
})
|
|
872
1003
|
|
|
873
|
-
test('the verdict
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
1004
|
+
test('the verdict tab shows the page\'s verdict, and the bar\'s history button its release history', async () => {
|
|
1005
|
+
const assigned = within =>
|
|
1006
|
+
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)
|
|
1007
|
+
await pressTab(TAB('verdict'))
|
|
1008
|
+
let shown = await assigned(null)
|
|
1009
|
+
assert.equal(shown?.length, 1, 'verdict')
|
|
1010
|
+
assert.equal(shown[0].tag, 'figure-verdict')
|
|
1011
|
+
assert.ok(shown[0].height > 0, 'figure-verdict is shown')
|
|
1012
|
+
await openOverlay('history')
|
|
1013
|
+
shown = await assigned('div[bar-overlay-panel=history]')
|
|
1014
|
+
assert.equal(shown?.length, 1, 'history')
|
|
1015
|
+
assert.equal(shown[0].tag, 'figure-history-panel')
|
|
1016
|
+
assert.ok(shown[0].height > 0, 'figure-history-panel is shown')
|
|
1017
|
+
/* Closed again: it lies over the side, and the checks after this press there. */
|
|
1018
|
+
await press('the history button', () => screenCheck.editor().shadowRoot.querySelector('ox-canvas-bar button[bar-overlay=history]'))
|
|
881
1019
|
})
|
|
882
1020
|
|
|
883
1021
|
test('the verdict tab\'s count is the report\'s, measured by the verdict', async () => {
|
|
@@ -946,7 +1084,7 @@ describe('the modeller page: the editor with the page\'s figure panel, verdict a
|
|
|
946
1084
|
await read(finding => product.answer('inspectFigure', () => ({ __typename: 'FigureInspection', blocked: true, findings: [finding] })), await read(() => alone.finding('occupancy-undeclared')))
|
|
947
1085
|
try {
|
|
948
1086
|
await open('fig-empty', 'empty')
|
|
949
|
-
await
|
|
1087
|
+
await openOverlay('history')
|
|
950
1088
|
await until('offered', () => !!screenCheck.deep('figure-history-panel')[0]?.shadowRoot?.querySelector('button[fit-base]'))
|
|
951
1089
|
await press('fit to the parts', () => screenCheck.deep('figure-history-panel')[0].shadowRoot.querySelector('button[fit-base]'))
|
|
952
1090
|
await until('said', () => screenCheck.deep('p[fit-error]')[0]?.textContent.trim() === 'figure.text.fit-needs-parts')
|
|
@@ -956,8 +1094,8 @@ describe('the modeller page: the editor with the page\'s figure panel, verdict a
|
|
|
956
1094
|
}
|
|
957
1095
|
})
|
|
958
1096
|
|
|
959
|
-
test('the view set in the
|
|
960
|
-
await
|
|
1097
|
+
test('the view set in the bar\'s menu holds for the next figure opened: its board is stood up with it', async () => {
|
|
1098
|
+
await openOverlay('view')
|
|
961
1099
|
const colour = await element('the sky colour', () => screenCheck.setting('figure.label.sky-color').querySelector('input[type=color]'))
|
|
962
1100
|
/* A colour picker is the browser's own window; its value is set as the picker sets it, and changed. */
|
|
963
1101
|
await colour.evaluate(input => {
|
|
@@ -968,9 +1106,11 @@ describe('the modeller page: the editor with the page\'s figure panel, verdict a
|
|
|
968
1106
|
try {
|
|
969
1107
|
assert.equal(await read(() => screenCheck.deep('figure-canvas')[0].scene.root.state.skyColor), '#336699', 'the new board\'s sky')
|
|
970
1108
|
} finally {
|
|
971
|
-
await
|
|
1109
|
+
await openOverlay('view')
|
|
972
1110
|
await press('reset the view', () => screenCheck.deep('figure-settings')[0].shadowRoot.querySelector('button[reset]'))
|
|
973
1111
|
await until('the view as it was', () => screenCheck.deep('figure-canvas')[0].scene.root.state.skyColor === '#7c7c7c')
|
|
1112
|
+
/* Closed again: it lies over the side, and the next check presses there. */
|
|
1113
|
+
await press('the view button', () => screenCheck.editor().shadowRoot.querySelector('ox-canvas-bar button[bar-overlay=view]'))
|
|
974
1114
|
}
|
|
975
1115
|
})
|
|
976
1116
|
|
|
@@ -978,6 +1118,8 @@ describe('the modeller page: the editor with the page\'s figure panel, verdict a
|
|
|
978
1118
|
await open('fig-p', 'arm-clip')
|
|
979
1119
|
await pressRow('upper-arm')
|
|
980
1120
|
await until('upper-arm chosen', () => screenCheck.editor().selected === 'upper-arm')
|
|
1121
|
+
/* The part's panel fits the side now; its places opened make it long enough to scroll. */
|
|
1122
|
+
await press('the capability fold', () => screenCheck.shownPanel().querySelector('section[capability] > details:not([open]) > summary'))
|
|
981
1123
|
const at = await read(() => {
|
|
982
1124
|
const r = screenCheck.shownPanel().getBoundingClientRect()
|
|
983
1125
|
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 방향 ·
|
|
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 방향 ·
|
|
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
|
+
})
|
package/test/fields.test.ts
CHANGED
|
@@ -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
|
|
74
|
-
|
|
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
|
})
|
|
@@ -43,6 +43,44 @@ test('the dock applies a proposal through the page, on the version the model saw
|
|
|
43
43
|
assert.equal(create.applied, undefined)
|
|
44
44
|
})
|
|
45
45
|
|
|
46
|
+
/*
|
|
47
|
+
The dock reads the draft when the person sends (figure audit 2026-10-16 N5). A save in flight is no edit: reading the
|
|
48
|
+
draft then was refused with the edit guard's Korean "save first" line, which the dock showed as the turn's error.
|
|
49
|
+
*/
|
|
50
|
+
test('the draft is read while a save is in flight', async () => {
|
|
51
|
+
const source = serializeFigureAsset(addFigureBox(createFigureAsset('A'), { id: 'body', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 } }))
|
|
52
|
+
let finish: (value: { id: string; source: string }) => void = () => {}
|
|
53
|
+
const session = createFigureProductSession({
|
|
54
|
+
load: async (id: string) => ({ id, source }),
|
|
55
|
+
save: (q: { id?: string; source: string }) => new Promise(resolve => (finish = () => resolve({ id: q.id ?? 'a', source: q.source })))
|
|
56
|
+
})
|
|
57
|
+
await session.open('a')
|
|
58
|
+
session.command({ kind: 'rename', name: 'B' } as never)
|
|
59
|
+
const saving = session.save()
|
|
60
|
+
assert.equal(session.read().saving, true)
|
|
61
|
+
assert.equal(JSON.parse(session.source()).name, 'B')
|
|
62
|
+
finish({ id: 'a', source })
|
|
63
|
+
await saving
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
/*
|
|
67
|
+
A revision is for the figure the model read, and a page with another figure open leaves it untaken (figure audit
|
|
68
|
+
2026-10-16 N3): the handler then says it needs that figure open. The version alone did not tell them apart -- two
|
|
69
|
+
figures opened in two page loads were both at their first context and revision 0.
|
|
70
|
+
*/
|
|
71
|
+
test('a revision made for another figure is left untaken, whatever version it names', async () => {
|
|
72
|
+
const source = serializeFigureAsset(addFigureBox(createFigureAsset('A'), { id: 'body', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 } }))
|
|
73
|
+
const session = createFigureProductSession({ load: async (id: string) => ({ id, source }), save: async (q: { id?: string; source: string }) => ({ id: q.id ?? 'a', source: q.source }) })
|
|
74
|
+
await session.open('a')
|
|
75
|
+
const now = session.read()
|
|
76
|
+
const other: FigureApplyDetail = { key: K1, steps: lid, create: false, figureId: 'b', context: now.context, revision: now.revision }
|
|
77
|
+
assert.equal(takeFigureOffer(session, other, { onSavedFigure: true, say }), false)
|
|
78
|
+
assert.equal(other.applied, undefined)
|
|
79
|
+
assert.equal(session.read().revision, now.revision)
|
|
80
|
+
const mine: FigureApplyDetail = { key: K1, steps: lid, create: false, figureId: 'a', context: now.context, revision: now.revision }
|
|
81
|
+
assert.equal(takeFigureOffer(session, mine, { onSavedFigure: true, say }), true)
|
|
82
|
+
})
|
|
83
|
+
|
|
46
84
|
/*
|
|
47
85
|
A figure not saved yet is a subject too (ADR-0061 decision 3): its conversation is anchored on the creation id the
|
|
48
86
|
first save inserts the row under, so the anchor is the same before and after that save. Taking a new figure's
|
|
@@ -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
|
+
})
|
package/test/layout.test.ts
CHANGED
|
@@ -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 가운데로', '바닥에 내려놓기', '눈금에
|
|
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
|
+
})
|
package/test/refusals.test.ts
CHANGED
|
@@ -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('벽
|
|
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', () => {
|
package/test/stage-model.test.ts
CHANGED
|
@@ -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
|
+
})
|
package/test/verdict.test.ts
CHANGED
|
@@ -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
|
+
})
|