@things-factory/figure-ui 10.1.208 → 10.1.211
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 +97 -88
- package/client/modeller/fasten-geometry.ts +2 -1
- package/client/modeller/figure-links.ts +87 -157
- package/client/modeller/figure-size-editor.ts +233 -0
- package/client/modeller/joint-drag.ts +25 -21
- package/client/modeller/joint-fan.ts +4 -3
- package/client/modeller/links.ts +87 -36
- package/client/modeller/live.ts +33 -0
- 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 +14 -1
- package/dist-client/modeller/editor.js +103 -89
- 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 +3 -4
- package/dist-client/modeller/figure-links.js +91 -145
- package/dist-client/modeller/figure-links.js.map +1 -1
- package/dist-client/modeller/figure-size-editor.d.ts +29 -0
- package/dist-client/modeller/figure-size-editor.js +262 -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 +21 -9
- package/dist-client/modeller/links.js +85 -35
- package/dist-client/modeller/links.js.map +1 -1
- package/dist-client/modeller/live.d.ts +14 -0
- package/dist-client/modeller/live.js +31 -0
- package/dist-client/modeller/live.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/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 +7 -7
- 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 +66 -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 +106 -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/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 +60 -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 +47 -10
- package/translations/es.json +47 -10
- package/translations/ja.json +47 -10
- package/translations/ko.json +52 -15
- package/translations/ms.json +47 -10
- package/translations/zh.json +47 -10
|
@@ -83,7 +83,7 @@ function sourceOf(kind, text) {
|
|
|
83
83
|
/* On the roller's side at 0° round its own Y -- its own +X (ADR-0140 3.9). */
|
|
84
84
|
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'mark', to: { part: 'roller', surface: { around: 0, along: 'centre' } } })
|
|
85
85
|
const j = asset.joints.find(o => o.body1.part === 'roller')
|
|
86
|
-
asset = applyFigureAuthoring(asset, { kind: 'set-joint', joint: { ...j, type: 'revolute', axis: 'Z', lowerLimit: -360, upperLimit: 360, state: { id: 'spin' } } })
|
|
86
|
+
asset = applyFigureAuthoring(asset, { kind: 'set-joint', joint: { ...j, motion: [{ type: 'revolute', axis: 'Z', lowerLimit: -360, upperLimit: 360, state: { id: 'spin' } }] } })
|
|
87
87
|
return serializeFigureAsset(applyFigureAuthoring(asset, { kind: 'make-repeat', part: 'roller', axis: 'x', pitch: 200 }))
|
|
88
88
|
}
|
|
89
89
|
throw Error(`no figure for ${kind}`)
|
|
@@ -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,9 +626,65 @@ 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
|
})
|
|
633
|
+
|
|
634
|
+
/* A joint is a list of axes (ADR-0159): one more is added where the motion is said, and taken away again. */
|
|
635
|
+
const jointOf = part => read(part => (JSON.parse(document.querySelector('figure-editor').session.source()).joints ?? []).find(j => j.body1.part === part) ?? null, part)
|
|
636
|
+
const button = async (text, attribute) =>
|
|
637
|
+
page().evaluate(
|
|
638
|
+
(text, attribute) => {
|
|
639
|
+
const deep = (root = document) => [...root.querySelectorAll('button')].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
|
|
640
|
+
const b = deep().find(x => x.textContent.includes(text) && (!attribute || x.hasAttribute(attribute)))
|
|
641
|
+
if (!b) return null
|
|
642
|
+
b.scrollIntoView({ block: 'center' })
|
|
643
|
+
const r = b.getBoundingClientRect()
|
|
644
|
+
return { x: r.left + r.width / 2, y: r.top + r.height / 2 }
|
|
645
|
+
},
|
|
646
|
+
text,
|
|
647
|
+
attribute
|
|
648
|
+
)
|
|
649
|
+
const rows = () =>
|
|
650
|
+
page().evaluate(() => {
|
|
651
|
+
const deep = (root = document) => [...root.querySelectorAll('div[motion-axis]')].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
|
|
652
|
+
return deep().length
|
|
653
|
+
})
|
|
654
|
+
|
|
655
|
+
test('an axis added to the shoulder: one step, after the first, on an axis the first does not use, with a control of its own', async () => {
|
|
656
|
+
const was = await jointOf('upper-arm')
|
|
657
|
+
await click(await button('figure.button.add-axis', 'add-axis'), 'add an axis')
|
|
658
|
+
const start = await state()
|
|
659
|
+
await click(await button('figure.button.add-axis'), 'the form\'s add-axis')
|
|
660
|
+
const after = await state()
|
|
661
|
+
assert.equal(after.error, '', 'not refused')
|
|
662
|
+
assert.equal(after.revision, start.revision + 1, 'one step')
|
|
663
|
+
const now = await jointOf('upper-arm')
|
|
664
|
+
assert.equal(now.motion.length, 2)
|
|
665
|
+
assert.deepEqual(now.motion[0], was.motion[0], 'the first axis as it was')
|
|
666
|
+
assert.notEqual(now.motion[1].axis, now.motion[0].axis, 'another axis')
|
|
667
|
+
assert.equal(now.motion[1].state.id, 'upper-arm-turn-2')
|
|
668
|
+
assert.equal(await rows(), 2, 'a row an axis')
|
|
669
|
+
})
|
|
670
|
+
|
|
671
|
+
test('the second axis taken away: one step, the first as it was', async () => {
|
|
672
|
+
const was = await jointOf('upper-arm')
|
|
673
|
+
const start = await state()
|
|
674
|
+
const removes = await page().evaluate(() => {
|
|
675
|
+
const deep = (root = document) => [...root.querySelectorAll('button[remove-axis]')].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
|
|
676
|
+
const b = deep()[1]
|
|
677
|
+
if (!b) return null
|
|
678
|
+
b.scrollIntoView({ block: 'center' })
|
|
679
|
+
const r = b.getBoundingClientRect()
|
|
680
|
+
return { x: r.left + r.width / 2, y: r.top + r.height / 2 }
|
|
681
|
+
})
|
|
682
|
+
await click(removes, 'remove the second axis')
|
|
683
|
+
assert.equal((await state()).revision, start.revision + 1, 'one step')
|
|
684
|
+
const now = await jointOf('upper-arm')
|
|
685
|
+
assert.deepEqual(now.motion, [was.motion[0]], 'the first axis alone, as it was')
|
|
686
|
+
assert.equal(await rows(), 1)
|
|
687
|
+
})
|
|
628
688
|
})
|
|
629
689
|
|
|
630
690
|
describe('the modeller made easier to find and to keep (2026-09-26 baseline)', () => {
|
|
@@ -660,16 +720,18 @@ describe('the modeller made easier to find and to keep (2026-09-26 baseline)', (
|
|
|
660
720
|
assert.equal(await resize.evaluate(b => b.getAttribute('aria-pressed')), 'false')
|
|
661
721
|
})
|
|
662
722
|
|
|
723
|
+
/* The part's size is the 「반응형」 tab's size editor (2026-10-24), and what a size typed keeps is said under it. */
|
|
663
724
|
test('a height typed into its field keeps the part\'s middle, and the size group says so (ADR-0140 4.1)', async () => {
|
|
664
725
|
await mount('box')
|
|
665
726
|
await click(await read(() => probe.partOnPage('box')))
|
|
727
|
+
await tab('response')
|
|
666
728
|
const was = await body('box')
|
|
667
729
|
const middleOf = b => b.t[1] + b.dimensions.start + b.dimensions.length / 2
|
|
668
730
|
assert.ok(await read(() => {
|
|
669
731
|
const deep = (root = document) => [...root.querySelectorAll('p[size-keeps]')].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
|
|
670
732
|
return deep().some(p => p.textContent.trim() === 'figure.text.size-keeps-centre')
|
|
671
733
|
}), 'said beside the size fields')
|
|
672
|
-
const height = await find('input', 'box.size
|
|
734
|
+
const height = await find('input', 'box Y figure.label.size-measure')
|
|
673
735
|
assert.ok(await height.evaluate(i => !!i), 'the height field')
|
|
674
736
|
const at = await centre(height)
|
|
675
737
|
await page().mouse.click(at.x, at.y, { clickCount: 3 })
|
|
@@ -744,20 +806,24 @@ describe('the modeller made easier to find and to keep (2026-09-26 baseline)', (
|
|
|
744
806
|
})
|
|
745
807
|
})
|
|
746
808
|
|
|
809
|
+
/*
|
|
810
|
+
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.
|
|
811
|
+
On a figure with no size, the first one declares the figure's size from its parts in the same step.
|
|
812
|
+
*/
|
|
747
813
|
describe('a part follows the figure size in one step (follow-size, 2026-09-26)', () => {
|
|
748
|
-
test('
|
|
814
|
+
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
815
|
await mount('box')
|
|
750
816
|
await click(await read(() => probe.row('box')))
|
|
751
817
|
await tab('response')
|
|
752
818
|
const at = await page().evaluate(() => {
|
|
753
|
-
const deep = (root = document) => [...root.querySelectorAll('
|
|
819
|
+
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
820
|
const b = deep()[0]
|
|
755
|
-
if (!b) return null
|
|
821
|
+
if (!b || b.checked) return null
|
|
756
822
|
b.scrollIntoView({ block: 'center' })
|
|
757
823
|
const r = b.getBoundingClientRect()
|
|
758
824
|
return { x: r.left + r.width / 2, y: r.top + r.height / 2 }
|
|
759
825
|
})
|
|
760
|
-
assert.ok(at, 'the
|
|
826
|
+
assert.ok(at, 'the width of a box that follows nothing can be made to follow')
|
|
761
827
|
await page().evaluate(() => {
|
|
762
828
|
window.__told = []
|
|
763
829
|
document.addEventListener('notify', e => window.__told.push(e.detail))
|
|
@@ -892,7 +958,7 @@ describe('a row\'s pitch made a board\'s to set (ADR-0096)', () => {
|
|
|
892
958
|
assert.deepEqual([input.min, input.max, source.designInputs['roller.pitch']], [100, 400, 200])
|
|
893
959
|
assert.deepEqual(source.joints.find(j => j.body1.part === 'roller').repeat.pitch, { from: { of: 'input', input: 'roller.pitch' } })
|
|
894
960
|
assert.equal((await read(() => probe.ids())).filter(id => id.includes('"roller.placed"')).length, before, 'the row as it was')
|
|
895
|
-
await typeInto('
|
|
961
|
+
await typeInto('figure.label.repeat-gap', '400')
|
|
896
962
|
source = await read(() => probe.source())
|
|
897
963
|
assert.equal(source.designInputs['roller.pitch'], 400, 'the pitch field sets the input')
|
|
898
964
|
assert.ok((await read(() => probe.ids())).filter(id => id.includes('"roller.placed"')).length < before, 'fewer rollers at a wider pitch')
|
|
@@ -919,10 +985,11 @@ describe('a row that carries a part (ADR-0093, 2026-09-26)', () => {
|
|
|
919
985
|
assert.ok(copy, 'a mark on the fourth roller is drawn')
|
|
920
986
|
await click(await read(id => probe.partOnPage(id), copy))
|
|
921
987
|
assert.equal((await state()).selected, 'mark', 'picked by the part it copies')
|
|
922
|
-
|
|
988
|
+
/* The row's pitch field is the selected part's own: named by what it is, the centre spacing. */
|
|
989
|
+
assert.equal(await read(() => probe.hasInput('figure.label.repeat-gap')), false, 'the mark has no row of its own to set')
|
|
923
990
|
await click(await read(() => probe.row('roller')))
|
|
924
991
|
assert.equal((await state()).selected, 'roller')
|
|
925
|
-
assert.ok(await read(() => probe.hasInput('
|
|
992
|
+
assert.ok(await read(() => probe.hasInput('figure.label.repeat-gap')), 'the roller offers its row')
|
|
926
993
|
/* 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
994
|
await click(await read(() => probe.button('change-fastening')))
|
|
928
995
|
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
|
|