@things-factory/figure-ui 10.1.271 → 10.1.273
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/modeller/canvas.ts +4 -2
- package/client/modeller/clips.ts +74 -4
- package/client/modeller/data-interface.ts +30 -18
- package/client/modeller/editor-styles.ts +17 -6
- package/client/modeller/editor.ts +2 -0
- package/client/modeller/fasten-form.ts +81 -6
- package/client/modeller/fasten-geometry.ts +5 -3
- package/client/modeller/figure-cavity-drawing.ts +1 -3
- package/client/modeller/figure-clips.ts +101 -8
- package/client/modeller/figure-data-panel.ts +384 -200
- package/client/modeller/figure-fasten-dialog.ts +41 -30
- package/client/modeller/figure-fasten-drawing.ts +24 -14
- package/client/modeller/figure-first-save.ts +4 -3
- package/client/modeller/figure-links.ts +109 -37
- package/client/modeller/figure-parts.ts +7 -11
- package/client/modeller/figure-preview.ts +299 -79
- package/client/modeller/figure-size-editor.ts +7 -2
- package/client/modeller/first-save.ts +8 -0
- package/client/modeller/focus-on-render.ts +4 -0
- package/client/modeller/pivot.ts +76 -0
- package/client/modeller/size-response.ts +14 -0
- package/client/modeller/stage-model.ts +50 -6
- package/dist-client/modeller/canvas.d.ts +1 -0
- package/dist-client/modeller/canvas.js +3 -2
- package/dist-client/modeller/canvas.js.map +1 -1
- package/dist-client/modeller/clips.d.ts +17 -3
- package/dist-client/modeller/clips.js +69 -4
- package/dist-client/modeller/clips.js.map +1 -1
- package/dist-client/modeller/data-interface.d.ts +11 -8
- package/dist-client/modeller/data-interface.js +27 -17
- package/dist-client/modeller/data-interface.js.map +1 -1
- package/dist-client/modeller/editor-styles.js +17 -6
- package/dist-client/modeller/editor-styles.js.map +1 -1
- package/dist-client/modeller/editor.js +2 -0
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/fasten-form.d.ts +13 -0
- package/dist-client/modeller/fasten-form.js +72 -7
- package/dist-client/modeller/fasten-form.js.map +1 -1
- package/dist-client/modeller/fasten-geometry.d.ts +3 -2
- package/dist-client/modeller/fasten-geometry.js +5 -3
- package/dist-client/modeller/fasten-geometry.js.map +1 -1
- package/dist-client/modeller/figure-cavity-drawing.js +1 -3
- package/dist-client/modeller/figure-cavity-drawing.js.map +1 -1
- package/dist-client/modeller/figure-clips.d.ts +9 -0
- package/dist-client/modeller/figure-clips.js +104 -8
- package/dist-client/modeller/figure-clips.js.map +1 -1
- package/dist-client/modeller/figure-data-panel.d.ts +10 -19
- package/dist-client/modeller/figure-data-panel.js +382 -190
- package/dist-client/modeller/figure-data-panel.js.map +1 -1
- package/dist-client/modeller/figure-fasten-dialog.js +39 -24
- package/dist-client/modeller/figure-fasten-dialog.js.map +1 -1
- package/dist-client/modeller/figure-fasten-drawing.js +24 -14
- package/dist-client/modeller/figure-fasten-drawing.js.map +1 -1
- package/dist-client/modeller/figure-first-save.js +4 -3
- package/dist-client/modeller/figure-first-save.js.map +1 -1
- package/dist-client/modeller/figure-links.d.ts +9 -0
- package/dist-client/modeller/figure-links.js +120 -51
- package/dist-client/modeller/figure-links.js.map +1 -1
- package/dist-client/modeller/figure-parts.js +7 -11
- package/dist-client/modeller/figure-parts.js.map +1 -1
- package/dist-client/modeller/figure-preview.d.ts +13 -0
- package/dist-client/modeller/figure-preview.js +296 -80
- package/dist-client/modeller/figure-preview.js.map +1 -1
- package/dist-client/modeller/figure-size-editor.js +7 -3
- package/dist-client/modeller/figure-size-editor.js.map +1 -1
- package/dist-client/modeller/first-save.d.ts +5 -0
- package/dist-client/modeller/first-save.js +7 -0
- package/dist-client/modeller/first-save.js.map +1 -1
- package/dist-client/modeller/focus-on-render.d.ts +2 -0
- package/dist-client/modeller/focus-on-render.js +4 -0
- package/dist-client/modeller/focus-on-render.js.map +1 -0
- package/dist-client/modeller/pivot.d.ts +12 -0
- package/dist-client/modeller/pivot.js +59 -0
- package/dist-client/modeller/pivot.js.map +1 -0
- package/dist-client/modeller/size-response.d.ts +6 -0
- package/dist-client/modeller/size-response.js +14 -0
- package/dist-client/modeller/size-response.js.map +1 -1
- package/dist-client/modeller/stage-model.d.ts +17 -3
- package/dist-client/modeller/stage-model.js +44 -6
- package/dist-client/modeller/stage-model.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +8 -7
- package/test/browser/editing-page.js +1 -1
- package/test/browser/editing.check.mjs +6 -5
- package/test/browser/links.check.mjs +30 -0
- package/test/browser/modeller.check.mjs +2 -2
- package/test/browser/parts-page.js +4 -0
- package/test/browser/parts.check.mjs +16 -10
- package/test/browser/screen-page.js +12 -1
- package/test/browser/screen.check.mjs +142 -11
- package/test/browser/section.check.mjs +21 -0
- package/test/browser/serve-response.mjs +5 -2
- package/test/browser/side-panel-sweep.check.mjs +171 -10
- package/test/clips.test.ts +49 -4
- package/test/data-motion.test.ts +39 -8
- package/test/fasten-form.test.ts +69 -0
- package/test/fasten-geometry.test.ts +13 -0
- package/test/first-save.test.ts +11 -1
- package/test/pivot.test.ts +57 -0
- package/test/size-response.test.ts +10 -2
- package/test/stage-model.test.ts +23 -1
- package/translations/en.json +31 -11
- package/translations/es.json +30 -10
- package/translations/ja.json +30 -10
- package/translations/ko.json +32 -12
- package/translations/ms.json +30 -10
- package/translations/zh.json +30 -10
- package/tutorial/humanoid/README.md +22 -0
- package/tutorial/humanoid/chapters.mjs +247 -0
- package/tutorial/humanoid/encode.mjs +26 -0
- package/tutorial/humanoid/lib.mjs +197 -0
- package/tutorial/humanoid/record.mjs +65 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@things-factory/figure-ui",
|
|
3
|
-
"version": "10.1.
|
|
3
|
+
"version": "10.1.273",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public",
|
|
6
6
|
"@things-factory:registry": "https://registry.npmjs.org"
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"clean": "npm run clean:server && npm run clean:client"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@hatiolab/figure-model": "^0.12.
|
|
30
|
+
"@hatiolab/figure-model": "^0.12.6",
|
|
31
31
|
"@hatiolab/things-scene": "^10.1.111",
|
|
32
32
|
"@material/web": "^2.0.0",
|
|
33
33
|
"@operato/data-grist": "^10.29.69",
|
|
@@ -42,11 +42,12 @@
|
|
|
42
42
|
"@operato/slot-layout-editor": "^10.29.58",
|
|
43
43
|
"@operato/styles": "^10.0.0",
|
|
44
44
|
"@operato/utils": "^10.29.43",
|
|
45
|
-
"@things-factory/ai-assistant": "^10.1.
|
|
46
|
-
"@things-factory/figure-service": "^10.1.
|
|
47
|
-
"@things-factory/figure-viewer": "^10.1.
|
|
48
|
-
"@things-factory/setting-base": "^10.1.
|
|
45
|
+
"@things-factory/ai-assistant": "^10.1.273",
|
|
46
|
+
"@things-factory/figure-service": "^10.1.273",
|
|
47
|
+
"@things-factory/figure-viewer": "^10.1.273",
|
|
48
|
+
"@things-factory/setting-base": "^10.1.273",
|
|
49
|
+
"json5": "^2.2.0",
|
|
49
50
|
"three": "^0.185.1"
|
|
50
51
|
},
|
|
51
|
-
"gitHead": "
|
|
52
|
+
"gitHead": "0ead3ad069edf9f1ef5076caec85916bdb26728f"
|
|
52
53
|
}
|
|
@@ -124,7 +124,7 @@ window.editing = {
|
|
|
124
124
|
},
|
|
125
125
|
/** The page point of a button by its words (the page loads no translations: the words are keys). */
|
|
126
126
|
button(words) {
|
|
127
|
-
const b = deep('button').find(b => b.textContent.includes(words))
|
|
127
|
+
const b = deep('button').find(b => b.textContent.includes(words) || b.title === words)
|
|
128
128
|
return b ? { ...centreOf(b), disabled: b.disabled } : null
|
|
129
129
|
},
|
|
130
130
|
/** The first range input inside the `host` elements: where its thumb is and how many pixels one unit of it spans. */
|
|
@@ -353,7 +353,7 @@ describe('the material: colours by purpose, one step each, a glow drawn while dr
|
|
|
353
353
|
test('a diameter typed on the arrangement tab is written once when it is left, as the radius it halves to', async () => {
|
|
354
354
|
await click(await read(() => probe.tab('figure.label.tab-short-response')))
|
|
355
355
|
const start = await revision()
|
|
356
|
-
await type('drum X · figure.label.size-diameter figure.label.size-measure', '600')
|
|
356
|
+
await type('drum X·Z · figure.label.size-diameter figure.label.size-measure', '600')
|
|
357
357
|
assert.equal(await revision(), start, 'nothing written yet')
|
|
358
358
|
await page().keyboard.press('Enter')
|
|
359
359
|
await sleep(400)
|
|
@@ -514,7 +514,8 @@ describe('a clip scrubbed and keyed (2026-09-24)', () => {
|
|
|
514
514
|
const typeInClips = async (label, text) => {
|
|
515
515
|
const field = await page().evaluateHandle(label => {
|
|
516
516
|
const deep = (root = document) => [...root.querySelectorAll('input')].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
|
|
517
|
-
|
|
517
|
+
/* The clip's own field, not the pose bar's above the clips: the bar keys the clip last scrubbed (tutorial 2026-11-12). */
|
|
518
|
+
return deep().find(i => i.type === 'number' && i.getAttribute('aria-label') === label && i.getRootNode().host?.localName === 'figure-clips' && !i.closest('div[pose-bar]')) ?? null
|
|
518
519
|
}, label)
|
|
519
520
|
assert.ok(await field.evaluate(i => !!i), `${label} in the clips`)
|
|
520
521
|
await field.evaluate(i => i.scrollIntoView({ block: 'center' }))
|
|
@@ -576,12 +577,12 @@ describe('a track moved round its loop, a clip copied (robot dog 2026-11-03)', (
|
|
|
576
577
|
await button.click()
|
|
577
578
|
}
|
|
578
579
|
|
|
579
|
-
test('a clip in a group is copied in one step; one in none is
|
|
580
|
-
/* The left door's clip is in no group, the right one's is in one. */
|
|
580
|
+
test('a clip in a group is copied in one step; one in none is offered too, and makes its group (review 2026-11-11)', async () => {
|
|
581
|
+
/* The left door's clip is in no group, the right one's is in one: both may be copied now. */
|
|
581
582
|
assert.equal(await read(() => {
|
|
582
583
|
const deep = (root = document) => [...root.querySelectorAll('button[copy-clip]')].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
|
|
583
584
|
return deep().map(b => b.disabled).join()
|
|
584
|
-
}), '
|
|
585
|
+
}), 'false,false')
|
|
585
586
|
const start = await revision()
|
|
586
587
|
await pressIn('button[copy-clip]', 1)
|
|
587
588
|
await until(async () => (await revision()) === start + 1, 'one step')
|
|
@@ -802,6 +802,23 @@ describe('the fastening window: tried freely, written once', () => {
|
|
|
802
802
|
assert.equal((await state()).revision, start.revision, 'nothing written while trying')
|
|
803
803
|
}
|
|
804
804
|
|
|
805
|
+
test('the ways to fasten stand on one line, a small view beside each word (user 2026-11-12)', async () => {
|
|
806
|
+
await pick('lamp')
|
|
807
|
+
await changeFastening()
|
|
808
|
+
const ways = await read(() => {
|
|
809
|
+
const deep = (s, root = document) => [...root.querySelectorAll(s)].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(s, e.shadowRoot)))
|
|
810
|
+
return deep('div[modes] > button[mode]').map(b => {
|
|
811
|
+
const r = b.getBoundingClientRect()
|
|
812
|
+
return { top: Math.round(r.top), height: Math.round(r.height) }
|
|
813
|
+
})
|
|
814
|
+
})
|
|
815
|
+
assert.ok(ways.length >= 2, `ways: ${JSON.stringify(ways)}`)
|
|
816
|
+
assert.equal(new Set(ways.map(w => w.top)).size, 1, `one line: ${JSON.stringify(ways)}`)
|
|
817
|
+
assert.ok(ways.every(w => w.height <= 30), `each as tall as a line: ${JSON.stringify(ways)}`)
|
|
818
|
+
await press('[cancel]')
|
|
819
|
+
await until(() => !probe.fastenTargets(), [], 'the window closed')
|
|
820
|
+
})
|
|
821
|
+
|
|
805
822
|
test('three choices tried, then 취소: nothing written, the window closed, nothing of them drawn', async () => {
|
|
806
823
|
await pick('lamp')
|
|
807
824
|
const was = await lamp()
|
|
@@ -905,6 +922,19 @@ describe('a motion on the motion tab', () => {
|
|
|
905
922
|
await until(() => look.drawn().includes('__preview:min:upper-arm') && look.drawn().includes('__preview:max:upper-arm'), [], 'both ends drawn')
|
|
906
923
|
assert.equal((await all('p[motion-summary]', 'figure.text.motion-summary')).length, 1)
|
|
907
924
|
})
|
|
925
|
+
|
|
926
|
+
test('its delete and edit are icons on the motion\'s line, and the size-by-state form is folded until asked for (user 2026-11-12)', async () => {
|
|
927
|
+
const seen = await read(() => {
|
|
928
|
+
const deep = (s, root = document) => [...root.querySelectorAll(s)].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(s, e.shadowRoot)))
|
|
929
|
+
const line = deep('div[motion-axis]')[0]
|
|
930
|
+
const icons = [...line.querySelectorAll('button[act]')].map(b => ({ title: b.title, words: b.textContent.replace(/delete|edit/g, '').trim() }))
|
|
931
|
+
const fold = deep('details[state-size-add]')[0]
|
|
932
|
+
return { icons, folded: fold ? !fold.open : null, heading: deep('h4[state-size]').length }
|
|
933
|
+
})
|
|
934
|
+
assert.deepEqual(seen.icons.map(i => i.words), ['', ''], 'no words beside the icons')
|
|
935
|
+
assert.deepEqual(seen.icons.map(i => i.title), ['figure.button.remove-motion', 'figure.button.change-motion'])
|
|
936
|
+
assert.deepEqual([seen.folded, seen.heading], [true, 0], 'folded, no heading over nothing')
|
|
937
|
+
})
|
|
908
938
|
})
|
|
909
939
|
|
|
910
940
|
describe('pressed on the canvas while the fastening window is open (ADR-0140 stage 4)', () => {
|
|
@@ -603,7 +603,7 @@ describe('a motion is changed where it is (2026-09-25)', () => {
|
|
|
603
603
|
const button = async text =>
|
|
604
604
|
page().evaluate(text => {
|
|
605
605
|
const deep = (root = document) => [...root.querySelectorAll('button')].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
|
|
606
|
-
const b = deep().find(x => x.textContent.includes(text))
|
|
606
|
+
const b = deep().find(x => x.textContent.includes(text) || x.title === text)
|
|
607
607
|
if (!b) return null
|
|
608
608
|
b.scrollIntoView({ block: 'center' })
|
|
609
609
|
const r = b.getBoundingClientRect()
|
|
@@ -641,7 +641,7 @@ describe('a motion is changed where it is (2026-09-25)', () => {
|
|
|
641
641
|
page().evaluate(
|
|
642
642
|
(text, attribute) => {
|
|
643
643
|
const deep = (root = document) => [...root.querySelectorAll('button')].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
|
|
644
|
-
const b = deep().find(x => x.textContent.includes(text) && (!attribute || x.hasAttribute(attribute)))
|
|
644
|
+
const b = deep().find(x => (x.textContent.includes(text) || x.title === text) && (!attribute || x.hasAttribute(attribute)))
|
|
645
645
|
if (!b) return null
|
|
646
646
|
b.scrollIntoView({ block: 'center' })
|
|
647
647
|
const r = b.getBoundingClientRect()
|
|
@@ -119,6 +119,10 @@ window.parts = {
|
|
|
119
119
|
}
|
|
120
120
|
return { row: box(li), name: box(li.querySelector('span[label]')), ops: box(li.querySelector('div[ops]')) }
|
|
121
121
|
},
|
|
122
|
+
/** Each row's height, the chosen one among them. */
|
|
123
|
+
rowHeights() {
|
|
124
|
+
return [...panel().shadowRoot.querySelectorAll('li')].map(li => li.getBoundingClientRect().height)
|
|
125
|
+
},
|
|
122
126
|
/** The list made this wide (the start pane narrowed by the author). */
|
|
123
127
|
narrow(px) {
|
|
124
128
|
panel().style.width = px ? `${px}px` : ''
|
|
@@ -305,17 +305,23 @@ describe('the list narrowed: the selected row keeps its name and its buttons in
|
|
|
305
305
|
before(() => mount('box', 'three'))
|
|
306
306
|
after(() => read(() => parts.narrow(0)))
|
|
307
307
|
|
|
308
|
-
|
|
308
|
+
/*
|
|
309
|
+
Chosen, a row keeps its height, wide or narrow: its buttons beside the name, the name cut short. Going to a line of
|
|
310
|
+
their own under 200 px, they pushed every row under it down as a part was chosen, and the next press landed on
|
|
311
|
+
another part (user 2026-11-12).
|
|
312
|
+
*/
|
|
313
|
+
test('the buttons stand beside the name, wide or narrow; the chosen row is as tall as the others, inside the list', async () => {
|
|
309
314
|
await pick('b')
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
315
|
+
for (const width of [320, 180]) {
|
|
316
|
+
await read(width => parts.narrow(width), width)
|
|
317
|
+
await sleep(200)
|
|
318
|
+
const at = await read(() => parts.selectedRowBoxes())
|
|
319
|
+
assert.ok(at.ops.top < at.name.bottom, `${width}: beside the name: ${JSON.stringify(at)}`)
|
|
320
|
+
assert.ok(at.ops.right <= at.row.right + 1 && at.name.right <= at.row.right + 1, `${width}: inside the row: ${JSON.stringify(at)}`)
|
|
321
|
+
assert.ok(at.name.right > at.name.left && at.name.right <= at.ops.left + 1, `${width}: the name shows, clear of the buttons: ${JSON.stringify(at)}`)
|
|
322
|
+
const heights = await read(() => parts.rowHeights())
|
|
323
|
+
assert.equal(new Set(heights.map(Math.round)).size, 1, `${width}: every row as tall: ${heights}`)
|
|
324
|
+
}
|
|
319
325
|
})
|
|
320
326
|
})
|
|
321
327
|
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
compileFigureAsset,
|
|
17
17
|
createFigureAsset,
|
|
18
18
|
editFigurePart,
|
|
19
|
+
figurePartsExtentOf,
|
|
19
20
|
robotArmFigure,
|
|
20
21
|
serializeFigureAsset
|
|
21
22
|
} from '@hatiolab/figure-model'
|
|
@@ -51,6 +52,15 @@ const FIGURES = {
|
|
|
51
52
|
/* A figure hung from the ceiling, its box under it. */
|
|
52
53
|
ceiling: () => applyFigureAuthoring(addFigureBox(named('Hung'), { id: 'box', size: { x: 400, y: 400, z: 400 }, position: { x: 0, y: -200, z: 0 } }), { kind: 'set-placement', placement: 'ceiling' }),
|
|
53
54
|
/* A drum: a palette part whose sizes are its own fields. */
|
|
55
|
+
/* A box whose every size is held at one value: the board cannot change its size. */
|
|
56
|
+
fixed: () => {
|
|
57
|
+
const asset = addFigureBox(named('Fixed'), cube('box', 0))
|
|
58
|
+
for (const input of asset.document.model.inputs) {
|
|
59
|
+
const at = asset.designInputs[input.id]
|
|
60
|
+
if (/^size\.[xyz]$/.test(input.id) && typeof at === 'number') Object.assign(input, { min: at, max: at })
|
|
61
|
+
}
|
|
62
|
+
return asset
|
|
63
|
+
},
|
|
54
64
|
drum: () => addFigurePrimitive(named('Drum'), { id: 'drum', kind: 'cylinder', dimensions: { radius: 200, length: 500 }, position: { x: 0, y: 250, z: 0 } }),
|
|
55
65
|
/* A lighting tower that follows its height only: the wider variants ask a width it does not declare. */
|
|
56
66
|
tower: () => {
|
|
@@ -77,7 +87,8 @@ const FIGURES = {
|
|
|
77
87
|
return applyFigureAuthoring(a, { kind: 'make-repeat', part: 'roller', axis: 'x', pitch: 200 })
|
|
78
88
|
},
|
|
79
89
|
/* The robot arm with a clip that turns its base: controls and a clip in the preview. */
|
|
80
|
-
|
|
90
|
+
/* Declares its size, so the preview offers the size help over it (shown only where the size can change). */
|
|
91
|
+
'arm-clip': () => editFigurePart(applyFigureAuthoring(robotArmFigure(), { kind: 'declare-instance-size', at: figurePartsExtentOf(robotArmFigure()) }), { kind: 'set-driver', state: 'j1-base', clip: 'cycle', duration: 4, keys: [{ seconds: 0, value: 0 }, { seconds: 4, value: 60 }] })
|
|
81
92
|
}
|
|
82
93
|
|
|
83
94
|
/** A figure by name, as saved text. */
|
|
@@ -604,6 +604,15 @@ describe('the preview: registered as a board registers it, posed and played, the
|
|
|
604
604
|
await mode('preview')
|
|
605
605
|
await until('the preview stands', () => !!screenCheck.deep('figure-preview')[0]?.shadowRoot?.querySelector('div[setup] select'))
|
|
606
606
|
assert.equal(await read(() => screenCheck.deep('figure-preview')[0].shadowRoot.querySelector('div[setup] select').value), 'forearm', 'the help speaks of the part chosen')
|
|
607
|
+
/* One line over the figure, its words in its title: three lines of help covered the figure (user 2026-11-12). */
|
|
608
|
+
const setup = await read(() => {
|
|
609
|
+
const box = screenCheck.deep('figure-preview')[0].shadowRoot.querySelector('div[setup]')
|
|
610
|
+
const top = s => { const r = box.querySelector(s).getBoundingClientRect(); return Math.round(r.top + r.height / 2) }
|
|
611
|
+
return { height: box.getBoundingClientRect().height, select: top('select'), button: top('button'), title: box.title }
|
|
612
|
+
})
|
|
613
|
+
assert.ok(Math.abs(setup.select - setup.button) <= 2, `one line: ${JSON.stringify(setup)}`)
|
|
614
|
+
assert.ok(setup.height <= 44, `no taller than a line: ${JSON.stringify(setup)}`)
|
|
615
|
+
assert.ok(setup.title.length > 0, 'its words in its title')
|
|
607
616
|
await mode('edit')
|
|
608
617
|
await until('the edit canvas in front', () => screenCheck.deep('figure-preview')[0].hidden)
|
|
609
618
|
assert.equal(await selected(), 'forearm', 'still chosen')
|
|
@@ -622,6 +631,21 @@ describe('the preview: registered as a board registers it, posed and played, the
|
|
|
622
631
|
assert.ok(dials.filter(d => d.unit !== 'deg').every(d => !d.dial), 'no dial for what does not turn')
|
|
623
632
|
|
|
624
633
|
const before = { revision: await revision(), pose: await read(() => JSON.parse(screenCheck.editor().session.source()).stateDefaults['j2-shoulder'] ?? 0) }
|
|
634
|
+
/*
|
|
635
|
+
The three buttons stand on one line from the start, unable until there is something to keep or clear: they do not
|
|
636
|
+
come and go and move the rows under them (user 2026-11-12).
|
|
637
|
+
*/
|
|
638
|
+
const row = () => ['pose', 'clear-tried', 'fold-controls'].map(name => {
|
|
639
|
+
const b = screenCheck.deep('figure-preview')[0].shadowRoot.querySelector(`div[clips] button[${name}]`)
|
|
640
|
+
return b ? { top: Math.round(b.getBoundingClientRect().top + b.getBoundingClientRect().height / 2), height: Math.round(b.getBoundingClientRect().height), disabled: b.disabled } : null
|
|
641
|
+
})
|
|
642
|
+
const untried = await read(row)
|
|
643
|
+
assert.ok(untried.every(Boolean), 'all three there')
|
|
644
|
+
assert.equal(new Set(untried.map(b => b.top)).size, 1, 'on one line')
|
|
645
|
+
/* The fold an icon now; keep and clear each one line high, not broken inside a word (user 2026-11-12). */
|
|
646
|
+
assert.equal(untried[0].height, untried[1].height, `each label on one line, not broken inside a word: ${JSON.stringify(untried)}`)
|
|
647
|
+
assert.ok(untried[0].height <= 24, `one line high: ${JSON.stringify(untried)}`)
|
|
648
|
+
assert.equal(untried[2].disabled, false, 'folding is always there to press')
|
|
625
649
|
const slider = await element('the shoulder', () => screenCheck.previewControl('j2-shoulder').querySelector('input[type=range]'))
|
|
626
650
|
await slider.focus()
|
|
627
651
|
for (let i = 0; i < 10; i++) await page().keyboard.press('ArrowRight')
|
|
@@ -633,19 +657,45 @@ describe('the preview: registered as a board registers it, posed and played, the
|
|
|
633
657
|
assert.equal(await read(() => JSON.parse(screenCheck.editor().session.source()).stateDefaults['j2-shoulder']), tried, 'the pose kept is the one tried')
|
|
634
658
|
})
|
|
635
659
|
|
|
660
|
+
test('a row\'s name dragged sideways scrubs its value, as the property panel\'s do: a degree a pixel, Shift ten times finer, held in its range (user 2026-11-12)', async () => {
|
|
661
|
+
const at = () => read(() => {
|
|
662
|
+
const l = screenCheck.previewControl('j1-base')
|
|
663
|
+
const r = l.querySelector('span[scrub]').getBoundingClientRect()
|
|
664
|
+
return { x: r.left + 10, y: r.top + r.height / 2, value: Number(l.querySelector('input[typed]').value), max: Number(l.querySelector('input[type=range]').max), cursor: getComputedStyle(l.querySelector('span[scrub]')).cursor }
|
|
665
|
+
})
|
|
666
|
+
const drag = async (dx, shift = false) => {
|
|
667
|
+
const from = await at()
|
|
668
|
+
if (shift) await page().keyboard.down('Shift')
|
|
669
|
+
await page().mouse.move(from.x, from.y)
|
|
670
|
+
await page().mouse.down()
|
|
671
|
+
await page().mouse.move(from.x + dx / 2, from.y)
|
|
672
|
+
await page().mouse.move(from.x + dx, from.y)
|
|
673
|
+
await page().mouse.up()
|
|
674
|
+
if (shift) await page().keyboard.up('Shift')
|
|
675
|
+
return (await at()).value - from.value
|
|
676
|
+
}
|
|
677
|
+
assert.equal((await at()).cursor, 'ew-resize', 'the name says it can be dragged')
|
|
678
|
+
assert.equal(await drag(20), 20, 'a degree a pixel')
|
|
679
|
+
assert.equal(Math.round((await drag(-20, true)) * 10) / 10, -2, 'Shift: a tenth of a degree a pixel')
|
|
680
|
+
const top = (await at()).max
|
|
681
|
+
await drag(5000)
|
|
682
|
+
assert.equal((await at()).value, top, 'held at the range\'s end')
|
|
683
|
+
})
|
|
684
|
+
|
|
636
685
|
test('a clip stands still until it is played: its speed reads 0, and the speed played is what every instance is given', async () => {
|
|
637
686
|
const clip = () =>
|
|
638
687
|
read(() => {
|
|
639
688
|
const l = screenCheck.previewControl('cycle')
|
|
640
|
-
return { value: Number(l.querySelector('input[type=range]').value), shown: l.querySelector('
|
|
689
|
+
return { value: Number(l.querySelector('input[type=range]').value), shown: l.querySelector('input[typed]').value }
|
|
641
690
|
})
|
|
642
|
-
assert.deepEqual(await clip(), { value: 0, shown: '0
|
|
691
|
+
assert.deepEqual(await clip(), { value: 0, shown: '0' }, 'not played yet')
|
|
643
692
|
const slider = await element('the clip\'s speed', () => screenCheck.previewControl('cycle').querySelector('input[type=range]'))
|
|
644
693
|
await slider.focus()
|
|
645
694
|
await page().keyboard.press('ArrowRight')
|
|
646
|
-
await until('played', () => screenCheck.previewControl('cycle').querySelector('
|
|
695
|
+
await until('played', () => screenCheck.previewControl('cycle').querySelector('input[typed]').value !== '0')
|
|
647
696
|
const now = await clip()
|
|
648
|
-
|
|
697
|
+
/* The speed in the same box every slider's value has (user 2026-11-12). */
|
|
698
|
+
assert.equal(now.shown, String(Math.round(now.value * 100) / 100))
|
|
649
699
|
const given = await read(() => screenCheck.deep('figure-preview')[0].scene.root.components.map(c => c.state.clips?.cycle))
|
|
650
700
|
assert.equal(given.length, 1, 'the one figure the preview stands')
|
|
651
701
|
assert.ok(given.every(v => v === now.value), `each given the speed shown: ${given}`)
|
|
@@ -675,7 +725,8 @@ describe('the preview: registered as a board registers it, posed and played, the
|
|
|
675
725
|
assert.ok((await given()).every(p => p['j2-shoulder'] === 37), 'a control the clip does not drive keeps its value')
|
|
676
726
|
await press('back to the default pose', () => screenCheck.deep('figure-preview')[0].shadowRoot.querySelector('button[clear-tried]'))
|
|
677
727
|
await until('all let go', () => screenCheck.deep('figure-preview')[0].scene.root.components.every(c => Object.keys(c.state.parameters ?? {}).length === 0))
|
|
678
|
-
|
|
728
|
+
/* Nothing left to clear: the button stays where it is, unable (user 2026-11-12: 「보이다 안보이다 하지마라」). */
|
|
729
|
+
assert.equal(await read(() => screenCheck.deep('figure-preview')[0].shadowRoot.querySelector('button[clear-tried]')?.disabled), true, 'nothing left to clear')
|
|
679
730
|
})
|
|
680
731
|
|
|
681
732
|
test('the preview looks from the front, the side or above, and its controls fold out of the way (robot dog, 2026-11-03)', async () => {
|
|
@@ -690,10 +741,56 @@ describe('the preview: registered as a board registers it, posed and played, the
|
|
|
690
741
|
await press('fold the controls', () => screenCheck.deep('figure-preview')[0].shadowRoot.querySelector('button[fold-controls]'))
|
|
691
742
|
await until('folded', () => !screenCheck.previewControl('j1-base'))
|
|
692
743
|
assert.ok(await read(() => screenCheck.deep('figure-preview')[0].shadowRoot.querySelector('div[clips] button[fold-controls]')), 'the one button stays')
|
|
744
|
+
/* Folded, it says what it opens, and stands on the stage's bottom, not at its top (user 2026-11-12). */
|
|
745
|
+
const folded = await read(() => {
|
|
746
|
+
const host = screenCheck.deep('figure-preview')[0]
|
|
747
|
+
const clips = host.shadowRoot.querySelector('div[clips]')
|
|
748
|
+
return { words: clips.querySelector('button[fold-controls]').textContent.replace('unfold_more', '').trim(), gap: Math.round(host.getBoundingClientRect().bottom - clips.getBoundingClientRect().bottom) }
|
|
749
|
+
})
|
|
750
|
+
assert.deepEqual(folded, { words: 'figure.button.controls-unfold', gap: 8 })
|
|
693
751
|
await press('unfold the controls', () => screenCheck.deep('figure-preview')[0].shadowRoot.querySelector('button[fold-controls]'))
|
|
694
752
|
await until('back', () => !!screenCheck.previewControl('j1-base'))
|
|
695
753
|
})
|
|
696
754
|
|
|
755
|
+
test('short (a window 300 high), the controls stay under the legend and scroll, their keep line and size help in sight (user 2026-11-12)', async () => {
|
|
756
|
+
const viewport = page().viewport()
|
|
757
|
+
await mode('preview')
|
|
758
|
+
await page().setViewport({ width: viewport.width, height: 300 })
|
|
759
|
+
try {
|
|
760
|
+
await until('the controls held short', () => {
|
|
761
|
+
const root = screenCheck.deep('figure-preview')[0].shadowRoot
|
|
762
|
+
const clips = root.querySelector('div[clips]')
|
|
763
|
+
return clips && clips.scrollHeight > clips.clientHeight + 10
|
|
764
|
+
})
|
|
765
|
+
const at = () => read(() => {
|
|
766
|
+
const host = screenCheck.deep('figure-preview')[0]
|
|
767
|
+
const root = host.shadowRoot
|
|
768
|
+
const r = el => el.getBoundingClientRect()
|
|
769
|
+
const clips = root.querySelector('div[clips]')
|
|
770
|
+
return {
|
|
771
|
+
legend: r(root.querySelector('div[legend]')).bottom,
|
|
772
|
+
stage: r(host).bottom,
|
|
773
|
+
clips: { top: r(clips).top, bottom: r(clips).bottom },
|
|
774
|
+
actions: r(root.querySelector('div[clips] div[actions]')).top,
|
|
775
|
+
setup: r(root.querySelector('div[clips] div[setup]')).bottom
|
|
776
|
+
}
|
|
777
|
+
})
|
|
778
|
+
const top = await at()
|
|
779
|
+
assert.ok(top.clips.top >= top.legend, `under the legend: ${JSON.stringify(top)}`)
|
|
780
|
+
assert.ok(top.clips.bottom <= top.stage, `inside the stage: ${JSON.stringify(top)}`)
|
|
781
|
+
await read(() => { const c = screenCheck.deep('figure-preview')[0].shadowRoot.querySelector('div[clips]'); c.scrollTop = c.scrollHeight })
|
|
782
|
+
await new Promise(resolve => setTimeout(resolve, 100))
|
|
783
|
+
const end = await at()
|
|
784
|
+
assert.ok(Math.abs(end.actions - end.clips.top) <= 1, `the keep line stays at the top as the rows scroll: ${JSON.stringify(end)}`)
|
|
785
|
+
assert.ok(Math.abs(end.setup - end.clips.bottom) <= 1, `the size help stays at the bottom: ${JSON.stringify(end)}`)
|
|
786
|
+
await read(() => { screenCheck.deep('figure-preview')[0].shadowRoot.querySelector('div[clips]').scrollTop = 0 })
|
|
787
|
+
const start = await at()
|
|
788
|
+
assert.ok(Math.abs(start.setup - start.clips.bottom) <= 1, `scrolled back, the size help still at the bottom: ${JSON.stringify(start)}`)
|
|
789
|
+
} finally {
|
|
790
|
+
await page().setViewport(viewport)
|
|
791
|
+
}
|
|
792
|
+
})
|
|
793
|
+
|
|
697
794
|
test('narrow (1024 wide and less, both sides open), the size help and the controls share the bottom row without covering each other', async () => {
|
|
698
795
|
const viewport = page().viewport()
|
|
699
796
|
await page().setViewport({ width: 1024, height: 768 })
|
|
@@ -713,13 +810,25 @@ describe('the preview: registered as a board registers it, posed and played, the
|
|
|
713
810
|
const r = el.getBoundingClientRect()
|
|
714
811
|
return { left: r.left, right: r.right, top: r.top, bottom: r.bottom }
|
|
715
812
|
}
|
|
716
|
-
|
|
813
|
+
const clips = root.querySelector('div[foot] > div[clips]')
|
|
814
|
+
return {
|
|
815
|
+
host: rect(screenCheck.deep('figure-preview')[0]),
|
|
816
|
+
foot: rect(root.querySelector('div[foot]')),
|
|
817
|
+
setup: rect(clips.querySelector(':scope > div[setup] > button')),
|
|
818
|
+
last: clips.lastElementChild.hasAttribute('setup'),
|
|
819
|
+
clips: rect(clips),
|
|
820
|
+
typed: [...clips.querySelectorAll('input[typed]')].map(rect),
|
|
821
|
+
padding: parseFloat(getComputedStyle(clips).paddingRight)
|
|
822
|
+
}
|
|
717
823
|
})
|
|
718
824
|
const inside = (a, b) => a.left >= b.left - 0.5 && a.right <= b.right + 0.5 && a.top >= b.top - 0.5 && a.bottom <= b.bottom + 0.5
|
|
719
|
-
|
|
720
|
-
assert.ok(inside(boxes.
|
|
721
|
-
assert.ok(
|
|
722
|
-
assert.ok(boxes.setup.
|
|
825
|
+
/* The help is the controls' last row, as wide as they are: a box of its own left a gap at its right (user 2026-11-12). */
|
|
826
|
+
assert.ok(inside(boxes.clips, boxes.foot), `${width}: in the bottom row: ${JSON.stringify(boxes)}`)
|
|
827
|
+
assert.ok(boxes.last, `${width}: the help is the controls' last row`)
|
|
828
|
+
assert.ok(Math.abs(boxes.setup.right - (boxes.clips.right - boxes.padding)) <= 1, `${width}: the help reaches the controls' right edge: ${JSON.stringify(boxes)}`)
|
|
829
|
+
/* Every slider's value in one column of boxes. */
|
|
830
|
+
assert.equal(new Set(boxes.typed.map(b => `${Math.round(b.left)}-${Math.round(b.right)}`)).size, 1, `${width}: one column of value boxes`)
|
|
831
|
+
assert.ok(Math.abs(boxes.typed[0].right - boxes.setup.right) <= 1, `${width}: the value boxes end where the help does`)
|
|
723
832
|
assert.ok(Math.abs(boxes.host.bottom - 8 - boxes.foot.bottom) <= 1, `${width}: the row stands on the stage's bottom edge: ${JSON.stringify(boxes)}`)
|
|
724
833
|
}
|
|
725
834
|
} finally {
|
|
@@ -759,6 +868,28 @@ describe('the preview: registered as a board registers it, posed and played, the
|
|
|
759
868
|
})
|
|
760
869
|
})
|
|
761
870
|
|
|
871
|
+
describe('the preview of a figure whose size cannot change', () => {
|
|
872
|
+
test('has no size help over it (user 2026-11-12)', async () => {
|
|
873
|
+
await mount('fixed')
|
|
874
|
+
await press('the preview mode', () => screenCheck.deep('button[mode]').find(b => b.title === 'figure.text.mode-preview-explained'))
|
|
875
|
+
await until('the preview stands', () => !!screenCheck.deep('figure-preview')[0]?.shadowRoot?.querySelector('div[foot]'))
|
|
876
|
+
assert.equal(await read(() => !!screenCheck.deep('figure-preview')[0].shadowRoot.querySelector('div[setup]')), false, 'no size help on a figure of one size')
|
|
877
|
+
/* Its size said at the legend's end, on the legend's one line; no box of sliders under it (user 2026-11-12). */
|
|
878
|
+
const viewport = page().viewport()
|
|
879
|
+
await page().setViewport({ ...viewport, width: 2400 })
|
|
880
|
+
await new Promise(resolve => setTimeout(resolve, 300))
|
|
881
|
+
const legend = await read(() => {
|
|
882
|
+
const root = screenCheck.deep('figure-preview')[0].shadowRoot
|
|
883
|
+
const middle = el => { const r = el.getBoundingClientRect(); return Math.round(r.top + r.height / 2) }
|
|
884
|
+
return { said: root.querySelector('div[legend] span[said]')?.textContent.trim(), sizes: !!root.querySelector('div[sizes]'), lines: new Set([...root.querySelectorAll('div[legend] button, div[legend] span[said]')].map(middle)).size }
|
|
885
|
+
})
|
|
886
|
+
await page().setViewport(viewport)
|
|
887
|
+
/* This page reads keys, not words, long enough to wrap the legend in a narrower window. */
|
|
888
|
+
assert.deepEqual(legend, { said: '400 × 400 × 400 mm', sizes: false, lines: 1 })
|
|
889
|
+
await boardsUp()
|
|
890
|
+
})
|
|
891
|
+
})
|
|
892
|
+
|
|
762
893
|
describe('the preview of a figure that does not declare every size', () => {
|
|
763
894
|
test('a variant drawn at another size than it asks says so in the legend', async () => {
|
|
764
895
|
await mount('tower')
|
|
@@ -818,7 +949,7 @@ describe('the preview stands one figure the author stretches (first 15 minutes,
|
|
|
818
949
|
})
|
|
819
950
|
/* The box is the figure's body at 6 m: the frame, as wide as asked (ADR-0145). */
|
|
820
951
|
await until('the figure 6 m wide, its body', () => Math.round(screenCheck.deep('figure-preview')[0].scene.root.components[0].model.width) === 6000)
|
|
821
|
-
const said = await read(() => screenCheck.deep('figure-preview')[0].shadowRoot.querySelector('div[
|
|
952
|
+
const said = await read(() => screenCheck.deep('figure-preview')[0].shadowRoot.querySelector('div[legend] span[said]').textContent.trim())
|
|
822
953
|
assert.ok(said.startsWith('6,000 × '), said)
|
|
823
954
|
const box = await read(() => { const m = screenCheck.deep('figure-preview')[0].scene.root.components[0].model; return m.left + m.width / 2 })
|
|
824
955
|
assert.equal(Math.round(box), await read(() => screenCheck.deep('figure-preview')[0].board.width / 2), 'grown about the board\'s centre')
|
|
@@ -748,6 +748,27 @@ describe('the fastening drawing in the fastening window', () => {
|
|
|
748
748
|
assert.deepEqual(joint.body0, { part: 'frame', face: 'front' })
|
|
749
749
|
assert.deepEqual([joint.body1, joint.origin?.facing], [{ part: 'leaf' }, 'meet'], 'set on the front by its real shape (ADR-0140 3.2)')
|
|
750
750
|
})
|
|
751
|
+
|
|
752
|
+
/* Tutorial 2026-11-11: the field a size opened did not take the keys; 600 typed and Enter fastened the part where it was. */
|
|
753
|
+
test('a size pressed in the drawing opens its field with the keys in it: typed and Enter moves the part there, the window stays', async () => {
|
|
754
|
+
await change()
|
|
755
|
+
await until(() => !!findDeep('text[dim]', { within: 'figure-fasten-drawing' }), 'a size in the drawing')
|
|
756
|
+
const start = await state()
|
|
757
|
+
await press('text[dim]', { within: 'figure-fasten-drawing' })
|
|
758
|
+
await until(() => {
|
|
759
|
+
const field = findDeep('div[where] input', { within: 'figure-fasten-drawing' })
|
|
760
|
+
return !!field && field.getRootNode().activeElement === field
|
|
761
|
+
}, 'the size\'s field, with the keys in it')
|
|
762
|
+
await page().keyboard.type('37')
|
|
763
|
+
await page().keyboard.press('Enter')
|
|
764
|
+
await sleep(300)
|
|
765
|
+
assert.ok(await read(() => !!probe.fastenTargets()), 'the window stays open')
|
|
766
|
+
assert.equal((await state()).revision, start.revision, 'nothing written before 적용')
|
|
767
|
+
await press('[apply]', { within: 'figure-fasten-dialog' })
|
|
768
|
+
await until(r => probe.state().revision === r + 1, 'one step', start.revision)
|
|
769
|
+
const align = (await read(() => probe.fastening('leaf'))).origin?.align ?? {}
|
|
770
|
+
assert.ok(Object.values(align).some(a => a?.mm === 37), `the typed size: ${JSON.stringify(align)}`)
|
|
771
|
+
})
|
|
751
772
|
})
|
|
752
773
|
|
|
753
774
|
describe('the face choice: asked where the edit was made, and the same edit goes again with it', () => {
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
/* Manual browser verification. Uses buildPage's FIGURE_MODEL / THINGS_SCENE overrides when provided. */
|
|
2
2
|
import { buildPage } from './build.mjs'
|
|
3
3
|
import { createServer } from 'node:http'
|
|
4
|
-
import { readFileSync } from 'node:fs'
|
|
4
|
+
import { readFileSync, writeFileSync } from 'node:fs'
|
|
5
5
|
import { dirname, join } from 'node:path'
|
|
6
6
|
import { createRequire } from 'node:module'
|
|
7
7
|
const require = createRequire(import.meta.url)
|
|
8
8
|
const bundle = await buildPage(process.argv[2] ?? 'response-page.js')
|
|
9
|
+
/* The words the modeller reads come from figure-ui and figure-viewer, as the app merges them: one alone showed keys. */
|
|
10
|
+
const koWords = join(process.env.TMPDIR ?? '/tmp', 'figure-ui-harness-ko.json')
|
|
11
|
+
writeFileSync(koWords, JSON.stringify({ ...JSON.parse(readFileSync(require.resolve('@things-factory/figure-viewer/translations/ko.json'), 'utf8')), ...JSON.parse(readFileSync(new URL('../../translations/ko.json', import.meta.url), 'utf8')) }))
|
|
9
12
|
const routes = new Map([
|
|
10
13
|
['/page.js', [bundle, 'application/javascript']],
|
|
11
|
-
['/ko.json', [
|
|
14
|
+
['/ko.json', [koWords, 'application/json']],
|
|
12
15
|
['/icons.woff2', [require.resolve('material-symbols/material-symbols-outlined.woff2'), 'font/woff2']],
|
|
13
16
|
['/workers/markdown-webworker.js', [join(dirname(bundle), 'workers/markdown-webworker.js'), 'application/javascript']]
|
|
14
17
|
])
|