@things-factory/figure-ui 10.1.207 → 10.1.210
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/data-interface.ts +5 -5
- package/client/modeller/editor.ts +74 -57
- package/client/modeller/fasten-geometry.ts +2 -1
- package/client/modeller/figure-links.ts +26 -120
- package/client/modeller/figure-size-editor.ts +196 -0
- package/client/modeller/joint-drag.ts +25 -21
- package/client/modeller/joint-fan.ts +4 -3
- package/client/modeller/links.ts +36 -24
- package/client/modeller/motion-ends.ts +10 -6
- package/client/modeller/size-response.ts +95 -0
- package/dist-client/modeller/data-interface.d.ts +1 -1
- package/dist-client/modeller/data-interface.js +5 -5
- package/dist-client/modeller/data-interface.js.map +1 -1
- package/dist-client/modeller/editor.d.ts +9 -1
- package/dist-client/modeller/editor.js +80 -58
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/fasten-geometry.js +1 -1
- package/dist-client/modeller/fasten-geometry.js.map +1 -1
- package/dist-client/modeller/figure-links.d.ts +1 -4
- package/dist-client/modeller/figure-links.js +26 -107
- package/dist-client/modeller/figure-links.js.map +1 -1
- package/dist-client/modeller/figure-size-editor.d.ts +27 -0
- package/dist-client/modeller/figure-size-editor.js +221 -0
- package/dist-client/modeller/figure-size-editor.js.map +1 -0
- package/dist-client/modeller/joint-drag.d.ts +1 -1
- package/dist-client/modeller/joint-drag.js +27 -22
- package/dist-client/modeller/joint-drag.js.map +1 -1
- package/dist-client/modeller/joint-fan.js +4 -3
- package/dist-client/modeller/joint-fan.js.map +1 -1
- package/dist-client/modeller/links.d.ts +9 -6
- package/dist-client/modeller/links.js +33 -21
- package/dist-client/modeller/links.js.map +1 -1
- package/dist-client/modeller/motion-ends.d.ts +4 -1
- package/dist-client/modeller/motion-ends.js +10 -6
- package/dist-client/modeller/motion-ends.js.map +1 -1
- package/dist-client/modeller/size-response.d.ts +29 -0
- package/dist-client/modeller/size-response.js +99 -0
- package/dist-client/modeller/size-response.js.map +1 -0
- package/dist-client/route.d.ts +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/docs/evidence/2026-10-24/current-size-panel.png +0 -0
- package/docs/evidence/2026-10-24/integrated-size.png +0 -0
- package/docs/evidence/2026-10-24/multipart-response.png +0 -0
- package/docs/evidence/2026-10-24/repeat-layout.png +0 -0
- package/docs/evidence/2026-10-24/repeat-shared-style.png +0 -0
- package/docs/evidence/2026-10-24/size-in-layout.png +0 -0
- package/docs/evidence/2026-10-24/size-shared-style.png +0 -0
- package/docs/evidence/2026-10-24/width-preview.png +0 -0
- package/docs/multipart-response-review-2026-10-24.md +77 -0
- package/docs/response-authoring-2026-10-24.md +46 -0
- package/docs/size-and-repeat-ux-2026-10-24.md +67 -0
- package/package.json +8 -8
- package/test/attachment-size-contract.test.ts +47 -0
- package/test/browser/acceptance-page.js +1 -1
- package/test/browser/detail-page.js +1 -1
- package/test/browser/detail-tabs-page.js +1 -1
- package/test/browser/editing-page.js +1 -1
- package/test/browser/editing.check.mjs +28 -14
- package/test/browser/links.check.mjs +41 -22
- package/test/browser/modeller-page.js +1 -1
- package/test/browser/modeller-revision.check.mjs +21 -7
- package/test/browser/modeller.check.mjs +51 -39
- package/test/browser/multipart-response-page.js +8 -0
- package/test/browser/parts.check.mjs +2 -1
- package/test/browser/product-page.js +1 -1
- package/test/browser/props.check.mjs +17 -14
- package/test/browser/response-page.js +12 -0
- package/test/browser/row-preview.check.mjs +3 -3
- package/test/browser/screen.check.mjs +57 -13
- package/test/browser/serve-response.mjs +24 -0
- package/test/clip-ai-proposal.test.ts +1 -1
- package/test/clip-consent.test.ts +2 -2
- package/test/command-preview.test.ts +7 -7
- package/test/harness-serve.test.ts +1 -1
- package/test/heading-scale-guard.test.ts +3 -3
- package/test/joint-drag.test.ts +6 -6
- package/test/joint-frames.test.ts +33 -32
- package/test/joints.ts +1 -6
- package/test/links.test.ts +21 -20
- package/test/multipart-response-fixture.js +17 -0
- package/test/multipart-response.test.ts +37 -0
- package/test/refusals.test.ts +2 -2
- package/test/resize-with-placement.test.ts +23 -1
- package/test/size-response.test.ts +49 -0
- package/test/size-source-pairing.test.ts +66 -0
- package/translations/en.json +44 -10
- package/translations/es.json +44 -10
- package/translations/ja.json +44 -10
- package/translations/ko.json +58 -24
- package/translations/ms.json +44 -10
- package/translations/zh.json +44 -10
|
@@ -72,13 +72,17 @@ async function mode(which) {
|
|
|
72
72
|
describe('work counted on the robot arm', () => {
|
|
73
73
|
before(() => mount('arm'))
|
|
74
74
|
|
|
75
|
-
/*
|
|
76
|
-
|
|
75
|
+
/*
|
|
76
|
+
The tabs are drawn out of sight with the rest: the selected part's motion is evaluated at its two ends, and the size
|
|
77
|
+
editor on 「배치」 asks once which of the parent's axes each of the part's own lies along, the figure at rest
|
|
78
|
+
(figureAxisPairsOf, ADR-0159 3) -- one evaluation for all three rows.
|
|
79
|
+
*/
|
|
80
|
+
test('picking a part on the canvas compiles nothing; it evaluates the two ends of the part\'s motion and its axes at rest', async () => {
|
|
77
81
|
const part = 'upper-arm'
|
|
78
82
|
const point = await read(id => probe.partOnPage(id), part)
|
|
79
83
|
const { before, spent } = await counted(() => click(point))
|
|
80
84
|
assert.equal((await read(() => probe.state())).selected, part)
|
|
81
|
-
assert.deepEqual(spent, { compiles: 0, evaluations:
|
|
85
|
+
assert.deepEqual(spent, { compiles: 0, evaluations: 3 })
|
|
82
86
|
/*
|
|
83
87
|
Once the screen rests, two commands are seen on copies (ADR-0097 §5.5): taking the part off (its step without the
|
|
84
88
|
joint, and its result) and the fastening the form opens on (its result). Each compiles only what it makes; the
|
|
@@ -89,11 +93,18 @@ describe('work counted on the robot arm', () => {
|
|
|
89
93
|
assert.ok(rested.compiles <= 3, JSON.stringify(rested))
|
|
90
94
|
})
|
|
91
95
|
|
|
92
|
-
/*
|
|
96
|
+
/*
|
|
97
|
+
The edit and its read are the kernel's two compiles (its version's asset is the edit's own); then the part's motion
|
|
98
|
+
ends, and its axes paired at rest for the size editor once more on the new version.
|
|
99
|
+
*/
|
|
93
100
|
test('a value typed into its field: the edit and its read, and nothing else while it is drawn', async () => {
|
|
101
|
+
/* The part's measures are the 「배치」 tab's size editor (2026-10-24). */
|
|
102
|
+
await click(await read(() => probe.tab('figure.label.tab-arrangement')))
|
|
103
|
+
await settled()
|
|
94
104
|
const field = await read(() => {
|
|
95
105
|
const deep = (root = document) => [...root.querySelectorAll('input[type=number]')].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
|
|
96
|
-
|
|
106
|
+
/* A measure of the part in the size editor. */
|
|
107
|
+
const input = deep().find(i => !i.disabled && / figure\.label\.size-measure$/.test(i.getAttribute('aria-label') ?? ''))
|
|
97
108
|
if (!input) return null
|
|
98
109
|
input.scrollIntoView({ block: 'center' })
|
|
99
110
|
const r = input.getBoundingClientRect()
|
|
@@ -105,7 +116,7 @@ describe('work counted on the robot arm', () => {
|
|
|
105
116
|
await page().keyboard.type(String(field.value + 10))
|
|
106
117
|
const { spent } = await counted(() => page().keyboard.press('Enter'))
|
|
107
118
|
assert.equal((await read(() => probe.state())).revision, revision + 1, `${field.label} is one step`)
|
|
108
|
-
assert.ok(spent.compiles <= 2 && spent.evaluations <=
|
|
119
|
+
assert.ok(spent.compiles <= 2 && spent.evaluations <= 6, JSON.stringify(spent))
|
|
109
120
|
})
|
|
110
121
|
})
|
|
111
122
|
|
|
@@ -178,7 +189,10 @@ describe('the edit canvas and the preview stay standing', () => {
|
|
|
178
189
|
test('a value changed beside the preview is drawn inside the scene that stands', async () => {
|
|
179
190
|
await click(await read(() => probe.row('box')))
|
|
180
191
|
await sleep(300)
|
|
181
|
-
|
|
192
|
+
/* The box's size is the 「배치」 tab's (2026-10-24). */
|
|
193
|
+
await click(await read(() => probe.tab('figure.label.tab-arrangement')))
|
|
194
|
+
await sleep(300)
|
|
195
|
+
const field = await read(() => probe.labelled('box Y figure.label.size-measure'))
|
|
182
196
|
assert.ok(field, 'the height field')
|
|
183
197
|
await page().mouse.click(field.x, field.y, { clickCount: 3 })
|
|
184
198
|
await page().keyboard.type('600')
|
|
@@ -53,8 +53,11 @@ async function click(point) {
|
|
|
53
53
|
await page().mouse.click(point.x, point.y)
|
|
54
54
|
await sleep(400)
|
|
55
55
|
}
|
|
56
|
-
/*
|
|
57
|
-
|
|
56
|
+
/*
|
|
57
|
+
A side tab pressed, by its short name; how a part answers a size is the 「반응형」 tab's (user 2026-10-17), named
|
|
58
|
+
「배치」 (figure.label.tab-arrangement) while a part is chosen.
|
|
59
|
+
*/
|
|
60
|
+
const tab = async name => click(await read(n => probe.tab(`figure.label.tab-short-${n}`) ?? (n === 'response' ? probe.tab('figure.label.tab-arrangement') : null), name))
|
|
58
61
|
|
|
59
62
|
const KEYS = { translate: 'KeyW', rotate: 'KeyE', scale: 'KeyR' }
|
|
60
63
|
|
|
@@ -495,37 +498,33 @@ describe('a figure made in the modeller is given its size from its parts (user 2
|
|
|
495
498
|
assert.deepEqual(await (await field()).evaluate(i => [i.value, i.placeholder]), ['', '∞'], 'empty, and nothing limits it')
|
|
496
499
|
})
|
|
497
500
|
|
|
501
|
+
/*
|
|
502
|
+
The box's size editor on 「반응형」 (ADR-0159 3): its width made to follow -- on nothing, the figure's own width --
|
|
503
|
+
then kept a difference from it, and the width typed.
|
|
504
|
+
*/
|
|
498
505
|
test('a part follows the figure\'s width less a difference: chosen and typed, the width is the size less it', async () => {
|
|
499
506
|
await click(await read(() => probe.partOnPage('box')))
|
|
500
507
|
assert.equal((await state()).selected, 'box')
|
|
501
|
-
const handle =
|
|
502
|
-
page().evaluateHandle(
|
|
503
|
-
(
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
})
|
|
520
|
-
|
|
521
|
-
assert.ok(await width.evaluate(s => !!s), 'the width can follow the figure size')
|
|
522
|
-
await width.select('instance:x')
|
|
523
|
-
await sleep(600)
|
|
524
|
-
const plus = await handle('input')
|
|
525
|
-
assert.ok(await plus.evaluate(i => !!i), 'a following width offers its difference')
|
|
526
|
-
await plus.evaluate(i => i.scrollIntoView({ block: 'center' }))
|
|
527
|
-
await plus.click({ clickCount: 3 })
|
|
528
|
-
await page().keyboard.type('-50')
|
|
508
|
+
const handle = selector =>
|
|
509
|
+
page().evaluateHandle(selector => {
|
|
510
|
+
const deep = (root = document) => [...root.querySelectorAll(selector)].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
|
|
511
|
+
return deep()[0] ?? null
|
|
512
|
+
}, selector)
|
|
513
|
+
const press = async (selector, what) => {
|
|
514
|
+
const element = await handle(selector)
|
|
515
|
+
assert.ok(await element.evaluate(e => !!e), what)
|
|
516
|
+
await click(await element.evaluate(e => {
|
|
517
|
+
e.scrollIntoView({ block: 'center' })
|
|
518
|
+
const r = e.getBoundingClientRect()
|
|
519
|
+
return { x: r.left + r.width / 2, y: r.top + r.height / 2 }
|
|
520
|
+
}))
|
|
521
|
+
}
|
|
522
|
+
await press('input[aria-label="box X figure.label.size-follows"]', 'the width can follow')
|
|
523
|
+
await press('div.dimension[data-axis=x] div.modes label:nth-of-type(2) input[type=radio]', 'a following width offers its difference')
|
|
524
|
+
const width = await handle('input[aria-label="box X figure.label.size-measure"]')
|
|
525
|
+
await width.evaluate(i => i.scrollIntoView({ block: 'center' }))
|
|
526
|
+
await width.click({ clickCount: 3 })
|
|
527
|
+
await page().keyboard.type('350')
|
|
529
528
|
await page().keyboard.press('Tab')
|
|
530
529
|
await sleep(600)
|
|
531
530
|
/*
|
|
@@ -534,6 +533,11 @@ describe('a figure made in the modeller is given its size from its parts (user 2
|
|
|
534
533
|
*/
|
|
535
534
|
assert.equal((await state()).error, '', 'taken')
|
|
536
535
|
assert.equal((await body('box')).dimensions['size.x'], 350, 'the figure\'s 400 less 50')
|
|
536
|
+
const said = await read(() => {
|
|
537
|
+
const deep = (root = document) => [...root.querySelectorAll('div.dimension[data-axis=x] p.result')].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
|
|
538
|
+
return deep().map(p => p.textContent.trim())
|
|
539
|
+
})
|
|
540
|
+
assert.deepEqual(said, ['figure.text.size-difference-less'], 'said as the size less a difference')
|
|
537
541
|
const asked = await read(() => {
|
|
538
542
|
const deep = (root = document) => [...root.querySelectorAll('ox-prompt')].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
|
|
539
543
|
return deep().length
|
|
@@ -622,7 +626,8 @@ describe('a motion is changed where it is (2026-09-25)', () => {
|
|
|
622
626
|
assert.equal(after.error, '', 'not refused')
|
|
623
627
|
assert.equal(after.revision, start.revision + 1, 'one step')
|
|
624
628
|
const now = await jointOf('upper-arm')
|
|
625
|
-
|
|
629
|
+
/* A joint is a list of axes (ADR-0159): the form changes its first. */
|
|
630
|
+
assert.equal(now.motion[0].upperLimit, 45, 'the new limit')
|
|
626
631
|
assert.equal(now.id, was.id, 'the same joint')
|
|
627
632
|
})
|
|
628
633
|
})
|
|
@@ -660,16 +665,18 @@ describe('the modeller made easier to find and to keep (2026-09-26 baseline)', (
|
|
|
660
665
|
assert.equal(await resize.evaluate(b => b.getAttribute('aria-pressed')), 'false')
|
|
661
666
|
})
|
|
662
667
|
|
|
668
|
+
/* The part's size is the 「반응형」 tab's size editor (2026-10-24), and what a size typed keeps is said under it. */
|
|
663
669
|
test('a height typed into its field keeps the part\'s middle, and the size group says so (ADR-0140 4.1)', async () => {
|
|
664
670
|
await mount('box')
|
|
665
671
|
await click(await read(() => probe.partOnPage('box')))
|
|
672
|
+
await tab('response')
|
|
666
673
|
const was = await body('box')
|
|
667
674
|
const middleOf = b => b.t[1] + b.dimensions.start + b.dimensions.length / 2
|
|
668
675
|
assert.ok(await read(() => {
|
|
669
676
|
const deep = (root = document) => [...root.querySelectorAll('p[size-keeps]')].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
|
|
670
677
|
return deep().some(p => p.textContent.trim() === 'figure.text.size-keeps-centre')
|
|
671
678
|
}), 'said beside the size fields')
|
|
672
|
-
const height = await find('input', 'box.size
|
|
679
|
+
const height = await find('input', 'box Y figure.label.size-measure')
|
|
673
680
|
assert.ok(await height.evaluate(i => !!i), 'the height field')
|
|
674
681
|
const at = await centre(height)
|
|
675
682
|
await page().mouse.click(at.x, at.y, { clickCount: 3 })
|
|
@@ -744,20 +751,24 @@ describe('the modeller made easier to find and to keep (2026-09-26 baseline)', (
|
|
|
744
751
|
})
|
|
745
752
|
})
|
|
746
753
|
|
|
754
|
+
/*
|
|
755
|
+
The one-click follow-size button is gone (2026-10-24): a side is made to follow by its own 'follows' in the size editor.
|
|
756
|
+
On a figure with no size, the first one declares the figure's size from its parts in the same step.
|
|
757
|
+
*/
|
|
747
758
|
describe('a part follows the figure size in one step (follow-size, 2026-09-26)', () => {
|
|
748
|
-
test('
|
|
759
|
+
test('a side made to follow on a figure with no size: its size is declared from the part, said in a line, and the part follows it', async () => {
|
|
749
760
|
await mount('box')
|
|
750
761
|
await click(await read(() => probe.row('box')))
|
|
751
762
|
await tab('response')
|
|
752
763
|
const at = await page().evaluate(() => {
|
|
753
|
-
const deep = (root = document) => [...root.querySelectorAll('
|
|
764
|
+
const deep = (root = document) => [...root.querySelectorAll('input[aria-label="box X figure.label.size-follows"]')].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
|
|
754
765
|
const b = deep()[0]
|
|
755
|
-
if (!b) return null
|
|
766
|
+
if (!b || b.checked) return null
|
|
756
767
|
b.scrollIntoView({ block: 'center' })
|
|
757
768
|
const r = b.getBoundingClientRect()
|
|
758
769
|
return { x: r.left + r.width / 2, y: r.top + r.height / 2 }
|
|
759
770
|
})
|
|
760
|
-
assert.ok(at, 'the
|
|
771
|
+
assert.ok(at, 'the width of a box that follows nothing can be made to follow')
|
|
761
772
|
await page().evaluate(() => {
|
|
762
773
|
window.__told = []
|
|
763
774
|
document.addEventListener('notify', e => window.__told.push(e.detail))
|
|
@@ -892,7 +903,7 @@ describe('a row\'s pitch made a board\'s to set (ADR-0096)', () => {
|
|
|
892
903
|
assert.deepEqual([input.min, input.max, source.designInputs['roller.pitch']], [100, 400, 200])
|
|
893
904
|
assert.deepEqual(source.joints.find(j => j.body1.part === 'roller').repeat.pitch, { from: { of: 'input', input: 'roller.pitch' } })
|
|
894
905
|
assert.equal((await read(() => probe.ids())).filter(id => id.includes('"roller.placed"')).length, before, 'the row as it was')
|
|
895
|
-
await typeInto('
|
|
906
|
+
await typeInto('figure.label.repeat-gap', '400')
|
|
896
907
|
source = await read(() => probe.source())
|
|
897
908
|
assert.equal(source.designInputs['roller.pitch'], 400, 'the pitch field sets the input')
|
|
898
909
|
assert.ok((await read(() => probe.ids())).filter(id => id.includes('"roller.placed"')).length < before, 'fewer rollers at a wider pitch')
|
|
@@ -919,10 +930,11 @@ describe('a row that carries a part (ADR-0093, 2026-09-26)', () => {
|
|
|
919
930
|
assert.ok(copy, 'a mark on the fourth roller is drawn')
|
|
920
931
|
await click(await read(id => probe.partOnPage(id), copy))
|
|
921
932
|
assert.equal((await state()).selected, 'mark', 'picked by the part it copies')
|
|
922
|
-
|
|
933
|
+
/* The row's pitch field is the selected part's own: named by what it is, the centre spacing. */
|
|
934
|
+
assert.equal(await read(() => probe.hasInput('figure.label.repeat-gap')), false, 'the mark has no row of its own to set')
|
|
923
935
|
await click(await read(() => probe.row('roller')))
|
|
924
936
|
assert.equal((await state()).selected, 'roller')
|
|
925
|
-
assert.ok(await read(() => probe.hasInput('
|
|
937
|
+
assert.ok(await read(() => probe.hasInput('figure.label.repeat-gap')), 'the roller offers its row')
|
|
926
938
|
/* Walk 1 (2026-09-26): the fastening form opens on the roller's own fastening -- by its centre, the drawing's way (2026-10-01). */
|
|
927
939
|
await click(await read(() => probe.button('change-fastening')))
|
|
928
940
|
assert.equal(await read(() => probe.fastenMode()), 'centre', 'the way it is fastened reads centre')
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import './modeller-page.js'
|
|
2
|
+
import { i18next } from '@operato/i18n'
|
|
3
|
+
import { serializeFigureAsset } from '@hatiolab/figure-model'
|
|
4
|
+
import { multipartResponseFixture } from '../multipart-response-fixture.js'
|
|
5
|
+
const ko = await fetch('/ko.json').then(r => r.json())
|
|
6
|
+
i18next.addResourceBundle('ko', 'translations', ko, true, true)
|
|
7
|
+
await i18next.changeLanguage('ko')
|
|
8
|
+
await window.harness.mount('box', serializeFigureAsset(multipartResponseFixture()))
|
|
@@ -76,8 +76,9 @@ async function press(...keys) {
|
|
|
76
76
|
for (const key of keys.slice(0, -1).reverse()) await page().keyboard.up(key)
|
|
77
77
|
await sleep(400)
|
|
78
78
|
}
|
|
79
|
+
/* A side tab by its short name; with a part chosen, 「반응형」 is named 「배치」 (figure.label.tab-arrangement). */
|
|
79
80
|
async function tab(name) {
|
|
80
|
-
await click(await read(
|
|
81
|
+
await click(await read(n => probe.tab(`figure.label.tab-short-${n}`) ?? (n === 'response' ? probe.tab('figure.label.tab-arrangement') : null), name), `the ${name} tab`)
|
|
81
82
|
}
|
|
82
83
|
|
|
83
84
|
describe('the parts list: each button does what it says, one step of the session each', () => {
|
|
@@ -46,7 +46,7 @@ export function doorSource() {
|
|
|
46
46
|
a = addFigureBox(a, { id: 'leaf', size: { x: 900, y: 1800, z: 40 }, position: { x: 0, y: 1000, z: 0 } })
|
|
47
47
|
a = applyFigureAuthoring(a, { kind: 'attach', part: 'leaf', to: { part: 'frame', face: 'top' } })
|
|
48
48
|
const j = a.joints.find(k => k.body1.part === 'leaf')
|
|
49
|
-
a = applyFigureAuthoring(a, { kind: 'set-joint', joint: { ...j, type: 'revolute', axis: 'Y', state: { id: 'open' } } })
|
|
49
|
+
a = applyFigureAuthoring(a, { kind: 'set-joint', joint: { ...j, motion: [{ type: 'revolute', axis: 'Y', state: { id: 'open' } }] } })
|
|
50
50
|
a = editFigurePart(a, { kind: 'set-driver', state: 'open', clip: 'swing', duration: 2, keys: [{ seconds: 0, value: 0 }, { seconds: 2, value: 80 }] })
|
|
51
51
|
return serializeFigureAsset(a)
|
|
52
52
|
}
|
|
@@ -70,8 +70,9 @@ async function press(selector, what) {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
/**
|
|
73
|
-
* The groups of the panel shown, top to bottom, by what each is: the part's name, the
|
|
74
|
-
*
|
|
73
|
+
* The groups of the panel shown, top to bottom, by what each is: the part's name, the place the links draw -- and its
|
|
74
|
+
* shape's own measures, for a shape that has any -- the look and 「더 보기」 on the properties tab; on 「반응형」 its size,
|
|
75
|
+
* a position that follows, the row.
|
|
75
76
|
*/
|
|
76
77
|
const groupsInOrder = () =>
|
|
77
78
|
read(() => {
|
|
@@ -80,7 +81,7 @@ const groupsInOrder = () =>
|
|
|
80
81
|
const links = panel.querySelector('figure-links')?.shadowRoot
|
|
81
82
|
const marks = {
|
|
82
83
|
name: panel.querySelector('input[name-field]'),
|
|
83
|
-
size: links?.querySelector('section[size], section[size-response]'),
|
|
84
|
+
size: links?.querySelector('section[size], section[size-response]') ?? panel.querySelector('section[slot=size]'),
|
|
84
85
|
place: links?.querySelector('section[fastening]'),
|
|
85
86
|
position: links?.querySelector('section[position-response]'),
|
|
86
87
|
row: panel.querySelector('section[slot=row]') ?? links?.querySelector('section[row]'),
|
|
@@ -93,9 +94,10 @@ const groupsInOrder = () =>
|
|
|
93
94
|
.map(([name]) => name)
|
|
94
95
|
})
|
|
95
96
|
|
|
96
|
-
/* A side tab pressed, by its name. */
|
|
97
|
+
/* A side tab pressed, by its name; with a part chosen, 「반응형」 is named 「배치」 (figure.label.tab-arrangement). */
|
|
97
98
|
async function tab(name) {
|
|
98
|
-
|
|
99
|
+
const named = `aside button[tab][aria-label="figure.label.tab-short-${name}"]`
|
|
100
|
+
await press(name === 'response' ? `${named}, aside button[tab][aria-label="figure.label.tab-arrangement"]` : named, `the ${name} tab`)
|
|
99
101
|
await read(() => document.querySelector('figure-editor').updateComplete)
|
|
100
102
|
}
|
|
101
103
|
|
|
@@ -150,10 +152,11 @@ describe('the side: six tabs, the view and the releases from the bar', () => {
|
|
|
150
152
|
describe('a part\'s groups in one order, whatever its state', () => {
|
|
151
153
|
before(() => mount('rollers'))
|
|
152
154
|
|
|
153
|
-
|
|
155
|
+
/* A box's size is the 「배치」 tab's (2026-10-24): it has no measures of its own shape left on the properties. */
|
|
156
|
+
test('a part fastened in a row: name, place, look, more; on 「반응형」 its size and its row', async () => {
|
|
154
157
|
await pick('roller')
|
|
155
158
|
await tab('properties')
|
|
156
|
-
assert.deepEqual(await groupsInOrder(), ['name', '
|
|
159
|
+
assert.deepEqual(await groupsInOrder(), ['name', 'place', 'look', 'more'])
|
|
157
160
|
await tab('response')
|
|
158
161
|
assert.deepEqual(await groupsInOrder(), ['size', 'row'])
|
|
159
162
|
await tab('properties')
|
|
@@ -161,10 +164,10 @@ describe('a part\'s groups in one order, whatever its state', () => {
|
|
|
161
164
|
|
|
162
165
|
test('a part on the plane, not in a row: the same order, the row folded where a row would be', async () => {
|
|
163
166
|
await pick('bed')
|
|
164
|
-
assert.deepEqual(await groupsInOrder(), ['name', '
|
|
165
|
-
/* On the plane, its
|
|
167
|
+
assert.deepEqual(await groupsInOrder(), ['name', 'place', 'look', 'more'])
|
|
168
|
+
/* On the plane, its place is its fastening's: no position worked out from the figure size is offered (2026-10-24). */
|
|
166
169
|
await tab('response')
|
|
167
|
-
assert.deepEqual(await groupsInOrder(), ['size', '
|
|
170
|
+
assert.deepEqual(await groupsInOrder(), ['size', 'row'])
|
|
168
171
|
await tab('properties')
|
|
169
172
|
})
|
|
170
173
|
|
|
@@ -221,14 +224,14 @@ describe('a group\'s 「⋯」: its rarer actions, by the mouse or the keyboard'
|
|
|
221
224
|
assert.equal(await read(() => JSON.parse(document.querySelector('figure-editor').session.source()).joints.some(j => j.body1.part === 'bed')), false, 'taken off')
|
|
222
225
|
})
|
|
223
226
|
|
|
227
|
+
/* The row's 「⋯」 is gone: taking a row off is a button of its own (2026-10-24). The roller's place group has one. */
|
|
224
228
|
test('Esc closes the menu and nothing is done', async () => {
|
|
225
229
|
await pick('roller')
|
|
226
|
-
|
|
227
|
-
await tab('response')
|
|
230
|
+
await tab('properties')
|
|
228
231
|
const start = await revision()
|
|
229
|
-
await press('section[
|
|
232
|
+
await press('section[fastening] button[group-menu]', 'the place group\'s 「⋯」')
|
|
230
233
|
await until(() => document.activeElement?.localName === 'ox-popup-menu', [], 'the menu open, with the focus')
|
|
231
|
-
assert.deepEqual(await read(() => [...document.querySelectorAll('ox-popup-menu[active] ox-popup-menuitem')].map(i => i.getAttribute('label'))), ['figure.button.
|
|
234
|
+
assert.deepEqual(await read(() => [...document.querySelectorAll('ox-popup-menu[active] ox-popup-menuitem')].map(i => i.getAttribute('label'))), ['figure.button.detach'])
|
|
232
235
|
await page().keyboard.press('Escape')
|
|
233
236
|
await until(() => !document.querySelector('ox-popup-menu[active]'), [], 'the menu closed')
|
|
234
237
|
await sleep(200)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/* Manual CUA check of the real editor, using an in-memory store and a fresh fixed-size part. */
|
|
2
|
+
import './modeller-page.js'
|
|
3
|
+
import { i18next } from '@operato/i18n'
|
|
4
|
+
import { addFigureBox, createFigureAsset, serializeFigureAsset } from '@hatiolab/figure-model'
|
|
5
|
+
|
|
6
|
+
const ko = await fetch('/ko.json').then(r => r.json())
|
|
7
|
+
i18next.addResourceBundle('ko', 'translations', ko, true, true)
|
|
8
|
+
await i18next.changeLanguage('ko')
|
|
9
|
+
const asset = addFigureBox(createFigureAsset('반응형 치수 검증'), {
|
|
10
|
+
id: 'frame', size: { x: 1500, y: 80, z: 600 }, position: { x: 0, y: 50, z: 0 }
|
|
11
|
+
})
|
|
12
|
+
await window.harness.mount('box', serializeFigureAsset(asset))
|
|
@@ -60,10 +60,10 @@ describe('a row\'s preview follows its basis', () => {
|
|
|
60
60
|
const at = await read(() => probe.row('bin'))
|
|
61
61
|
await page().mouse.click(at.x, at.y)
|
|
62
62
|
await until(() => probe.state().selected === 'bin', [], 'bin selected')
|
|
63
|
-
/* The row is the 「반응형」 tab's (user 2026-10-17). */
|
|
64
|
-
const tab = await read(() => probe.tab('figure.label.tab-
|
|
63
|
+
/* The row is the 「반응형」 tab's (user 2026-10-17), named 「배치」 while a part is chosen (figure.label.tab-arrangement). */
|
|
64
|
+
const tab = await read(() => probe.tab('figure.label.tab-arrangement'))
|
|
65
65
|
await page().mouse.click(tab.x, tab.y)
|
|
66
|
-
await until(() => document.querySelector('figure-editor').shadowRoot.querySelector('button[tab][on]')?.getAttribute('aria-label') === 'figure.label.tab-
|
|
66
|
+
await until(() => document.querySelector('figure-editor').shadowRoot.querySelector('button[tab][on]')?.getAttribute('aria-label') === 'figure.label.tab-arrangement', [], 'the 배치 tab on')
|
|
67
67
|
await click('details[row-fold] > summary', {}, 'the row\'s fold')
|
|
68
68
|
})
|
|
69
69
|
|
|
@@ -82,6 +82,8 @@ 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
|
+
/* 「반응형」 while a part is chosen: named by what it holds for the part, its size and arrangement (2026-10-24). */
|
|
86
|
+
const ARRANGEMENT = 'figure.label.tab-arrangement'
|
|
85
87
|
/*
|
|
86
88
|
The view settings or the releases, opened from the bar over the work (they were tabs once); left open if open. The
|
|
87
89
|
releases open from 「발행」, the bar's one way to them (user 2026-10-18).
|
|
@@ -226,7 +228,9 @@ describe('the frame: parts · canvas · one side of seven tabs, and the bar over
|
|
|
226
228
|
assert.equal(await saveOn(), false, 'saved: the save button is dimmed')
|
|
227
229
|
|
|
228
230
|
await pressRow('box')
|
|
229
|
-
|
|
231
|
+
/* A part's size is the 「배치」 tab's (2026-10-24). */
|
|
232
|
+
await pressTab(ARRANGEMENT)
|
|
233
|
+
const height = await element('the height field', () => screenCheck.deep('input').find(i => i.getAttribute('aria-label') === 'box Y figure.label.size-measure'))
|
|
230
234
|
await height.click({ clickCount: 3 })
|
|
231
235
|
await page().keyboard.type('600')
|
|
232
236
|
await page().keyboard.press('Enter')
|
|
@@ -240,6 +244,7 @@ describe('the frame: parts · canvas · one side of seven tabs, and the bar over
|
|
|
240
244
|
await until('redone', () => probe.value('box.size.y') === 600)
|
|
241
245
|
await press('save', () => screenCheck.editor().shadowRoot.querySelector('ox-canvas-bar button[save]'))
|
|
242
246
|
await until('saved: dimmed again', () => screenCheck.editor().shadowRoot.querySelector('ox-canvas-bar button[save]').disabled)
|
|
247
|
+
await pressTab(TAB('properties'))
|
|
243
248
|
|
|
244
249
|
const width = () => read(() => Math.round(screenCheck.deep('figure-canvas')[0].getBoundingClientRect().width))
|
|
245
250
|
const was = await width()
|
|
@@ -316,7 +321,7 @@ describe('the editor is drawn in the shell\'s tokens (2026-09-27)', () => {
|
|
|
316
321
|
describe('a part chosen: each tab in its place, the figure-wide tabs give way, the list\'s root goes back', () => {
|
|
317
322
|
before(() => mount('arm'))
|
|
318
323
|
|
|
319
|
-
test('properties hold the part\'s
|
|
324
|
+
test('properties hold the part\'s place; 「반응형」 its size and how it answers one; capability its places; motion its motion and controls -- each under the part\'s head', async () => {
|
|
320
325
|
await pressRow('upper-arm')
|
|
321
326
|
await until('upper-arm chosen', () => screenCheck.editor().selected === 'upper-arm')
|
|
322
327
|
const holds = () =>
|
|
@@ -334,10 +339,11 @@ describe('a part chosen: each tab in its place, the figure-wide tabs give way, t
|
|
|
334
339
|
await pressTab(TAB('properties'))
|
|
335
340
|
let now = await holds()
|
|
336
341
|
assert.ok(now.subject, 'the part\'s head')
|
|
337
|
-
|
|
342
|
+
/* A box's size is the 「배치」 tab's, in its size editor (2026-10-24): the properties keep its place. */
|
|
343
|
+
assert.ok(now.links.includes('figure.label.group-place') && !now.links.includes('figure.label.group-size'), `the shape's links: ${now.links}`)
|
|
338
344
|
assert.ok(now.material, 'its material')
|
|
339
|
-
/* How it answers a size has a tab of its own (user 2026-10-17, 「반응형」). */
|
|
340
|
-
await pressTab(
|
|
345
|
+
/* How it answers a size has a tab of its own (user 2026-10-17, 「반응형」), named 「배치」 while a part is chosen. */
|
|
346
|
+
await pressTab(ARRANGEMENT)
|
|
341
347
|
now = await holds()
|
|
342
348
|
assert.ok(now.subject, 'the part\'s head')
|
|
343
349
|
assert.ok(now.links.includes('figure.label.group-size'), `the response's links: ${now.links}`)
|
|
@@ -726,6 +732,8 @@ describe('the camera follows a size typed far from the view (first 15 minutes, 2
|
|
|
726
732
|
test('a width typed ten times larger fits the view again; a little wider leaves the camera where it is', async () => {
|
|
727
733
|
await mount('box')
|
|
728
734
|
await pressRow('box')
|
|
735
|
+
/* A part's size is the 「배치」 tab's (2026-10-24). */
|
|
736
|
+
await pressTab(ARRANGEMENT)
|
|
729
737
|
await read(() => {
|
|
730
738
|
const canvas = screenCheck.deep('figure-canvas')[0]
|
|
731
739
|
const fit = canvas.fit.bind(canvas)
|
|
@@ -733,7 +741,7 @@ describe('the camera follows a size typed far from the view (first 15 minutes, 2
|
|
|
733
741
|
canvas.fit = () => (window.fits++, fit())
|
|
734
742
|
})
|
|
735
743
|
const typeWidth = async mm => {
|
|
736
|
-
const width = await element('the width field', () => screenCheck.deep('input').find(i => i.getAttribute('aria-label') === 'box.size
|
|
744
|
+
const width = await element('the width field', () => screenCheck.deep('input').find(i => i.getAttribute('aria-label') === 'box X figure.label.size-measure'))
|
|
737
745
|
await width.click({ clickCount: 3 })
|
|
738
746
|
await page().keyboard.type(String(mm))
|
|
739
747
|
await page().keyboard.press('Enter')
|
|
@@ -745,6 +753,7 @@ describe('the camera follows a size typed far from the view (first 15 minutes, 2
|
|
|
745
753
|
assert.equal(await read(() => window.fits), 0, 'a little wider: the camera stays')
|
|
746
754
|
await typeWidth(4000)
|
|
747
755
|
await until('the view fitted to it', () => window.fits >= 1)
|
|
756
|
+
await pressTab(TAB('properties'))
|
|
748
757
|
await boardsUp()
|
|
749
758
|
})
|
|
750
759
|
})
|
|
@@ -796,16 +805,25 @@ describe('a part\'s size row: a rule\'s numbers only where a rule sets every sid
|
|
|
796
805
|
assert.equal(await sizeRow(), null)
|
|
797
806
|
})
|
|
798
807
|
|
|
808
|
+
/*
|
|
809
|
+
A box's size is the 「배치」 tab's size editor (2026-10-24), one row a side: made to follow on every side, each row
|
|
810
|
+
shows the size the rule gives it, and the properties draw no size row.
|
|
811
|
+
*/
|
|
799
812
|
test('a box made to follow the figure\'s size on every side shows the size the rule gives it', async () => {
|
|
800
813
|
await mount('box')
|
|
801
814
|
await pressRow('box')
|
|
802
815
|
await until('box chosen', () => screenCheck.editor().selected === 'box')
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
816
|
+
await pressTab(ARRANGEMENT)
|
|
817
|
+
for (const axis of ['X', 'Y', 'Z']) {
|
|
818
|
+
const before = await revision()
|
|
819
|
+
await press(`${axis} made to follow`, axis => screenCheck.deep('input').find(i => i.getAttribute('aria-label') === `box ${axis} figure.label.size-follows`), axis)
|
|
820
|
+
await until(`${axis} following`, before => screenCheck.editor().session.read().revision > before, before)
|
|
821
|
+
}
|
|
806
822
|
await until('following', () => ['x', 'y', 'z'].every(axis => !screenCheck.editor().session.read().fields.some(f => f.id === `box.size.${axis}`)))
|
|
823
|
+
const measures = await read(() => ['X', 'Y', 'Z'].map(axis => screenCheck.deep('input').find(i => i.getAttribute('aria-label') === `box ${axis} figure.label.size-measure`)?.value))
|
|
824
|
+
assert.deepEqual(measures, ['400', '400', '400'], 'each side the size the rule gives it')
|
|
807
825
|
await pressTab(TAB('properties'))
|
|
808
|
-
assert.
|
|
826
|
+
assert.equal(await sizeRow(), null, 'no size row on the properties')
|
|
809
827
|
})
|
|
810
828
|
})
|
|
811
829
|
|
|
@@ -829,7 +847,20 @@ describe('the side panel is found as it was left, per part', () => {
|
|
|
829
847
|
}
|
|
830
848
|
before(() => mount('arm'))
|
|
831
849
|
|
|
850
|
+
/*
|
|
851
|
+
The part's properties fit a tall window since its size moved to 「배치」 (2026-10-24): a shorter one gives the panel
|
|
852
|
+
something to scroll.
|
|
853
|
+
*/
|
|
832
854
|
test('scrolled and a fold opened on one part; another part opens at its top; back, the first is as it was left', async () => {
|
|
855
|
+
const viewport = page().viewport()
|
|
856
|
+
await page().setViewport({ ...viewport, height: 600 })
|
|
857
|
+
try {
|
|
858
|
+
await scrolledAndBack()
|
|
859
|
+
} finally {
|
|
860
|
+
await page().setViewport(viewport)
|
|
861
|
+
}
|
|
862
|
+
})
|
|
863
|
+
async function scrolledAndBack() {
|
|
833
864
|
await pressRow('upper-arm')
|
|
834
865
|
await until('upper-arm chosen', () => screenCheck.editor().selected === 'upper-arm')
|
|
835
866
|
/* 「더 보기」 starts open on this page: folded first, so the fold opened below is one this part leaves open. */
|
|
@@ -854,7 +885,7 @@ describe('the side panel is found as it was left, per part', () => {
|
|
|
854
885
|
await pressRow('upper-arm')
|
|
855
886
|
await until('upper-arm as it was left', top => screenCheck.editor().selected === 'upper-arm' && Math.abs(screenCheck.shownPanel().scrollTop - top) <= 2, top)
|
|
856
887
|
assert.deepEqual(await folds(), opened, 'its folds as they were')
|
|
857
|
-
}
|
|
888
|
+
}
|
|
858
889
|
})
|
|
859
890
|
|
|
860
891
|
describe('the fastening window says whether it changes the fastening the part has or fastens it anew', () => {
|
|
@@ -1075,12 +1106,15 @@ describe('the modeller page: the editor with the page\'s figure panel, verdict a
|
|
|
1075
1106
|
|
|
1076
1107
|
test('an edit is judged as it stands on screen: the source the server is sent is the one edited', async () => {
|
|
1077
1108
|
await pressRow('left')
|
|
1078
|
-
|
|
1109
|
+
/* A part's size is the 「배치」 tab's (2026-10-24). */
|
|
1110
|
+
await pressTab(ARRANGEMENT)
|
|
1111
|
+
const height = await element('the height field', () => screenCheck.deep('input').find(i => i.getAttribute('aria-label') === 'left Y figure.label.size-measure'))
|
|
1079
1112
|
await height.click({ clickCount: 3 })
|
|
1080
1113
|
await page().keyboard.type('600')
|
|
1081
1114
|
await page().keyboard.press('Enter')
|
|
1082
1115
|
await until('the edit made', () => screenCheck.editor().session.read().fields.find(f => f.id === 'left.size.y')?.value === 600)
|
|
1083
1116
|
await until('the edited source judged', () => product.calls().filter(c => c.field === 'inspectFigureDraft').at(-1)?.variables.source === screenCheck.editor().session.revisionNow().text)
|
|
1117
|
+
await pressTab(TAB('properties'))
|
|
1084
1118
|
})
|
|
1085
1119
|
|
|
1086
1120
|
test('a save sends the card\'s picture with the figure', async () => {
|
|
@@ -1114,7 +1148,17 @@ describe('the modeller page: the editor with the page\'s figure panel, verdict a
|
|
|
1114
1148
|
}
|
|
1115
1149
|
})
|
|
1116
1150
|
|
|
1151
|
+
/* The part's properties fit a tall window since its size moved to 「배치」 (2026-10-24): a shorter one scrolls. */
|
|
1117
1152
|
test('another figure opened starts its panels clean: a part of the same name opens at its top', async () => {
|
|
1153
|
+
const viewport = page().viewport()
|
|
1154
|
+
await page().setViewport({ ...viewport, height: 600 })
|
|
1155
|
+
try {
|
|
1156
|
+
await anotherFigureClean()
|
|
1157
|
+
} finally {
|
|
1158
|
+
await page().setViewport(viewport)
|
|
1159
|
+
}
|
|
1160
|
+
})
|
|
1161
|
+
async function anotherFigureClean() {
|
|
1118
1162
|
await open('fig-p', 'arm-clip')
|
|
1119
1163
|
await pressRow('upper-arm')
|
|
1120
1164
|
await until('upper-arm chosen', () => screenCheck.editor().selected === 'upper-arm')
|
|
@@ -1136,7 +1180,7 @@ describe('the modeller page: the editor with the page\'s figure panel, verdict a
|
|
|
1136
1180
|
await until('upper-arm chosen', () => screenCheck.editor().selected === 'upper-arm')
|
|
1137
1181
|
for (let i = 0; i < 3; i++) await page().waitForFunction(() => new Promise(done => requestAnimationFrame(() => requestAnimationFrame(() => done(true)))))
|
|
1138
1182
|
assert.equal(await read(() => screenCheck.shownPanel().scrollTop), 0, 'not where the other figure\'s part was left')
|
|
1139
|
-
}
|
|
1183
|
+
}
|
|
1140
1184
|
|
|
1141
1185
|
test('a new figure\'s AI draft puts a request in the assistant\'s input, and sends nothing', async () => {
|
|
1142
1186
|
await read(() => assistant.forget())
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/* Manual browser verification. Uses buildPage's FIGURE_MODEL / THINGS_SCENE overrides when provided. */
|
|
2
|
+
import { buildPage } from './build.mjs'
|
|
3
|
+
import { createServer } from 'node:http'
|
|
4
|
+
import { readFileSync } from 'node:fs'
|
|
5
|
+
import { dirname, join } from 'node:path'
|
|
6
|
+
import { createRequire } from 'node:module'
|
|
7
|
+
const require = createRequire(import.meta.url)
|
|
8
|
+
const bundle = await buildPage(process.argv[2] ?? 'response-page.js')
|
|
9
|
+
const routes = new Map([
|
|
10
|
+
['/page.js', [bundle, 'application/javascript']],
|
|
11
|
+
['/ko.json', [new URL('../../translations/ko.json', import.meta.url), 'application/json']],
|
|
12
|
+
['/icons.woff2', [require.resolve('material-symbols/material-symbols-outlined.woff2'), 'font/woff2']],
|
|
13
|
+
['/workers/markdown-webworker.js', [join(dirname(bundle), 'workers/markdown-webworker.js'), 'application/javascript']]
|
|
14
|
+
])
|
|
15
|
+
const html = `<!doctype html><html lang="ko"><meta charset="utf-8"><title>반응형 치수 — 실제 편집기 로컬 검증</title><style>
|
|
16
|
+
@font-face{font-family:"Material Symbols Outlined";src:url(/icons.woff2)}
|
|
17
|
+
html,body{margin:0;height:100%;font-family:sans-serif}body{display:flex}figure-editor{flex:1;min-width:0;min-height:0}
|
|
18
|
+
</style><body><script type="module" src="/page.js"></script></body></html>`
|
|
19
|
+
createServer((req, res) => {
|
|
20
|
+
const file = routes.get(req.url)
|
|
21
|
+
if (!file && req.url !== '/') { res.writeHead(404); res.end(); return }
|
|
22
|
+
res.setHeader('Content-Type', file ? file[1] : 'text/html; charset=utf-8')
|
|
23
|
+
res.end(file ? readFileSync(file[0]) : html)
|
|
24
|
+
}).listen(Number(process.env.PORT ?? 4391), '127.0.0.1', function () { console.log(`Ready http://127.0.0.1:${this.address().port}/`) })
|
|
@@ -23,7 +23,7 @@ function door(): string {
|
|
|
23
23
|
a = addFigureBox(a, { id: 'sign', size: { x: 200, y: 100, z: 10 }, position: { x: 0, y: 2200, z: 200 } })
|
|
24
24
|
a = applyFigureAuthoring(a, { kind: 'attach', part: 'leaf', to: { part: 'frame', face: 'top' } })
|
|
25
25
|
const j = a.joints!.find(k => k.body1.part === 'leaf')!
|
|
26
|
-
a = applyFigureAuthoring(a, { kind: 'set-joint', joint: { ...j, type: 'revolute', axis: 'Y', state: { id: 'open' } } })
|
|
26
|
+
a = applyFigureAuthoring(a, { kind: 'set-joint', joint: { ...j, motion: [{ type: 'revolute', axis: 'Y', state: { id: 'open' } }] } })
|
|
27
27
|
a = editFigurePart(a, { kind: 'set-driver', state: 'open', clip: 'swing', duration: 2, keys: [{ seconds: 0, value: 0 }, { seconds: 2, value: 80 }] })
|
|
28
28
|
return serializeFigureAsset(a)
|
|
29
29
|
}
|
|
@@ -22,7 +22,7 @@ function doors(): string {
|
|
|
22
22
|
a = addFigureBox(a, { id, size: { x: 900, y: 1800, z: 40 }, position: { x, y: 1000, z: 0 } })
|
|
23
23
|
a = applyFigureAuthoring(a, { kind: 'attach', part: id, to: { part: 'frame', face: 'top' } })
|
|
24
24
|
const j = a.joints!.find(k => k.body1.part === id)!
|
|
25
|
-
a = applyFigureAuthoring(a, { kind: 'set-joint', joint: { ...j, type: 'revolute', axis: 'Y', state: { id: `${id}-open` } } })
|
|
25
|
+
a = applyFigureAuthoring(a, { kind: 'set-joint', joint: { ...j, motion: [{ type: 'revolute', axis: 'Y', state: { id: `${id}-open` } }] } })
|
|
26
26
|
a = editFigurePart(a, { kind: 'set-driver', state: `${id}-open`, clip: `${id}-swing`, duration: 2, keys: [{ seconds: 0, value: 0 }, { seconds: 2, value: 80 }] })
|
|
27
27
|
}
|
|
28
28
|
return serializeFigureAsset(a)
|
|
@@ -31,7 +31,7 @@ const store = { load: async (id: string) => ({ id, source: doors() }), save: asy
|
|
|
31
31
|
const clipsOf = (s: ReturnType<typeof createFigureProductSession>) => (parseFigureAsset(s.source()) as FigureAsset).drivers?.map(d => d.clip) ?? []
|
|
32
32
|
const fixed = (s: ReturnType<typeof createFigureProductSession>, part: string) => {
|
|
33
33
|
const j = (parseFigureAsset(s.source()) as FigureAsset).joints!.find(k => k.body1.part === part)!
|
|
34
|
-
return { kind: 'set-joint', joint: { id: j.id,
|
|
34
|
+
return { kind: 'set-joint', joint: { id: j.id, body0: j.body0, body1: j.body1, origin: j.origin } } as never
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
test('refused and named first; agreed, exactly those go in one step; undo brings them back; consent does not outlive it', async () => {
|