@things-factory/figure-ui 10.1.204 → 10.1.206
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 +91 -9
- package/client/modeller/editor-styles.ts +23 -22
- package/client/modeller/editor.ts +217 -184
- package/client/modeller/fasten-form.ts +130 -12
- package/client/modeller/figure-fasten-dialog.ts +45 -11
- package/client/modeller/figure-fasten-drawing.ts +233 -12
- package/client/modeller/figure-links.ts +2 -2
- package/client/modeller/figure-preview.ts +6 -6
- package/client/modeller/figure-section-editor.ts +6 -6
- package/client/modeller/figure-slot-layout-panel.ts +1 -1
- package/client/modeller/gizmo.ts +22 -1
- package/client/modeller/keys.ts +10 -1
- package/client/modeller/links.ts +79 -46
- package/client/modeller/palette.ts +1 -1
- package/client/modeller/product-session.ts +16 -4
- package/client/modeller/refusal-teller.ts +3 -25
- package/client/modeller/refusals.ts +19 -26
- package/client/modeller/section-drawing.ts +7 -7
- package/client/modeller/size-drag.ts +112 -0
- package/client/modeller/slot-layout-host.ts +3 -3
- package/client/modeller/stage-model.ts +6 -9
- package/client/pages/figure-modeller-page.ts +1 -4
- package/dist-client/modeller/canvas.d.ts +24 -3
- package/dist-client/modeller/canvas.js +96 -11
- package/dist-client/modeller/canvas.js.map +1 -1
- package/dist-client/modeller/editor-styles.js +23 -22
- package/dist-client/modeller/editor-styles.js.map +1 -1
- package/dist-client/modeller/editor.d.ts +35 -35
- package/dist-client/modeller/editor.js +221 -193
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/fasten-form.d.ts +46 -4
- package/dist-client/modeller/fasten-form.js +120 -11
- package/dist-client/modeller/fasten-form.js.map +1 -1
- package/dist-client/modeller/figure-fasten-dialog.d.ts +18 -4
- package/dist-client/modeller/figure-fasten-dialog.js +49 -12
- package/dist-client/modeller/figure-fasten-dialog.js.map +1 -1
- package/dist-client/modeller/figure-fasten-drawing.d.ts +33 -1
- package/dist-client/modeller/figure-fasten-drawing.js +235 -11
- package/dist-client/modeller/figure-fasten-drawing.js.map +1 -1
- package/dist-client/modeller/figure-links.d.ts +1 -1
- package/dist-client/modeller/figure-links.js +2 -2
- package/dist-client/modeller/figure-links.js.map +1 -1
- package/dist-client/modeller/figure-preview.js +4 -4
- package/dist-client/modeller/figure-preview.js.map +1 -1
- package/dist-client/modeller/figure-section-editor.d.ts +3 -3
- package/dist-client/modeller/figure-section-editor.js +4 -4
- package/dist-client/modeller/figure-section-editor.js.map +1 -1
- package/dist-client/modeller/figure-slot-layout-panel.js +1 -1
- package/dist-client/modeller/figure-slot-layout-panel.js.map +1 -1
- package/dist-client/modeller/gizmo.d.ts +18 -1
- package/dist-client/modeller/gizmo.js +19 -0
- package/dist-client/modeller/gizmo.js.map +1 -1
- package/dist-client/modeller/keys.d.ts +1 -1
- package/dist-client/modeller/keys.js +6 -0
- package/dist-client/modeller/keys.js.map +1 -1
- package/dist-client/modeller/links.d.ts +30 -24
- package/dist-client/modeller/links.js +67 -42
- package/dist-client/modeller/links.js.map +1 -1
- package/dist-client/modeller/palette.js +1 -1
- package/dist-client/modeller/palette.js.map +1 -1
- package/dist-client/modeller/product-session.d.ts +15 -13
- package/dist-client/modeller/product-session.js +15 -5
- package/dist-client/modeller/product-session.js.map +1 -1
- package/dist-client/modeller/refusal-teller.d.ts +1 -12
- package/dist-client/modeller/refusal-teller.js +3 -22
- package/dist-client/modeller/refusal-teller.js.map +1 -1
- package/dist-client/modeller/refusals.d.ts +6 -14
- package/dist-client/modeller/refusals.js +16 -27
- package/dist-client/modeller/refusals.js.map +1 -1
- package/dist-client/modeller/section-drawing.d.ts +3 -4
- package/dist-client/modeller/section-drawing.js +10 -9
- package/dist-client/modeller/section-drawing.js.map +1 -1
- package/dist-client/modeller/size-drag.d.ts +38 -0
- package/dist-client/modeller/size-drag.js +85 -0
- package/dist-client/modeller/size-drag.js.map +1 -0
- package/dist-client/modeller/slot-layout-host.d.ts +1 -3
- package/dist-client/modeller/slot-layout-host.js +3 -3
- package/dist-client/modeller/slot-layout-host.js.map +1 -1
- package/dist-client/modeller/stage-model.d.ts +5 -7
- package/dist-client/modeller/stage-model.js +6 -6
- package/dist-client/modeller/stage-model.js.map +1 -1
- package/dist-client/pages/figure-modeller-page.d.ts +0 -2
- package/dist-client/pages/figure-modeller-page.js +1 -6
- package/dist-client/pages/figure-modeller-page.js.map +1 -1
- package/dist-client/route.d.ts +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -7
- package/test/align.test.ts +7 -11
- package/test/browser/acceptance-page.js +11 -1
- package/test/browser/editing.check.mjs +17 -13
- package/test/browser/links-page.js +2 -2
- package/test/browser/links.check.mjs +124 -69
- package/test/browser/modeller-page.js +30 -18
- package/test/browser/modeller-revision.check.mjs +39 -0
- package/test/browser/modeller.check.mjs +77 -26
- package/test/browser/parts.check.mjs +49 -0
- package/test/browser/screen-page.js +0 -2
- package/test/browser/screen.check.mjs +11 -13
- package/test/browser/section.check.mjs +29 -2
- package/test/fasten-form.test.ts +102 -1
- package/test/gizmo.test.ts +25 -16
- package/test/keys.test.ts +11 -0
- package/test/links.test.ts +46 -36
- package/test/refusals.test.ts +17 -21
- package/test/resize-with-placement.test.ts +14 -13
- package/test/revision.test.ts +3 -2
- package/test/section-editor.test.ts +9 -10
- package/test/size-drag.test.ts +114 -0
- package/test/slot-layout-host.test.ts +13 -8
- package/translations/en.json +32 -34
- package/translations/es.json +32 -34
- package/translations/ja.json +32 -34
- package/translations/ko.json +32 -34
- package/translations/ms.json +32 -34
- package/translations/zh.json +32 -34
|
@@ -388,6 +388,18 @@ describe('a figure made in the modeller is given its size from its parts (user 2
|
|
|
388
388
|
await tab('response')
|
|
389
389
|
})
|
|
390
390
|
|
|
391
|
+
/* In words a person reads (user 2026-10-20): what the figure does on a drawing, not the range behind it. */
|
|
392
|
+
const sizeGroup = () =>
|
|
393
|
+
page().evaluate(() => {
|
|
394
|
+
const deep = (root = document) => [...root.querySelectorAll('section[board-size]')].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
|
|
395
|
+
const s = deep()[0]
|
|
396
|
+
return s ? { title: [...(s.querySelector('h3')?.childNodes ?? [])].filter(n => n.nodeType === 3).map(n => n.textContent).join('').trim(), fixed: s.querySelector('p[fixed-size]')?.textContent.trim() ?? null, followers: s.querySelector('p[followers]')?.textContent.trim() ?? null, dashes: s.querySelectorAll('div[pair][range] > span[dash]').length } : null
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
test('fixed: the size it is always drawn at, in one sentence, and the way to let a drawing resize it', async () => {
|
|
400
|
+
assert.deepEqual(await sizeGroup(), { title: 'figure.label.board-size', fixed: 'figure.text.board-size-fixed', followers: null, dashes: 0 })
|
|
401
|
+
})
|
|
402
|
+
|
|
391
403
|
test('with nothing picked, the figure offers to declare its size; clicked, its size fields appear', async () => {
|
|
392
404
|
const button = await page().evaluateHandle(() => {
|
|
393
405
|
const deep = (root = document) => [...root.querySelectorAll('button')].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
|
|
@@ -407,6 +419,10 @@ describe('a figure made in the modeller is given its size from its parts (user 2
|
|
|
407
419
|
assert.deepEqual(await read(() => probe.fields('size.')), { 'size.x': 400, 'size.y': 400, 'size.z': 400 }, 'the size it is authored at: its parts now')
|
|
408
420
|
})
|
|
409
421
|
|
|
422
|
+
test('resizable: each axis reads smallest – largest, and which parts change with it (none on a lone box)', async () => {
|
|
423
|
+
assert.deepEqual(await sizeGroup(), { title: 'figure.label.board-size', fixed: null, followers: 'figure.text.board-size-no-followers', dashes: 3 })
|
|
424
|
+
})
|
|
425
|
+
|
|
410
426
|
test('the sizes it may be placed at are typed: the largest width, one step, and a smallest past its size refused', async () => {
|
|
411
427
|
const field = id =>
|
|
412
428
|
page().evaluateHandle(id => {
|
|
@@ -436,8 +452,8 @@ describe('a figure made in the modeller is given its size from its parts (user 2
|
|
|
436
452
|
assert.equal((await declared())['size.x'][1], 3000, 'the largest width is what was typed')
|
|
437
453
|
await type('size.x.min', '500')
|
|
438
454
|
assert.notEqual((await state()).error, '', 'the figure is 400 wide: a smallest of 500 is refused')
|
|
439
|
-
assert.equal((await declared())['size.x'][0]
|
|
440
|
-
assert.
|
|
455
|
+
assert.equal((await declared())['size.x'][0] ?? null, null, 'and the smallest is still open (ADR-0087 decision 6)')
|
|
456
|
+
assert.deepEqual(await (await field('size.x.min')).evaluate(i => [i.value, i.placeholder]), ['', '0'], 'the field empty again, what the parts allow written faintly in it')
|
|
441
457
|
})
|
|
442
458
|
|
|
443
459
|
/*
|
|
@@ -451,6 +467,34 @@ describe('a figure made in the modeller is given its size from its parts (user 2
|
|
|
451
467
|
assert.notEqual(said, 'figure.text.edit-refused', `said in its own words: ${said}`)
|
|
452
468
|
})
|
|
453
469
|
|
|
470
|
+
test('a field emptied opens its end again, in one step: the largest is what the parts allow, nothing', async () => {
|
|
471
|
+
const field = () =>
|
|
472
|
+
page().evaluateHandle(() => {
|
|
473
|
+
const deep = (root = document) => [...root.querySelectorAll('input')].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
|
|
474
|
+
return deep().find(i => i.getAttribute('aria-label') === 'size.x.max') ?? null
|
|
475
|
+
})
|
|
476
|
+
/* Selected where it is drawn, as a person does, then typed over. */
|
|
477
|
+
const typeOver = async keys => {
|
|
478
|
+
const at = await (await field()).evaluate(i => {
|
|
479
|
+
i.scrollIntoView({ block: 'center' })
|
|
480
|
+
const r = i.getBoundingClientRect()
|
|
481
|
+
return { x: r.left + r.width / 2, y: r.top + r.height / 2 }
|
|
482
|
+
})
|
|
483
|
+
await page().mouse.click(at.x, at.y, { clickCount: 3 })
|
|
484
|
+
await keys()
|
|
485
|
+
await page().keyboard.press('Tab')
|
|
486
|
+
await sleep(600)
|
|
487
|
+
}
|
|
488
|
+
await typeOver(() => page().keyboard.type('3000'))
|
|
489
|
+
assert.equal(await (await field()).evaluate(i => i.value), '3000', 'narrowed first')
|
|
490
|
+
const start = await state()
|
|
491
|
+
await typeOver(() => page().keyboard.press('Backspace'))
|
|
492
|
+
const after = await state()
|
|
493
|
+
assert.equal(after.error, '', 'not refused')
|
|
494
|
+
assert.equal(after.revision, start.revision + 1, 'one step')
|
|
495
|
+
assert.deepEqual(await (await field()).evaluate(i => [i.value, i.placeholder]), ['', '∞'], 'empty, and nothing limits it')
|
|
496
|
+
})
|
|
497
|
+
|
|
454
498
|
test('a part follows the figure\'s width less a difference: chosen and typed, the width is the size less it', async () => {
|
|
455
499
|
await click(await read(() => probe.partOnPage('box')))
|
|
456
500
|
assert.equal((await state()).selected, 'box')
|
|
@@ -484,23 +528,17 @@ describe('a figure made in the modeller is given its size from its parts (user 2
|
|
|
484
528
|
await page().keyboard.type('-50')
|
|
485
529
|
await page().keyboard.press('Tab')
|
|
486
530
|
await sleep(600)
|
|
487
|
-
/*
|
|
488
|
-
|
|
489
|
-
|
|
531
|
+
/*
|
|
532
|
+
The sizes the figure may be drawn at are worked out from its parts (ADR-0087 decision 6), not refused against a
|
|
533
|
+
range nobody chose: the link is taken as typed, with nothing asked.
|
|
534
|
+
*/
|
|
535
|
+
assert.equal((await state()).error, '', 'taken')
|
|
536
|
+
assert.equal((await body('box')).dimensions['size.x'], 350, 'the figure\'s 400 less 50')
|
|
490
537
|
const asked = await read(() => {
|
|
491
538
|
const deep = (root = document) => [...root.querySelectorAll('ox-prompt')].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
|
|
492
|
-
|
|
493
|
-
const confirm = prompt?.shadowRoot?.querySelector('#confirm')
|
|
494
|
-
if (!confirm) return null
|
|
495
|
-
const r = confirm.getBoundingClientRect()
|
|
496
|
-
return { text: prompt.text, label: confirm.textContent.trim(), x: r.left + r.width / 2, y: r.top + r.height / 2 }
|
|
539
|
+
return deep().length
|
|
497
540
|
})
|
|
498
|
-
assert.
|
|
499
|
-
assert.match(asked.label, /figure.button.narrow-range-from/, 'the same link is offered with the sizes narrowed')
|
|
500
|
-
await click(asked)
|
|
501
|
-
await sleep(600)
|
|
502
|
-
assert.equal((await state()).error, '', 'taken')
|
|
503
|
-
assert.equal((await body('box')).dimensions['size.x'], 350, 'the figure\'s 400 less 50')
|
|
541
|
+
assert.equal(asked, 0, 'nothing asked')
|
|
504
542
|
})
|
|
505
543
|
|
|
506
544
|
test('grown by one ratio, every part is made to follow it in one step, and the offer goes', async () => {
|
|
@@ -518,8 +556,15 @@ describe('a figure made in the modeller is given its size from its parts (user 2
|
|
|
518
556
|
return { x: r.left + r.width / 2, y: r.top + r.height / 2 }
|
|
519
557
|
}))
|
|
520
558
|
}
|
|
559
|
+
/* Back to the whole figure: a key in a field is the field's, so the field typed in is left first, then Esc. */
|
|
560
|
+
await page().evaluate(() => {
|
|
561
|
+
let focused = document.activeElement
|
|
562
|
+
while (focused?.shadowRoot?.activeElement) focused = focused.shadowRoot.activeElement
|
|
563
|
+
focused?.blur()
|
|
564
|
+
})
|
|
521
565
|
await page().keyboard.press('Escape')
|
|
522
566
|
await sleep(300)
|
|
567
|
+
assert.equal((await state()).selected, '', 'nothing picked')
|
|
523
568
|
await press('figure.button.scale-uniformly')
|
|
524
569
|
assert.equal((await state()).error, '', 'grows by one ratio')
|
|
525
570
|
const start = await state()
|
|
@@ -615,9 +660,15 @@ describe('the modeller made easier to find and to keep (2026-09-26 baseline)', (
|
|
|
615
660
|
assert.equal(await resize.evaluate(b => b.getAttribute('aria-pressed')), 'false')
|
|
616
661
|
})
|
|
617
662
|
|
|
618
|
-
test('a height typed into its field keeps the
|
|
663
|
+
test('a height typed into its field keeps the part\'s middle, and the size group says so (ADR-0140 4.1)', async () => {
|
|
619
664
|
await mount('box')
|
|
620
665
|
await click(await read(() => probe.partOnPage('box')))
|
|
666
|
+
const was = await body('box')
|
|
667
|
+
const middleOf = b => b.t[1] + b.dimensions.start + b.dimensions.length / 2
|
|
668
|
+
assert.ok(await read(() => {
|
|
669
|
+
const deep = (root = document) => [...root.querySelectorAll('p[size-keeps]')].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
|
|
670
|
+
return deep().some(p => p.textContent.trim() === 'figure.text.size-keeps-centre')
|
|
671
|
+
}), 'said beside the size fields')
|
|
621
672
|
const height = await find('input', 'box.size.y')
|
|
622
673
|
assert.ok(await height.evaluate(i => !!i), 'the height field')
|
|
623
674
|
const at = await centre(height)
|
|
@@ -626,8 +677,8 @@ describe('the modeller made easier to find and to keep (2026-09-26 baseline)', (
|
|
|
626
677
|
await page().keyboard.press('Enter')
|
|
627
678
|
await sleep(600)
|
|
628
679
|
const now = await body('box')
|
|
629
|
-
/* The box is drawn
|
|
630
|
-
assert.equal(Math.round(now
|
|
680
|
+
/* The box is drawn along y from its start: its middle is its origin plus the start plus half the length. */
|
|
681
|
+
assert.equal(Math.round(middleOf(now)), Math.round(middleOf(was)), `its middle where it was: ${middleOf(was)} -> ${middleOf(now)}`)
|
|
631
682
|
assert.equal(now.dimensions.length, 1000)
|
|
632
683
|
})
|
|
633
684
|
|
|
@@ -895,14 +946,14 @@ describe('a row that carries a part (ADR-0093, 2026-09-26)', () => {
|
|
|
895
946
|
|
|
896
947
|
describe('the row section opens on one click', () => {
|
|
897
948
|
test('a part that can be made a row: one click on the folded heading opens it, a second folds it', async () => {
|
|
898
|
-
await mount('
|
|
899
|
-
await click(await read(() => probe.row('
|
|
949
|
+
await mount('shelf')
|
|
950
|
+
await click(await read(() => probe.row('slat')))
|
|
900
951
|
await tab('response')
|
|
901
952
|
const summary = await page().evaluateHandle(() => {
|
|
902
953
|
const deep = (root = document) => [...root.querySelectorAll('details[row-fold] > summary')].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
|
|
903
954
|
return deep()[0] ?? null
|
|
904
955
|
})
|
|
905
|
-
assert.ok(await summary.evaluate(s => !!s && !!s.parentElement.querySelector('button[act]')), 'the
|
|
956
|
+
assert.ok(await summary.evaluate(s => !!s && !!s.parentElement.querySelector('button[act]')), 'the slat is offered a row to make')
|
|
906
957
|
const at = await summary.evaluate(s => {
|
|
907
958
|
s.scrollIntoView({ block: 'center' })
|
|
908
959
|
const r = s.getBoundingClientRect()
|
|
@@ -915,9 +966,9 @@ describe('the row section opens on one click', () => {
|
|
|
915
966
|
})
|
|
916
967
|
|
|
917
968
|
test('a part picked again: a fold opened at once stays open -- the folds kept for the part are put back first', async () => {
|
|
918
|
-
await mount('
|
|
919
|
-
await click(await read(() => probe.row('
|
|
920
|
-
await click(await read(() => probe.row('
|
|
969
|
+
await mount('shelf')
|
|
970
|
+
await click(await read(() => probe.row('slat')))
|
|
971
|
+
await click(await read(() => probe.row('bed')))
|
|
921
972
|
const open = await read(async () => {
|
|
922
973
|
const deep = (selector, root = document) => [...root.querySelectorAll(selector)].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(selector, e.shadowRoot)))
|
|
923
974
|
const editor = deep('figure-editor')[0]
|
|
@@ -925,7 +976,7 @@ describe('the row section opens on one click', () => {
|
|
|
925
976
|
The panel is drawn for the part once the editor and its links have updated; a click can come any time after
|
|
926
977
|
that, before a frame is drawn. Here it comes right then.
|
|
927
978
|
*/
|
|
928
|
-
editor.selected = '
|
|
979
|
+
editor.selected = 'slat'
|
|
929
980
|
await editor.updateComplete
|
|
930
981
|
await Promise.all(deep('figure-links').map(links => links.updateComplete))
|
|
931
982
|
await null
|
|
@@ -141,6 +141,55 @@ describe('the parts list: each button does what it says, one step of the session
|
|
|
141
141
|
})
|
|
142
142
|
})
|
|
143
143
|
|
|
144
|
+
/*
|
|
145
|
+
Copy and paste with the canvas focused (user 2026-10-21): left to the scene, Ctrl+C / Ctrl+V copied the canvas object
|
|
146
|
+
with the part's name -- two objects of one name, the copy moving the original and never a part of its own.
|
|
147
|
+
*/
|
|
148
|
+
describe('copy and paste on the canvas: a part of its own, named by figure-model', () => {
|
|
149
|
+
before(() => mount('box', 'three'))
|
|
150
|
+
|
|
151
|
+
const withControl = async code => {
|
|
152
|
+
await page().keyboard.down('Control')
|
|
153
|
+
await page().keyboard.press(code)
|
|
154
|
+
await page().keyboard.up('Control')
|
|
155
|
+
await sleep(400)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
test('Ctrl+C then Ctrl+V on the canvas: one step, a new row with a name of its own, one canvas object per name', async () => {
|
|
159
|
+
await click(await read(() => probe.partOnPage('b')), 'b on the canvas')
|
|
160
|
+
assert.equal((await state()).selected, 'b', 'picked on the canvas, the canvas focused')
|
|
161
|
+
const was = await ids()
|
|
162
|
+
const step = await revision()
|
|
163
|
+
await withControl('KeyC')
|
|
164
|
+
await withControl('KeyV')
|
|
165
|
+
const now = await ids()
|
|
166
|
+
const made = now.filter(id => !was.includes(id))
|
|
167
|
+
assert.equal(made.length, 1, `one new part: ${now}`)
|
|
168
|
+
assert.notEqual(made[0], 'b')
|
|
169
|
+
assert.equal(await revision(), step + 1, 'one step')
|
|
170
|
+
assert.equal((await state()).selected, made[0], 'the copy is the part selected')
|
|
171
|
+
const drawn = await read(() => parts.drawn())
|
|
172
|
+
assert.equal(new Set(drawn).size, drawn.length, `no two canvas objects of one name: ${drawn}`)
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
test('Ctrl+X and Ctrl+Shift+V are kept from the scene: nothing cut or pasted on the canvas alone', async () => {
|
|
176
|
+
await click(await read(() => probe.partOnPage('b')), 'b on the canvas')
|
|
177
|
+
const was = await ids()
|
|
178
|
+
const step = await revision()
|
|
179
|
+
await withControl('KeyX')
|
|
180
|
+
await page().keyboard.down('Control')
|
|
181
|
+
await page().keyboard.down('Shift')
|
|
182
|
+
await page().keyboard.press('KeyV')
|
|
183
|
+
await page().keyboard.up('Shift')
|
|
184
|
+
await page().keyboard.up('Control')
|
|
185
|
+
await sleep(400)
|
|
186
|
+
assert.deepEqual(await ids(), was)
|
|
187
|
+
assert.equal(await revision(), step)
|
|
188
|
+
const drawn = await read(() => parts.drawn())
|
|
189
|
+
assert.deepEqual([...drawn].sort(), [...was].sort(), 'the canvas draws the figure\'s parts, no more, no fewer')
|
|
190
|
+
})
|
|
191
|
+
})
|
|
192
|
+
|
|
144
193
|
describe('hidden on the workbench: off the canvas, not out of the figure, and let go when another figure opens', () => {
|
|
145
194
|
before(() => mount('box', 'three'))
|
|
146
195
|
|
|
@@ -8,7 +8,6 @@ import './modeller-page.js'
|
|
|
8
8
|
import { captureFigureThumbnail } from 'figure-ui-client/modeller/thumbnail.js'
|
|
9
9
|
import { drawableFigureParts } from 'figure-ui-client/modeller/renderer.js'
|
|
10
10
|
import { previewTypeOf } from '@things-factory/figure-viewer/dist-client/preview-model.js'
|
|
11
|
-
import { figureReportOf } from 'figure-ui-client/modeller/maturity.js'
|
|
12
11
|
import { hasFigure } from '@hatiolab/things-scene'
|
|
13
12
|
import {
|
|
14
13
|
addFigureBox,
|
|
@@ -118,7 +117,6 @@ window.screenCheck = {
|
|
|
118
117
|
registered: type => hasFigure(type),
|
|
119
118
|
previewTypeOf,
|
|
120
119
|
/** The report the verdict tab measures, for the figure the editor holds. */
|
|
121
|
-
capacityNow: () => figureReportOf(window.screenCheck.editor().session.revisionNow().asset)?.capacity.instances,
|
|
122
120
|
/** The card's picture of a figure (saved text), as a save takes it. */
|
|
123
121
|
async picture(text) {
|
|
124
122
|
const asset = JSON.parse(text)
|
|
@@ -223,9 +223,9 @@ describe('the frame: parts · canvas · one side of seven tabs, and the bar over
|
|
|
223
223
|
await pressTab(TAB('properties'))
|
|
224
224
|
})
|
|
225
225
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
await
|
|
226
|
+
/* The count once drawn on the verdict tab covered its icon and was not something to act on (2026-10-20): none. */
|
|
227
|
+
test('the verdict tab carries no count over its icon', async () => {
|
|
228
|
+
assert.equal(await read(() => screenCheck.editor().shadowRoot.querySelectorAll('button[tab] span[badge]').length), 0)
|
|
229
229
|
})
|
|
230
230
|
|
|
231
231
|
test('nothing chosen: the properties panel opens on the figure\'s own settings, with no part head', async () => {
|
|
@@ -240,8 +240,11 @@ describe('the frame: parts · canvas · one side of seven tabs, and the bar over
|
|
|
240
240
|
assert.equal(await read(() => screenCheck.editor().shadowRoot.querySelector('ox-canvas-bar').pane === screenCheck.editor().shadowRoot.querySelector('ox-three-pane')), true, 'the bar folds the editor\'s own frame')
|
|
241
241
|
const titles = await read(() => ['undo', 'redo'].map(a => screenCheck.editor().shadowRoot.querySelector('ox-canvas-bar').shadowRoot.querySelector(`button[data-action=${a}]`).title))
|
|
242
242
|
assert.ok(titles[0].endsWith('(Ctrl+Z)') && titles[1].endsWith('(Ctrl+Y)'), `the keys in the titles: ${titles}`)
|
|
243
|
-
|
|
244
|
-
|
|
243
|
+
/* Saved or not is the save button's: pressable with something to save, dimmed without -- no words beside it. */
|
|
244
|
+
const status = () => read(() => screenCheck.editor().shadowRoot.querySelector('ox-canvas-bar [role=status]')?.textContent.trim() ?? null)
|
|
245
|
+
const saveOn = () => read(() => !screenCheck.editor().shadowRoot.querySelector('ox-canvas-bar button[save]').disabled)
|
|
246
|
+
assert.equal(await status(), null, 'nothing said while nothing is under way')
|
|
247
|
+
assert.equal(await saveOn(), false, 'saved: the save button is dimmed')
|
|
245
248
|
|
|
246
249
|
await pressRow('box')
|
|
247
250
|
const height = await element('the height field', () => screenCheck.deep('input').find(i => i.getAttribute('aria-label') === 'box.size.y'))
|
|
@@ -249,14 +252,15 @@ describe('the frame: parts · canvas · one side of seven tabs, and the bar over
|
|
|
249
252
|
await page().keyboard.type('600')
|
|
250
253
|
await page().keyboard.press('Enter')
|
|
251
254
|
await until('the height is 600', () => probe.value('box.size.y') === 600)
|
|
252
|
-
await until('
|
|
255
|
+
await until('something to save', () => !screenCheck.editor().shadowRoot.querySelector('ox-canvas-bar button[save]').disabled)
|
|
256
|
+
assert.equal(await status(), null, 'and still no words beside it')
|
|
253
257
|
|
|
254
258
|
await pressBar('undo')
|
|
255
259
|
await until('undone', () => probe.value('box.size.y') === 400)
|
|
256
260
|
await pressBar('redo')
|
|
257
261
|
await until('redone', () => probe.value('box.size.y') === 600)
|
|
258
262
|
await press('save', () => screenCheck.editor().shadowRoot.querySelector('ox-canvas-bar button[save]'))
|
|
259
|
-
await until('
|
|
263
|
+
await until('saved: dimmed again', () => screenCheck.editor().shadowRoot.querySelector('ox-canvas-bar button[save]').disabled)
|
|
260
264
|
|
|
261
265
|
const width = () => read(() => Math.round(screenCheck.deep('figure-canvas')[0].getBoundingClientRect().width))
|
|
262
266
|
const was = await width()
|
|
@@ -1077,12 +1081,6 @@ describe('the modeller page: the editor with the page\'s figure panel, verdict a
|
|
|
1077
1081
|
await press('the release button', () => screenCheck.editor().shadowRoot.querySelector('ox-canvas-bar button[publish]'))
|
|
1078
1082
|
})
|
|
1079
1083
|
|
|
1080
|
-
test('the verdict tab\'s count is the report\'s, measured by the verdict', async () => {
|
|
1081
|
-
const expected = await read(() => screenCheck.capacityNow())
|
|
1082
|
-
assert.ok(expected > 0)
|
|
1083
|
-
await until('the count on the tab', expected => screenCheck.editor().shadowRoot.querySelector('button[tab][aria-label="figure.label.tab-short-verdict"] span[badge]')?.textContent.trim() === expected.toLocaleString(), expected)
|
|
1084
|
-
})
|
|
1085
|
-
|
|
1086
1084
|
test('a new type asks the verdict again', async () => {
|
|
1087
1085
|
await pressTab(TAB('properties'))
|
|
1088
1086
|
await pressRow('left')
|
|
@@ -291,11 +291,11 @@ describe('the section editor on a box: what follows, what is kept, and a figure
|
|
|
291
291
|
await openEditor()
|
|
292
292
|
})
|
|
293
293
|
|
|
294
|
-
test('the length says
|
|
294
|
+
test('the length says what a length typed keeps: the part\'s middle (ADR-0140 4.1)', async () => {
|
|
295
295
|
const now = await editorNow()
|
|
296
296
|
assert.equal(now.length.label, 'figure.label.section-editor-length')
|
|
297
297
|
assert.equal(now.length.disabled, false)
|
|
298
|
-
assert.equal(now.length.note, 'figure.text.
|
|
298
|
+
assert.equal(now.length.note, 'figure.text.size-keeps-centre')
|
|
299
299
|
})
|
|
300
300
|
|
|
301
301
|
test('the figure changed while the editor is open: apply is refused, and closing does not offer it', async () => {
|
|
@@ -330,6 +330,33 @@ describe('the section editor on a box whose sizes follow the figure', () => {
|
|
|
330
330
|
})
|
|
331
331
|
})
|
|
332
332
|
|
|
333
|
+
/*
|
|
334
|
+
* The user's case (2026-10-21, 「수정본 검증 - 기본 버튼 상자」): 「속 공간 만들기」 ticked on a box whose sizes follow the
|
|
335
|
+
* figure's was refused, for a 1 mm the figure had never been asked to go to. Now it is one step and nothing is refused:
|
|
336
|
+
* how small the figure can then be is computed from the walls (ADR-0087 decision 6).
|
|
337
|
+
*/
|
|
338
|
+
describe('the inside made on a box whose sizes follow the figure', () => {
|
|
339
|
+
before(async () => {
|
|
340
|
+
await mount('follows')
|
|
341
|
+
await pick('box')
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
test('ticked, it is one step, drawn, and not refused', async () => {
|
|
345
|
+
const start = await state()
|
|
346
|
+
/* Clicked where it is drawn, as a person does. */
|
|
347
|
+
const at = await (await element('label[toggle] input', { within: 'figure-section-panel' })).evaluate(e => {
|
|
348
|
+
e.scrollIntoView({ block: 'center' })
|
|
349
|
+
const r = e.getBoundingClientRect()
|
|
350
|
+
return { x: r.left + r.width / 2, y: r.top + r.height / 2 }
|
|
351
|
+
})
|
|
352
|
+
await page().mouse.click(at.x, at.y)
|
|
353
|
+
await until(r => probe.state().revision === r + 1, 'one step', start.revision)
|
|
354
|
+
assert.equal((await state()).error, '', 'not refused')
|
|
355
|
+
await until(() => probe.sectionPanel()?.cavityDrawing, 'the inside drawn')
|
|
356
|
+
assert.ok((await held('box')).cavity, 'the box has its inside')
|
|
357
|
+
})
|
|
358
|
+
})
|
|
359
|
+
|
|
333
360
|
describe('a drawn section\'s end drawn to one point', () => {
|
|
334
361
|
test('turned on, the length is called the height', async () => {
|
|
335
362
|
await mount('wedge')
|
package/test/fasten-form.test.ts
CHANGED
|
@@ -9,7 +9,7 @@ import assert from 'node:assert/strict'
|
|
|
9
9
|
import { addFigureBox, applyFigureAuthoring, compileFigureAsset, createFigureAsset } from '@hatiolab/figure-model'
|
|
10
10
|
import type { FigureAsset } from '@hatiolab/figure-model'
|
|
11
11
|
|
|
12
|
-
import { aimFasten, fastenChanged, fastenDetailWordsOf, fastenFormOf, fastenPlanOf, fastenSentenceOf, fastenTargetsOf, freshFasten } from '../client/modeller/fasten-form.ts'
|
|
12
|
+
import { aimFasten, aimFastenAt, fastenChanged, fastenDetailWordsOf, fastenFormOf, fastenPlanOf, fastenSentenceOf, fastenTargetsOf, freshFasten, keptGapOf } from '../client/modeller/fasten-form.ts'
|
|
13
13
|
import { revisionOf } from './revision-of.ts'
|
|
14
14
|
|
|
15
15
|
const box = (asset: FigureAsset, id: string, [x, y, z]: number[], at = [0, 0, 0]) => addFigureBox(asset, { id, size: { x, y, z }, position: { x: at[0]!, y: at[1]!, z: at[2]! } })
|
|
@@ -94,6 +94,46 @@ test('the drawing\'s changes: another face aims again, the way, the gap, a place
|
|
|
94
94
|
assert.equal(fastenPlanOf(r, 'roller', f).targetFace, 'front')
|
|
95
95
|
})
|
|
96
96
|
|
|
97
|
+
/*
|
|
98
|
+
* 「현재 상태로 붙이기」(ADR-0140 stage 4, user 2026-10-20): aimed at a face, the gap starts at the distance the part
|
|
99
|
+
* stands from it, so fastening does not move it. Changing how it meets the face works it out again; a number typed is
|
|
100
|
+
* the author's and stays; a press on the canvas puts the part there, with no gap.
|
|
101
|
+
*/
|
|
102
|
+
const centreOf = (asset: FigureAsset, id: string) => (compileFigureAsset(asset).evaluate().geometry as Array<{ id: string; pose?: { t: number[] } }>).find(g => g.id === id)!.pose!.t
|
|
103
|
+
/* A plate on the floor, its top at 20 mm, and a lid 40 mm above it. */
|
|
104
|
+
function floating() {
|
|
105
|
+
let a = fasten(box(createFigureAsset('Lift'), 'plate', [600, 20, 600], [0, 10, 0]), 'plate', 'plane')
|
|
106
|
+
return revisionOf(box(a, 'lid', [100, 20, 100], [0, 70, 0]))
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
test('aimed at a face, the gap is where the part stands, and fastening it does not move it', () => {
|
|
110
|
+
const r = floating()
|
|
111
|
+
const f = keptGapOf(r, 'lid', aimFasten(r, 'lid', fastenFormOf(r, 'lid'), { part: 'plate' }))
|
|
112
|
+
assert.deepEqual([f.gap, f.gapFrom], [40, 'kept'])
|
|
113
|
+
const applied = applyFigureAuthoring(r.asset, fastenPlanOf(r, 'lid', f).action!)
|
|
114
|
+
assert.deepEqual(centreOf(applied, 'lid'), centreOf(r.asset, 'lid'), 'the lid where it was')
|
|
115
|
+
const onPlane = keptGapOf(r, 'lid', aimFasten(r, 'lid', f, { plane: true }))
|
|
116
|
+
assert.equal(onPlane.gap, 60, 'aimed at the floor instead: 60 mm above it')
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
test('another way of meeting works the gap out again; a number typed stays; a press on the canvas puts it there', () => {
|
|
120
|
+
const r = floating()
|
|
121
|
+
const aimed = keptGapOf(r, 'lid', aimFasten(r, 'lid', fastenFormOf(r, 'lid'), { part: 'plate' }))
|
|
122
|
+
const flush = fastenChanged(r, 'lid', aimed, { mode: 'flush' })
|
|
123
|
+
const applied = applyFigureAuthoring(r.asset, fastenPlanOf(r, 'lid', flush).action!)
|
|
124
|
+
assert.deepEqual(centreOf(applied, 'lid'), centreOf(r.asset, 'lid'), 'flush, and still where it was')
|
|
125
|
+
assert.deepEqual(fastenDetailWordsOf(aimed, say), [], 'the gap it stands at is the default: nothing said on the fold')
|
|
126
|
+
const typed = fastenChanged(r, 'lid', aimed, { gap: 5 })
|
|
127
|
+
assert.equal(typed.gapFrom, 'typed')
|
|
128
|
+
assert.deepEqual(fastenDetailWordsOf(typed, say), ['figure.text.fasten-summary-gap(mm=5)'])
|
|
129
|
+
assert.equal(fastenChanged(r, 'lid', typed, { mode: 'flush' }).gap, 5, 'the author\'s number is left')
|
|
130
|
+
assert.equal(keptGapOf(r, 'lid', aimFasten(r, 'lid', typed, { plane: true })).gap, 5)
|
|
131
|
+
const plate = (r.geometry as Array<{ id: string }>).find(g => g.id === 'plate')!.id
|
|
132
|
+
const pressed = aimFastenAt(r, 'lid', aimed, { id: plate, point: [0, 20, 0], normal: [0, 1, 0] }).form!
|
|
133
|
+
assert.deepEqual([pressed.gap, pressed.gapFrom], [0, 'pressed'])
|
|
134
|
+
assert.equal(fastenChanged(r, 'lid', pressed, { mode: 'flush' }).gap, 0, 'pressed: left at 0')
|
|
135
|
+
})
|
|
136
|
+
|
|
97
137
|
test('the target list: every other part, the one hanging from this part listed as a loop', () => {
|
|
98
138
|
const r = conveyor()
|
|
99
139
|
assert.deepEqual(fastenTargetsOf(r, 'frame'), [
|
|
@@ -102,3 +142,64 @@ test('the target list: every other part, the one hanging from this part listed a
|
|
|
102
142
|
])
|
|
103
143
|
assert.deepEqual(fastenTargetsOf(r, 'lid').map(t => t.part), ['frame', 'roller'])
|
|
104
144
|
})
|
|
145
|
+
|
|
146
|
+
/* A drum lying on the floor along Y and a ball, with a tag standing beside each (ADR-0140 3.9). */
|
|
147
|
+
async function curved() {
|
|
148
|
+
const { addFigurePrimitive } = await import('@hatiolab/figure-model')
|
|
149
|
+
let a = addFigurePrimitive(createFigureAsset('Curved'), { id: 'drum', kind: 'cylinder', dimensions: { radius: 100, length: 1600 }, position: { x: 0, y: 0, z: 0 } } as never)
|
|
150
|
+
a = addFigurePrimitive(a, { id: 'ball', kind: 'sphere', dimensions: { radius: 100, radiusY: 100 }, position: { x: 0, y: 0, z: 3000 } } as never)
|
|
151
|
+
a = box(a, 'tag', [40, 40, 40], [600, 600, 600])
|
|
152
|
+
return box(a, 'cap', [40, 40, 40], [0, 600, 3000])
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
test('aimed at a cylinder it stands beside, it goes on the side at the point it faces; aimed at a sphere, on its round', async () => {
|
|
156
|
+
const r = revisionOf(await curved())
|
|
157
|
+
const drum = aimFasten(r, 'tag', freshFasten(), { part: 'drum' })
|
|
158
|
+
assert.deepEqual(drum.surface, { around: 315, along: { mm: 600 } }, 'beside it, 600 up its length, toward +X +Z')
|
|
159
|
+
const plan = fastenPlanOf(r, 'tag', drum)
|
|
160
|
+
assert.deepEqual((plan.action as { to: unknown }).to, { part: 'drum', surface: { around: 315, along: { mm: 600 } } })
|
|
161
|
+
assert.equal((plan.action as { align?: unknown }).align, undefined, 'on a surface, where is its own words')
|
|
162
|
+
assert.deepEqual(plan.surface, { form: 'around', axis: 'y', place: { around: 315, along: { mm: 600 } } })
|
|
163
|
+
assert.equal(fastenSentenceOf(drum, plan, '바닥', say), 'figure.text.fasten-sentence-meet(to=drum의 figure.label.surface-around())')
|
|
164
|
+
const ball = aimFasten(r, 'cap', freshFasten(), { part: 'ball' })
|
|
165
|
+
assert.deepEqual(ball.surface, { toward: { x: 0, y: 1, z: 0 } }, 'straight above it')
|
|
166
|
+
/* Applied, and opened again: on the surface as it was. */
|
|
167
|
+
const done = applyFigureAuthoring(r.asset, plan.action!)
|
|
168
|
+
assert.deepEqual(fastenFormOf(revisionOf(done), 'tag').surface, { around: 315, along: { mm: 600 } })
|
|
169
|
+
const { rowBlockOf } = await import('../client/modeller/links.ts')
|
|
170
|
+
assert.equal(rowBlockOf(done, 'tag'), 'on-surface', 'a point of a curved surface has no face to lay a row across')
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
test('a face or the surface, as the drawing asks; the surface chosen starts where the part stands', async () => {
|
|
174
|
+
const r = revisionOf(await curved())
|
|
175
|
+
const side = aimFasten(r, 'tag', freshFasten(), { part: 'drum' })
|
|
176
|
+
const top = fastenChanged(r, 'tag', side, { targetFace: 'top' })
|
|
177
|
+
assert.equal(top.surface, undefined, 'its end disc is a face')
|
|
178
|
+
assert.deepEqual((fastenPlanOf(r, 'tag', top).action as { to: unknown }).to, { part: 'drum', face: 'top' })
|
|
179
|
+
const back = fastenChanged(r, 'tag', top, { onSurface: true })
|
|
180
|
+
assert.deepEqual(back.surface, { around: 315, along: { mm: 600 } })
|
|
181
|
+
const turned = fastenChanged(r, 'tag', back, { surface: { around: 90, along: 'max' } })
|
|
182
|
+
assert.deepEqual((fastenPlanOf(r, 'tag', turned).action as { to: unknown }).to, { part: 'drum', surface: { around: 90, along: 'max' } })
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
test("pressed on the canvas: the face or the point pressed, read in that part's own frame; itself and a curve with no fastening said (ADR-0140 stage 4)", async () => {
|
|
186
|
+
const { addFigurePrimitive } = await import('@hatiolab/figure-model')
|
|
187
|
+
/* A drum lying along X (turned a quarter about Z), a rounded box, and the tag to fasten. */
|
|
188
|
+
let a = addFigurePrimitive(createFigureAsset('Pressed'), { id: 'drum', kind: 'cylinder', dimensions: { radius: 100, length: 400 }, position: { x: 0, y: 500, z: 0 }, rotation: { x: 0, y: 0, z: 90 } } as never)
|
|
189
|
+
a = addFigurePrimitive(a, { id: 'soft', kind: 'rounded-box', dimensions: { width: 300, height: 200, depth: 300, round: 40 }, position: { x: 0, y: 100, z: 1500 } } as never)
|
|
190
|
+
const r = revisionOf(box(a, 'tag', [40, 40, 40], [900, 900, 900]))
|
|
191
|
+
const pose = (r.geometry as Array<{ id: string; pose: { r: number[][]; t: number[] } }>).find(g => g.id === 'drum')!.pose
|
|
192
|
+
const world = (v: number[], move = true) => [0, 1, 2].map(i => [0, 1, 2].reduce((s, c) => s + pose.r[i]![c]! * v[c]!, move ? pose.t[i]! : 0))
|
|
193
|
+
const rad = (d: number) => (d * Math.PI) / 180
|
|
194
|
+
const at = (deg: number) => [Math.cos(rad(deg)), 0, -Math.sin(rad(deg))]
|
|
195
|
+
/* A 12-sided drawing: the facet between 0° and 30°, pressed where the ray at 20° meets it, 50 up its own Y. */
|
|
196
|
+
const s = (100 * Math.cos(rad(15))) / Math.cos(rad(5))
|
|
197
|
+
const press = { id: 'drum', point: world(at(20).map((v, i) => (i === 1 ? 50 : v * s))), normal: world(at(15), false) }
|
|
198
|
+
const side = aimFastenAt(r, 'tag', freshFasten(), press)
|
|
199
|
+
assert.deepEqual([side.note, side.form?.target, side.form?.surface], [undefined, 'drum', { around: 20, along: { mm: 50 } }])
|
|
200
|
+
const end = aimFastenAt(r, 'tag', freshFasten(), { id: 'drum', point: world([10, 200, 20]), normal: world([0, 1, 0], false) })
|
|
201
|
+
assert.deepEqual([end.form?.surface, end.form?.targetFace, end.form?.align, end.form?.alignMm], [undefined, 'top', { x: 'mm', z: 'mm' }, { x: 10, z: 20 }], "its end, wherever the drum's own Y lies, its middle where pressed")
|
|
202
|
+
assert.deepEqual(aimFastenAt(r, 'tag', freshFasten(), { id: 'tag', point: [900, 920, 900], normal: [0, 1, 0] }), { note: 'aim-self', part: 'tag' })
|
|
203
|
+
const corner = aimFastenAt(r, 'tag', freshFasten(), { id: 'soft', point: [145, 100, 1645], normal: [Math.SQRT1_2, 0, Math.SQRT1_2] })
|
|
204
|
+
assert.deepEqual([corner.note, corner.form?.target], ['aim-not-here', 'soft'], 'a rounded corner: aimed at the part, said')
|
|
205
|
+
})
|
package/test/gizmo.test.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { fileURLToPath } from 'node:url'
|
|
|
11
11
|
|
|
12
12
|
import { addFigureBox, addFigurePrimitive, applyFigureAuthoring, compileFigureAsset, createFigureAsset, createFigureEditorWorkspace } from '@hatiolab/figure-model'
|
|
13
13
|
|
|
14
|
-
import { gizmoChanges, gizmoReachOf } from '../client/modeller/gizmo.ts'
|
|
14
|
+
import { gizmoChanges, gizmoReachOf, sameMeasureAxesOf } from '../client/modeller/gizmo.ts'
|
|
15
15
|
import { moving } from './joints.ts'
|
|
16
16
|
|
|
17
17
|
const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
|
|
@@ -127,33 +127,31 @@ test('원기둥 · 구도 크기 기즈모가 먹는다 — 단면은 원으로'
|
|
|
127
127
|
})
|
|
128
128
|
|
|
129
129
|
/*
|
|
130
|
-
*
|
|
131
|
-
* (
|
|
132
|
-
*
|
|
130
|
+
* 크기는 커널의 한 함수로 바뀐다(ADR-0140 4.1 · 4.2, `figureSizeEditOf`): 숫자 칸은 부품의 가운데를, 손잡이는 잡은
|
|
131
|
+
* 쪽의 반대편을 그 자리에 둔다 — 배치(바닥 · 천장)와 상관없이, 누운 원기둥이든 서 있는 기둥이든. 같은 치수 · 같은
|
|
132
|
+
* 기준이면 칸과 손잡이가 같은 결과다.
|
|
133
133
|
*/
|
|
134
|
-
test('크기
|
|
135
|
-
const {
|
|
134
|
+
test('크기 칸은 가운데를, 손잡이는 반대쪽을 — 배치와 상관없이 커널의 한 함수로', async () => {
|
|
135
|
+
const { figureSizeEditOf, applyFigureAuthoring } = await import('@hatiolab/figure-model')
|
|
136
136
|
let asset = createFigureAsset('Post')
|
|
137
137
|
asset = addFigureBox(asset, { id: 'post', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 } })
|
|
138
|
-
asset = addFigurePrimitive(asset, { id: 'drum', kind: 'cylinder', dimensions: { radius: 50, length: 100 }, position: { x: 0, y: 300, z: 0 } })
|
|
139
138
|
asset = addFigurePrimitive(asset, { id: 'roller', kind: 'cylinder', dimensions: { radius: 30, length: 500 }, position: { x: 0, y: 30, z: 0 }, rotation: { x: 90, y: 0, z: 0 } })
|
|
140
139
|
const centreY = (a: typeof asset, part: string) => {
|
|
141
140
|
const g = compileFigureAsset(a).evaluate().geometry.find(o => o.id === part)! as unknown as { pose: { t: number[] } }
|
|
142
141
|
return g.pose.t[1]!
|
|
143
142
|
}
|
|
144
143
|
const set = (input: string, value: number) => ({ kind: 'set-design', input, unit: 'mm', value }) as never
|
|
145
|
-
const grow = (a: typeof asset, part: string, input: string, value: number) => apply(a,
|
|
144
|
+
const grow = (a: typeof asset, part: string, input: string, value: number, keep: never) => apply(a, figureSizeEditOf(a, part, [set(input, value)], keep).changes)
|
|
146
145
|
const postH = gizmoReachOf(asset, 'post').sizes![1]
|
|
147
|
-
assert.equal(centreY(grow(asset, 'post', postH, 2200
|
|
148
|
-
|
|
149
|
-
assert.equal(centreY(grow(asset, '
|
|
146
|
+
assert.equal(centreY(grow(asset, 'post', postH, 2200, { keep: 'centre' } as never), 'post'), 50, '숫자 칸: 가운데가 그 자리')
|
|
147
|
+
assert.equal(centreY(grow(asset, 'post', postH, 2200, { keep: 'opposite', axis: 'y', side: 1 } as never), 'post') - 2200 / 2, 0, '위 손잡이: 아랫면이 그 자리')
|
|
148
|
+
assert.equal(centreY(grow(asset, 'post', postH, 2200, { keep: 'opposite', axis: 'y', side: -1 } as never), 'post') + 2200 / 2, 100, '아래 손잡이: 윗면이 그 자리')
|
|
150
149
|
const rollerLength = gizmoReachOf(asset, 'roller').round!.along[0]!
|
|
151
|
-
assert.deepEqual(
|
|
152
|
-
|
|
153
|
-
/* 천장 도형이면 윗면을 지킨다 — 전에는 여기서도 아랫면을 지켜 천장을 뚫고 올라갔다. */
|
|
150
|
+
assert.deepEqual(figureSizeEditOf(asset, 'roller', [set(rollerLength, 800)], { keep: 'centre' }).changes, [set(rollerLength, 800)], '누운 롤러의 길이: 가운데가 원점이라 자리는 그대로')
|
|
151
|
+
/* 천장 도형도 같다 — 배치가 지키는 면을 따로 두지 않는다(ADR-0140 4.1 이 ADR-0097 §4.3 을 대신함). */
|
|
154
152
|
let hung = addFigureBox(createFigureAsset('Lamp'), { id: 'lamp', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: -50, z: 0 } })
|
|
155
|
-
hung = applyFigureAuthoring(hung, { kind: '
|
|
156
|
-
assert.equal(centreY(grow(hung, 'lamp', gizmoReachOf(hung, 'lamp').sizes![1], 300
|
|
153
|
+
hung = applyFigureAuthoring(hung, { kind: 'set-placement', placement: 'ceiling' })
|
|
154
|
+
assert.equal(centreY(grow(hung, 'lamp', gizmoReachOf(hung, 'lamp').sizes![1], 300, { keep: 'centre' } as never), 'lamp'), -50, '천장 도형: 숫자 칸도 가운데')
|
|
157
155
|
})
|
|
158
156
|
|
|
159
157
|
/* 손잡이는 키를 몰라도 찾는다 — 도구 막대에 옮기기 · 돌리기 · 크기, 제 숫자로 놓인 부품에만 (09-26). */
|
|
@@ -161,3 +159,14 @@ test('도구 막대의 옮기기 · 돌리기 · 크기 — 키와 같은 길,
|
|
|
161
159
|
const ko = JSON.parse(read('../translations/ko.json'))
|
|
162
160
|
assert.deepEqual(['translate', 'rotate', 'scale'].map(m => ko['figure.button.gizmo-' + m]), ['옮기기', '돌리기', '크기 바꾸기'])
|
|
163
161
|
})
|
|
162
|
+
|
|
163
|
+
/* ADR-0140 4.1: the size handles that change one measure are said while the size tool is on. */
|
|
164
|
+
test('같은 치수 — 원기둥의 X · Z 핸들은 한 지름, 구는 셋이 하나, 상자는 따로', () => {
|
|
165
|
+
const at = { x: 0, y: 0, z: 0 }
|
|
166
|
+
const drum = addFigurePrimitive(createFigureAsset('Drum'), { id: 'drum', kind: 'cylinder', dimensions: { radius: 50, length: 200 }, position: at })
|
|
167
|
+
const ball = addFigurePrimitive(createFigureAsset('Ball'), { id: 'ball', kind: 'sphere', dimensions: { radius: 50, radiusY: 50 }, position: at })
|
|
168
|
+
const crate = addFigureBox(createFigureAsset('Crate'), { id: 'crate', size: { x: 100, y: 100, z: 100 }, position: at })
|
|
169
|
+
assert.deepEqual(sameMeasureAxesOf(gizmoReachOf(drum, 'drum')), ['x', 'z'])
|
|
170
|
+
assert.deepEqual(sameMeasureAxesOf(gizmoReachOf(ball, 'ball')), ['x', 'y', 'z'])
|
|
171
|
+
assert.equal(sameMeasureAxesOf(gizmoReachOf(crate, 'crate')), undefined)
|
|
172
|
+
})
|
package/test/keys.test.ts
CHANGED
|
@@ -41,3 +41,14 @@ test('캔버스에서 누른 W · E · R 은 씬이 이미 받았다 — 또 넘
|
|
|
41
41
|
for (const key of ['w', 'e', 'r']) assert.equal(keyActionOf({ key, inScene: true }, 'arm'), null, key)
|
|
42
42
|
assert.equal(keyActionOf({ key: 'Delete', inScene: true }, 'arm'), 'remove', '다른 키는 그대로')
|
|
43
43
|
})
|
|
44
|
+
|
|
45
|
+
/* Ctrl+C · Ctrl+V are the modeller's: the scene's copied the canvas object with the part's name (user 2026-10-21). */
|
|
46
|
+
test('복사 · 붙여넣기는 모델러의 것 — 고른 부품의 Ctrl+C 는 복사, Ctrl+V 는 붙여넣기, 씬의 잘라내기 · 여러 개 붙여넣기는 막는다', () => {
|
|
47
|
+
assert.equal(keyActionOf({ key: 'c', ctrlKey: true }, 'arm'), 'copy')
|
|
48
|
+
assert.equal(keyActionOf({ key: 'C', metaKey: true }, 'arm'), 'copy')
|
|
49
|
+
assert.equal(keyActionOf({ key: 'c', ctrlKey: true }, ''), 'hold', '고른 것이 없어도 씬에 넘기지 않는다')
|
|
50
|
+
assert.equal(keyActionOf({ key: 'v', ctrlKey: true }, ''), 'paste', '고른 것이 없어도 붙여넣기')
|
|
51
|
+
assert.equal(keyActionOf({ key: 'V', ctrlKey: true, shiftKey: true }, 'arm'), 'hold')
|
|
52
|
+
assert.equal(keyActionOf({ key: 'x', ctrlKey: true }, 'arm'), 'hold')
|
|
53
|
+
assert.equal(keyActionOf({ key: 'v', ctrlKey: true, target: { tagName: 'INPUT' } }, ''), null, '칸 안의 붙여넣기는 칸의 것')
|
|
54
|
+
})
|