@things-factory/figure-ui 10.1.181 → 10.1.185
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/editor-styles.ts +28 -9
- package/client/modeller/editor.ts +105 -12
- package/client/modeller/fasten-form.ts +207 -0
- package/client/modeller/fasten-geometry.ts +5 -1
- package/client/modeller/figure-facts.ts +4 -35
- package/client/modeller/figure-fasten-dialog.ts +457 -0
- package/client/modeller/figure-fasten-drawing.ts +32 -11
- package/client/modeller/figure-links.ts +56 -300
- package/client/modeller/figure-properties.ts +23 -70
- package/client/modeller/modeller-keys.ts +4 -4
- package/dist-client/modeller/clips.d.ts +2 -2
- package/dist-client/modeller/editor-styles.js +28 -9
- package/dist-client/modeller/editor-styles.js.map +1 -1
- package/dist-client/modeller/editor.d.ts +23 -2
- package/dist-client/modeller/editor.js +121 -12
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/fasten-form.d.ts +69 -0
- package/dist-client/modeller/fasten-form.js +131 -0
- package/dist-client/modeller/fasten-form.js.map +1 -0
- package/dist-client/modeller/fasten-geometry.d.ts +3 -1
- package/dist-client/modeller/fasten-geometry.js +6 -1
- package/dist-client/modeller/fasten-geometry.js.map +1 -1
- package/dist-client/modeller/figure-facts.d.ts +2 -13
- package/dist-client/modeller/figure-facts.js +2 -29
- package/dist-client/modeller/figure-facts.js.map +1 -1
- package/dist-client/modeller/figure-fasten-dialog.d.ts +69 -0
- package/dist-client/modeller/figure-fasten-dialog.js +433 -0
- package/dist-client/modeller/figure-fasten-dialog.js.map +1 -0
- package/dist-client/modeller/figure-fasten-drawing.d.ts +6 -3
- package/dist-client/modeller/figure-fasten-drawing.js +36 -10
- package/dist-client/modeller/figure-fasten-drawing.js.map +1 -1
- package/dist-client/modeller/figure-links.d.ts +14 -38
- package/dist-client/modeller/figure-links.js +58 -301
- package/dist-client/modeller/figure-links.js.map +1 -1
- package/dist-client/modeller/figure-properties.d.ts +12 -12
- package/dist-client/modeller/figure-properties.js +23 -72
- package/dist-client/modeller/figure-properties.js.map +1 -1
- package/dist-client/modeller/modeller-keys.js +4 -4
- package/dist-client/modeller/modeller-keys.js.map +1 -1
- package/dist-client/route.d.ts +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +8 -8
- package/test/browser/links-page.js +4 -4
- package/test/browser/links.check.mjs +196 -46
- package/test/browser/modeller-page.js +23 -0
- package/test/browser/modeller.check.mjs +19 -20
- package/test/browser/namespace-page.js +2 -29
- package/test/browser/popup-stand-in.js +48 -0
- package/test/browser/product-page.check.mjs +60 -9
- package/test/browser/product-page.js +30 -33
- package/test/browser/property-section.check.mjs +3 -3
- package/test/browser/props.check.mjs +28 -7
- package/test/browser/screen-page.js +7 -2
- package/test/browser/screen.check.mjs +28 -23
- package/test/browser/section-page.js +13 -41
- package/test/browser/section.check.mjs +39 -30
- package/test/fasten-form.test.ts +77 -0
- package/test/fasten-geometry.test.ts +2 -0
- package/test/first-minute.test.ts +7 -4
- package/test/properties.test.ts +11 -19
- package/translations/en.json +5 -12
- package/translations/es.json +5 -12
- package/translations/ja.json +5 -12
- package/translations/ko.json +5 -12
- package/translations/ms.json +5 -12
- package/translations/zh.json +5 -12
- package/client/modeller/mounting-part.ts +0 -15
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@things-factory/figure-ui",
|
|
3
|
-
"version": "10.1.
|
|
3
|
+
"version": "10.1.185",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public",
|
|
6
6
|
"@things-factory:registry": "https://registry.npmjs.org"
|
|
@@ -28,12 +28,12 @@
|
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@hatiolab/figure-model": "^0.7.2",
|
|
31
|
-
"@hatiolab/things-scene": "^10.1.
|
|
31
|
+
"@hatiolab/things-scene": "^10.1.92",
|
|
32
32
|
"@material/web": "^2.0.0",
|
|
33
33
|
"@operato/data-grist": "^10.29.39",
|
|
34
34
|
"@operato/graphql": "^10.29.39",
|
|
35
35
|
"@operato/i18n": "^10.0.0",
|
|
36
|
-
"@operato/input": "^10.29.
|
|
36
|
+
"@operato/input": "^10.29.41",
|
|
37
37
|
"@operato/layout": "^10.29.39",
|
|
38
38
|
"@operato/markdown": "^10.23.0",
|
|
39
39
|
"@operato/p13n": "^10.29.39",
|
|
@@ -42,11 +42,11 @@
|
|
|
42
42
|
"@operato/slot-layout-editor": "^10.29.39",
|
|
43
43
|
"@operato/styles": "^10.0.0",
|
|
44
44
|
"@operato/utils": "^10.29.39",
|
|
45
|
-
"@things-factory/ai-assistant": "^10.1.
|
|
46
|
-
"@things-factory/figure-service": "^10.1.
|
|
47
|
-
"@things-factory/figure-viewer": "^10.1.
|
|
48
|
-
"@things-factory/setting-base": "^10.1.
|
|
45
|
+
"@things-factory/ai-assistant": "^10.1.183",
|
|
46
|
+
"@things-factory/figure-service": "^10.1.183",
|
|
47
|
+
"@things-factory/figure-viewer": "^10.1.185",
|
|
48
|
+
"@things-factory/setting-base": "^10.1.183",
|
|
49
49
|
"three": "^0.185.1"
|
|
50
50
|
},
|
|
51
|
-
"gitHead": "
|
|
51
|
+
"gitHead": "3cfe5c959e7e82d772f46fd280037900fa89fcaa"
|
|
52
52
|
}
|
|
@@ -68,12 +68,12 @@ window.look = {
|
|
|
68
68
|
},
|
|
69
69
|
/**
|
|
70
70
|
* From now on, keep what every event of this type carries: anywhere on the page, or -- `section` given -- sent by that
|
|
71
|
-
* part panel to the editor, which it still is once
|
|
71
|
+
* part panel (or the fastening window: `dialog`) to the editor, which it still is once it has left the page.
|
|
72
72
|
*/
|
|
73
73
|
hear(type, section) {
|
|
74
74
|
const key = section ? `${section}:${type}` : type
|
|
75
75
|
const keep = e => heard[key].push(e.detail === undefined ? null : JSON.parse(JSON.stringify(e.detail)))
|
|
76
|
-
if (section) deep('figure-links').find(l => l.section === section).addEventListener(type, keep)
|
|
76
|
+
if (section) (section === 'dialog' ? deep('figure-fasten-dialog')[0] : deep('figure-links').find(l => l.section === section)).addEventListener(type, keep)
|
|
77
77
|
else if (!heard[key]) document.addEventListener(type, keep, true)
|
|
78
78
|
heard[key] = []
|
|
79
79
|
},
|
|
@@ -102,9 +102,9 @@ window.look = {
|
|
|
102
102
|
const small = label?.querySelector('small[fastened-to]')
|
|
103
103
|
return small ? words(small) : null
|
|
104
104
|
},
|
|
105
|
-
/** An event a part's panel (`section`: shape, places or motion) sends the editor, as
|
|
105
|
+
/** An event a part's panel (`section`: shape, places or motion) or the fastening window (`dialog`) sends the editor, as it would send it. */
|
|
106
106
|
tell(section, type, detail) {
|
|
107
|
-
const panel = deep('figure-links').find(l => l.section === section)
|
|
107
|
+
const panel = section === 'dialog' ? deep('figure-fasten-dialog')[0] : deep('figure-links').find(l => l.section === section)
|
|
108
108
|
panel.dispatchEvent(new CustomEvent(type, { detail, bubbles: true, composed: true }))
|
|
109
109
|
},
|
|
110
110
|
/** What the canvas tells the editor when a drag on a part ends. */
|
|
@@ -90,11 +90,19 @@ async function pick(part) {
|
|
|
90
90
|
await until(id => probe.state().selected === id, [part], `${part} selected`)
|
|
91
91
|
}
|
|
92
92
|
const tab = name => read(l => probe.tab(l), `figure.label.tab-short-${name}`).then(point => click(point, `the ${name} tab`))
|
|
93
|
-
/** A fastening the part has is said in one line; 「바꾸기」 opens
|
|
93
|
+
/** A fastening the part has is said in one line; 「바꾸기」 opens the fastening window on it. */
|
|
94
94
|
async function changeFastening() {
|
|
95
95
|
await until(() => look.all('button[change-fastening]').length === 1, [], 'the fastening said in one line')
|
|
96
96
|
await (await handle('button[change-fastening]')).click()
|
|
97
|
-
await until(() =>
|
|
97
|
+
await until(() => !!probe.fastenTargets(), [], 'the fastening window open')
|
|
98
|
+
}
|
|
99
|
+
/** The window's list: each row's part and words, whether chosen, whether it can be. Null while the window is closed. */
|
|
100
|
+
const targets = () => read(() => probe.fastenTargets())
|
|
101
|
+
const windowOpen = () => read(() => !!probe.fastenTargets())
|
|
102
|
+
/** A row of the window's list pressed, as a hand does, and waited for as the one chosen. */
|
|
103
|
+
async function aimAt(part) {
|
|
104
|
+
await click(await read(p => probe.fastenTarget(p), part), `the row of ${part} in the window`)
|
|
105
|
+
await until(p => probe.fastenTargets()?.some(t => t.target === p && t.selected), [part], `${part} chosen in the window`)
|
|
98
106
|
}
|
|
99
107
|
/** An element handle found through every shadow root. */
|
|
100
108
|
const handle = (selector, test) =>
|
|
@@ -348,36 +356,50 @@ describe('a position worked out from the figure size', () => {
|
|
|
348
356
|
})
|
|
349
357
|
})
|
|
350
358
|
|
|
351
|
-
describe('the fastening
|
|
359
|
+
describe('the fastening window on the robot arm', () => {
|
|
352
360
|
test('the upper arm opens on what it is fastened to; a part hanging from it is listed, not choosable, with the reason', async () => {
|
|
353
361
|
await mount('arm')
|
|
354
362
|
await pick('upper-arm')
|
|
355
363
|
await changeFastening()
|
|
356
364
|
const own = (await source()).joints.find(j => j.body1.part === 'upper-arm').body0.part
|
|
357
|
-
const
|
|
358
|
-
assert.
|
|
359
|
-
const forearm =
|
|
360
|
-
assert.deepEqual(forearm, {
|
|
365
|
+
const list = await targets()
|
|
366
|
+
assert.deepEqual(list.filter(t => t.selected).map(t => t.target), [own], 'its own fastening')
|
|
367
|
+
const forearm = list.find(t => t.target === 'forearm')
|
|
368
|
+
assert.deepEqual(forearm, { target: 'forearm', text: 'figure.text.target-would-loop', selected: false, disabled: true })
|
|
369
|
+
})
|
|
370
|
+
|
|
371
|
+
test('the parts hanging from it are said in one line, and the chips show the part names as they are written', async () => {
|
|
372
|
+
const said = await read(() => {
|
|
373
|
+
const root = document.querySelector('figure-fasten-dialog').shadowRoot
|
|
374
|
+
const chip = root.querySelector('ox-input-select-buttons').shadowRoot.querySelector('md-filter-chip')
|
|
375
|
+
const label = chip.shadowRoot.querySelector('.label-text') ?? chip.shadowRoot.querySelector('.label')
|
|
376
|
+
return { lines: root.querySelectorAll('p[loops]').length, transform: getComputedStyle(label).textTransform }
|
|
377
|
+
})
|
|
378
|
+
const hanging = (await targets()).filter(t => t.disabled).map(t => t.target)
|
|
379
|
+
assert.ok(hanging.length > 1, `several parts hang from the upper arm: ${hanging}`)
|
|
380
|
+
assert.deepEqual(said, { lines: 1, transform: 'none' })
|
|
361
381
|
})
|
|
362
382
|
})
|
|
363
383
|
|
|
364
|
-
describe('a new fastening for a door leaf: seen before it is done', () => {
|
|
384
|
+
describe('a new fastening for a door leaf: chosen in the window, seen before it is done', () => {
|
|
365
385
|
before(async () => {
|
|
366
386
|
await mount('door')
|
|
367
387
|
await read(() => look.hear('preview-command'))
|
|
368
388
|
})
|
|
369
389
|
const ghosts = () => read(() => look.drawn().filter(n => n.startsWith('__preview')))
|
|
370
390
|
|
|
371
|
-
test('started,
|
|
391
|
+
test('started, the window opens in the standard popup, asking what to fasten to, nothing chosen', async () => {
|
|
372
392
|
await pick('leaf')
|
|
373
|
-
await press('button[
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
assert.
|
|
393
|
+
await press('button[fasten-start]')
|
|
394
|
+
const list = await until(() => probe.fastenTargets(), [], 'the fastening window')
|
|
395
|
+
assert.deepEqual(list.filter(t => t.selected), [], 'nothing chosen')
|
|
396
|
+
assert.ok(list.some(t => t.target === 'frame' && !t.disabled), 'the frame offered')
|
|
397
|
+
assert.deepEqual((await all('[apply]')).map(b => b.disabled), [true], 'nothing to apply yet')
|
|
398
|
+
assert.deepEqual(await read(() => probe.fastenPopup()), { title: 'figure.label.fastening', movable: true, backdrop: true, role: 'dialog' })
|
|
377
399
|
})
|
|
378
400
|
|
|
379
401
|
test('the frame chosen: the editor is told the command, and the canvas draws where the leaf would go and the face it meets', async () => {
|
|
380
|
-
await
|
|
402
|
+
await aimAt('frame')
|
|
381
403
|
await until(() => look.drawn().includes('__preview:leaf') && look.drawn().includes('__target-face'), [], 'the leaf and the face drawn')
|
|
382
404
|
const told = (await heard('preview-command')).filter(Boolean).at(-1)
|
|
383
405
|
assert.equal(told?.action.kind, 'attach')
|
|
@@ -393,51 +415,49 @@ describe('a new fastening for a door leaf: seen before it is done', () => {
|
|
|
393
415
|
assert.deepEqual((await all('span[fasten-summary]')).map(s => s.text), ['figure.text.fasten-summary-gap'])
|
|
394
416
|
})
|
|
395
417
|
|
|
396
|
-
test('
|
|
397
|
-
await
|
|
398
|
-
await until(() => !look.drawn().some(n => n.startsWith('__preview')), [], 'nothing drawn on the motion tab')
|
|
399
|
-
await tab('properties')
|
|
400
|
-
await until(() => look.drawn().includes('__preview:leaf'), [], 'drawn again')
|
|
401
|
-
})
|
|
402
|
-
|
|
403
|
-
test('a refused command clears what was drawn and says why', async () => {
|
|
404
|
-
await read(() => look.tell('shape', 'preview-command', { action: { kind: 'attach', part: 'leaf', face: 'bottom', to: { part: 'frame', face: 'nowhere' } } }))
|
|
418
|
+
test('a refused command clears what was drawn and says why, in the window', async () => {
|
|
419
|
+
await read(() => look.tell('dialog', 'preview-command', { action: { kind: 'attach', part: 'leaf', face: 'bottom', to: { part: 'frame', face: 'nowhere' } } }))
|
|
405
420
|
await until(() => look.all('[preview-words]', 'figure.text.preview-refused').length > 0, [], 'the refusal said')
|
|
406
421
|
assert.deepEqual(await ghosts(), [], 'nothing of the earlier result drawn')
|
|
407
422
|
})
|
|
408
423
|
|
|
409
|
-
test('the
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
await
|
|
413
|
-
await until(() => probe.
|
|
414
|
-
|
|
424
|
+
test('a press outside the window closes it with nothing written, and nothing it tried is drawn', async () => {
|
|
425
|
+
const start = await state()
|
|
426
|
+
/* The motion tab, under the popup's backdrop: the press closes the popup, it does not reach the tab. */
|
|
427
|
+
await click(await read(l => probe.tab(l), 'figure.label.tab-short-motion'), 'outside the window')
|
|
428
|
+
await until(() => !probe.fastenTargets(), [], 'the window closed')
|
|
429
|
+
await until(() => !look.drawn().some(n => n.startsWith('__preview')), [], 'nothing it tried drawn')
|
|
430
|
+
assert.equal(await read(() => look.tab()), 'figure.label.tab-short-properties', 'the press did not reach the tab')
|
|
431
|
+
assert.equal((await state()).revision, start.revision, 'nothing written')
|
|
415
432
|
})
|
|
416
433
|
|
|
417
|
-
test('"fasten it first" from the motion tab opens this part\'s
|
|
418
|
-
await pick('leaf')
|
|
419
|
-
await press('button[act]', 'figure.button.fasten-start')
|
|
420
|
-
await choose('figure.label.target-part', 'frame')
|
|
421
|
-
/* The panel scrolled back to its head, the fastening below the fold. */
|
|
422
|
-
const panel = await at('section[fastening]')
|
|
423
|
-
await page().mouse.move(panel.x, panel.y)
|
|
424
|
-
await page().mouse.wheel({ deltaY: -5000 })
|
|
425
|
-
await until(() => !look.inView('section[fastening]'), [], 'the fastening section scrolled out of view')
|
|
434
|
+
test('"fasten it first" from the motion tab opens this part\'s window, on what it has, on the properties tab', async () => {
|
|
426
435
|
await tab('motion')
|
|
427
436
|
await click(await at('button[go-fasten]'), 'go-fasten')
|
|
428
|
-
await until(() =>
|
|
437
|
+
const list = await until(() => probe.fastenTargets(), [], 'the window open')
|
|
429
438
|
assert.equal(await read(() => look.tab()), 'figure.label.tab-short-properties', 'on the properties tab, not another')
|
|
430
|
-
assert.
|
|
431
|
-
|
|
439
|
+
assert.deepEqual(list.filter(t => t.selected), [], 'the fastening it has: none')
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
test('Esc closes the window and tells the editor to clear what it tried; the leaf stays selected', async () => {
|
|
443
|
+
await aimAt('frame')
|
|
444
|
+
await until(() => look.drawn().includes('__preview:leaf'), [], 'the leaf drawn where it would go')
|
|
445
|
+
await read(() => look.hear('preview-command', 'dialog'))
|
|
446
|
+
await page().keyboard.press('Escape')
|
|
447
|
+
await until(() => !probe.fastenTargets(), [], 'the window closed')
|
|
448
|
+
assert.equal((await state()).selected, 'leaf', 'Esc was the window\'s, not the modeller\'s')
|
|
449
|
+
assert.deepEqual(await heard('preview-command', 'dialog'), [null], 'told once: nothing to show')
|
|
450
|
+
assert.deepEqual(await ghosts(), [])
|
|
432
451
|
})
|
|
433
452
|
|
|
434
|
-
test('the frame\'s front chosen in the drawing,
|
|
453
|
+
test('the frame\'s front chosen in the drawing, 붙이기: one step, the leaf keeps its place across that face', async () => {
|
|
454
|
+
await pick('leaf')
|
|
455
|
+
await press('button[fasten-start]')
|
|
435
456
|
const was = (await read(() => probe.body('leaf'))).t.map(Math.round)
|
|
436
|
-
await
|
|
457
|
+
await aimAt('frame')
|
|
437
458
|
/*
|
|
438
459
|
The frame's faces are offered once the drawing has the frame: a press before that is not a choice. And the press is
|
|
439
|
-
aimed once the
|
|
440
|
-
that move it down -- or it lands where the face was.
|
|
460
|
+
aimed once the window has drawn what the choice brings -- the leaf where it would go -- or it lands where the face was.
|
|
441
461
|
*/
|
|
442
462
|
await until(() => look.face('front')?.pickable && look.drawn().includes('__preview:leaf'), [], 'the front face offered, the leaf drawn where it would go')
|
|
443
463
|
await settled()
|
|
@@ -445,9 +465,10 @@ describe('a new fastening for a door leaf: seen before it is done', () => {
|
|
|
445
465
|
await click(await at('path[face="front"]'), 'the front face in the drawing')
|
|
446
466
|
await until(() => look.face('front').chosen, [], 'the front face drawn as the one chosen')
|
|
447
467
|
const start = await state()
|
|
448
|
-
await
|
|
468
|
+
await press('[apply]')
|
|
449
469
|
assert.equal((await state()).error, '', 'not refused')
|
|
450
470
|
await oneStep(start)
|
|
471
|
+
assert.equal(await windowOpen(), false, 'the window closed')
|
|
451
472
|
assert.equal((await source()).joints.find(j => j.body1.part === 'leaf').body0.face, 'front')
|
|
452
473
|
const now = (await read(() => probe.body('leaf'))).t.map(Math.round)
|
|
453
474
|
assert.deepEqual([now[0], now[1]], [was[0], was[1]], `left-right and height kept: was ${was}, now ${now}`)
|
|
@@ -526,6 +547,14 @@ describe('a bench: what each part is fastened to, what detaching would do, a row
|
|
|
526
547
|
await until(() => look.all('section[fastening] [detach-words]', 'figure.text.detach-keeps-children').length === 1, [], 'said under the fastening')
|
|
527
548
|
})
|
|
528
549
|
|
|
550
|
+
test('the part let go of, the panel leaving tells the editor to clear the face it lit', async () => {
|
|
551
|
+
await read(() => look.hear('preview-command', 'shape'))
|
|
552
|
+
await page().keyboard.press('Escape')
|
|
553
|
+
await until(() => probe.state().selected === '', [], 'nothing selected')
|
|
554
|
+
assert.deepEqual(await heard('preview-command', 'shape'), [null], 'told once: nothing to show')
|
|
555
|
+
await until(() => !look.drawn().includes('__target-face'), [], 'the face no longer lit')
|
|
556
|
+
})
|
|
557
|
+
|
|
529
558
|
test('a part that would stay where it stands: nothing is said under its fastening', async () => {
|
|
530
559
|
await pick('lamp')
|
|
531
560
|
/* Taking it off is the place group's 「⋯」 (chief architect 2026-10-16). */
|
|
@@ -639,6 +668,127 @@ async function poll(ask, ms = 5000) {
|
|
|
639
668
|
}
|
|
640
669
|
}
|
|
641
670
|
|
|
671
|
+
/*
|
|
672
|
+
* The fastening window writes once (user 2026-10-17): whatever is tried in it is a preview, 「적용」 is one step of the
|
|
673
|
+
* history with all of it, and 「취소」 or Esc leave the history as it was.
|
|
674
|
+
*/
|
|
675
|
+
describe('the fastening window: tried freely, written once', () => {
|
|
676
|
+
before(async () => {
|
|
677
|
+
let a = fasten(box(createFigureAsset('Bench'), 'leg', [60, 700, 60], [-400, 350, 0]), 'leg', 'plane')
|
|
678
|
+
a = fasten(box(a, 'top', [1200, 40, 600], [0, 900, 0]), 'top', 'leg')
|
|
679
|
+
a = fasten(box(a, 'lamp', [80, 80, 80], [200, 960, 100]), 'lamp', 'top')
|
|
680
|
+
await mount('bench-window', box(a, 'loose', [100, 100, 100], [800, 50, 0]))
|
|
681
|
+
})
|
|
682
|
+
const lamp = async () => (await source()).joints.find(j => j.body1.part === 'lamp')
|
|
683
|
+
const lampTold = () => read(() => look.heard('preview-command').filter(Boolean).at(-1)?.action ?? null)
|
|
684
|
+
/* Flush, a gap of 40 and the top's front: three choices, each drawn on the canvas, none written. */
|
|
685
|
+
async function tryThree(start) {
|
|
686
|
+
await read(() => look.hear('preview-command'))
|
|
687
|
+
await press('button[mode="flush"]')
|
|
688
|
+
await click(await at('details[fasten-detail] > summary'), 'the details')
|
|
689
|
+
await type(await handle('details[fasten-detail] input', 'figure.label.gap'), '40')
|
|
690
|
+
await until(() => look.face('front')?.pickable, [], 'the front face offered')
|
|
691
|
+
await click(await at('path[face="front"]'), 'the front face in the drawing')
|
|
692
|
+
await until(() => look.heard('preview-command').filter(Boolean).at(-1)?.action.to?.face === 'front', [], 'the three told')
|
|
693
|
+
const told = await lampTold()
|
|
694
|
+
assert.deepEqual([told.facing, told.gap, told.to], ['flush', 40, { part: 'top', face: 'front' }], 'all three in the command tried')
|
|
695
|
+
assert.equal((await state()).revision, start.revision, 'nothing written while trying')
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
test('three choices tried, then 취소: nothing written, the window closed, nothing of them drawn', async () => {
|
|
699
|
+
await pick('lamp')
|
|
700
|
+
const was = await lamp()
|
|
701
|
+
await changeFastening()
|
|
702
|
+
const start = await state()
|
|
703
|
+
await tryThree(start)
|
|
704
|
+
await press('[cancel]')
|
|
705
|
+
await until(() => !probe.fastenTargets(), [], 'the window closed')
|
|
706
|
+
await settled()
|
|
707
|
+
await sleep(300)
|
|
708
|
+
assert.equal((await state()).revision, start.revision, 'the history as it was')
|
|
709
|
+
assert.deepEqual(await lamp(), was, 'the fastening as it was')
|
|
710
|
+
await until(() => !look.drawn().some(n => n.startsWith('__preview')), [], 'nothing tried drawn')
|
|
711
|
+
})
|
|
712
|
+
|
|
713
|
+
test('Esc closes it the same way, and leaves the part selected', async () => {
|
|
714
|
+
await pick('lamp')
|
|
715
|
+
const was = await lamp()
|
|
716
|
+
await changeFastening()
|
|
717
|
+
const start = await state()
|
|
718
|
+
await press('button[mode="flush"]')
|
|
719
|
+
await page().keyboard.press('Escape')
|
|
720
|
+
await until(() => !probe.fastenTargets(), [], 'the window closed')
|
|
721
|
+
await sleep(300)
|
|
722
|
+
const now = await state()
|
|
723
|
+
assert.deepEqual([now.revision, now.selected], [start.revision, 'lamp'], 'nothing written, the lamp still selected')
|
|
724
|
+
assert.deepEqual(await lamp(), was)
|
|
725
|
+
})
|
|
726
|
+
|
|
727
|
+
test('three choices and one 적용: one step holding all three; one undo takes all three back', async () => {
|
|
728
|
+
await pick('lamp')
|
|
729
|
+
const was = await lamp()
|
|
730
|
+
await changeFastening()
|
|
731
|
+
const start = await state()
|
|
732
|
+
await tryThree(start)
|
|
733
|
+
await press('[apply]')
|
|
734
|
+
await oneStep(start)
|
|
735
|
+
assert.equal(await windowOpen(), false, 'the window closed')
|
|
736
|
+
const now = await lamp()
|
|
737
|
+
assert.deepEqual([now.body0.face, now.origin?.facing, now.origin?.gap], ['front', 'flush', 40], JSON.stringify(now))
|
|
738
|
+
await page().keyboard.down('Control')
|
|
739
|
+
await page().keyboard.press('z')
|
|
740
|
+
await page().keyboard.up('Control')
|
|
741
|
+
await until(w => JSON.stringify(JSON.parse(document.querySelector('figure-editor').session.source()).joints.find(j => j.body1.part === 'lamp')) === w, [JSON.stringify(was)], 'one undo: the fastening as it was')
|
|
742
|
+
})
|
|
743
|
+
|
|
744
|
+
test('moved by the popup\'s head out of the way of the canvas: nothing written, still open', async () => {
|
|
745
|
+
await pick('lamp')
|
|
746
|
+
await changeFastening()
|
|
747
|
+
const rect = () => read(() => {
|
|
748
|
+
const r = document.querySelector('figure-fasten-dialog')?.getBoundingClientRect()
|
|
749
|
+
return r ? { x: Math.round(r.left), y: Math.round(r.top) } : null
|
|
750
|
+
})
|
|
751
|
+
const was = await rect()
|
|
752
|
+
const head = await read(() => {
|
|
753
|
+
const h = document.querySelector('figure-fasten-dialog').closest('ox-floating-overlay').shadowRoot.querySelector('[header] h1').getBoundingClientRect()
|
|
754
|
+
return { x: h.left + h.width / 2, y: h.top + h.height / 2 }
|
|
755
|
+
})
|
|
756
|
+
const start = await state()
|
|
757
|
+
await page().mouse.move(head.x, head.y)
|
|
758
|
+
await page().mouse.down()
|
|
759
|
+
for (let i = 1; i <= 6; i++) await page().mouse.move(head.x - 30 * i, head.y + 20 * i)
|
|
760
|
+
await page().mouse.up()
|
|
761
|
+
const moved = await rect()
|
|
762
|
+
assert.deepEqual([moved.x - was.x, moved.y - was.y], [-180, 120], `moved with the pointer: ${JSON.stringify([was, moved])}`)
|
|
763
|
+
assert.equal(await windowOpen(), true, 'still open')
|
|
764
|
+
assert.equal((await state()).revision, start.revision, 'nothing written')
|
|
765
|
+
await press('[cancel]')
|
|
766
|
+
await until(() => !probe.fastenTargets(), [], 'the window closed')
|
|
767
|
+
})
|
|
768
|
+
|
|
769
|
+
test('the keys: the arrows move among the chips and light that part on the canvas, the pointer lights another, Space chooses, Enter applies', async () => {
|
|
770
|
+
await pick('lamp')
|
|
771
|
+
await changeFastening()
|
|
772
|
+
const litAt = y => Math.abs((document.querySelector('figure-editor').shadowRoot.querySelector('figure-canvas').highlight?.pose.t[1] ?? 0) - y) < 2
|
|
773
|
+
/* The list has the focus, on the top it is fastened to. */
|
|
774
|
+
await until(() => document.querySelector('figure-fasten-dialog')?.shadowRoot.querySelector('ox-input-select-buttons')?.shadowRoot.activeElement?.getAttribute('data-value') === 'top', [], 'the focus on the chosen chip')
|
|
775
|
+
await page().keyboard.press('ArrowLeft')
|
|
776
|
+
/* The leg's top, 700 high (the lit plate a little over it); moving the focus lights, it does not choose. */
|
|
777
|
+
await until(litAt, [702], 'the leg\'s top lit')
|
|
778
|
+
assert.equal((await targets()).find(t => t.selected)?.target, 'top', 'still the top chosen')
|
|
779
|
+
const loose = await read(() => probe.fastenTarget('loose'))
|
|
780
|
+
await page().mouse.move(loose.x, loose.y)
|
|
781
|
+
await until(litAt, [102], 'the loose box\'s top lit under the pointer')
|
|
782
|
+
await page().keyboard.press('Space')
|
|
783
|
+
await until(() => probe.fastenTargets().find(t => t.selected)?.target === 'leg', [], 'the leg chosen by Space')
|
|
784
|
+
const start = await state()
|
|
785
|
+
await page().keyboard.press('Enter')
|
|
786
|
+
await oneStep(start)
|
|
787
|
+
assert.equal((await lamp()).body0.part, 'leg', 'fastened to what was chosen')
|
|
788
|
+
assert.equal(await windowOpen(), false)
|
|
789
|
+
})
|
|
790
|
+
})
|
|
791
|
+
|
|
642
792
|
describe('a motion on the motion tab', () => {
|
|
643
793
|
test('named by its parts, and drawn at both ends of its range', async () => {
|
|
644
794
|
await mount('arm')
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* `client/` by the runner.
|
|
5
5
|
*/
|
|
6
6
|
import 'figure-ui-client/modeller/editor.js'
|
|
7
|
+
import './popup-stand-in.js'
|
|
7
8
|
import { budgetOf, figureReportOf } from 'figure-ui-client/modeller/maturity.js'
|
|
8
9
|
import { createFigureProductSession } from 'figure-ui-client/modeller/product-session.js'
|
|
9
10
|
import { addFigureBox, addFigurePrimitive, applyFigureAuthoring, createFigureAsset, figureWork, robotArmFigure, serializeFigureAsset } from '@hatiolab/figure-model'
|
|
@@ -200,6 +201,28 @@ window.probe = {
|
|
|
200
201
|
fastenMode() {
|
|
201
202
|
return deep('figure-fasten-drawing')[0]?.mode ?? null
|
|
202
203
|
},
|
|
204
|
+
/** The fastening window's list of what to fasten to, each row's part and words, whether chosen, whether it can be; null while closed. */
|
|
205
|
+
fastenTargets() {
|
|
206
|
+
const dialog = deep('figure-fasten-dialog')[0]
|
|
207
|
+
if (!dialog) return null
|
|
208
|
+
const root = dialog.shadowRoot
|
|
209
|
+
const chips = [...(root.querySelector('ox-input-select-buttons')?.shadowRoot?.querySelectorAll('md-filter-chip') ?? [])]
|
|
210
|
+
return [
|
|
211
|
+
...chips.map(c => ({ target: c.getAttribute('data-value'), text: c.getAttribute('label') ?? '', selected: !!c.selected, disabled: false })),
|
|
212
|
+
/* A part hanging from this one: not a chip, said why under them. */
|
|
213
|
+
...[...root.querySelectorAll('p[loops]')].flatMap(p => p.getAttribute('targets').split(',').map(target => ({ target, text: p.textContent.replace(/\s+/g, ' ').trim(), selected: false, disabled: true })))
|
|
214
|
+
]
|
|
215
|
+
},
|
|
216
|
+
/** The page point of a chip of the fastening window's list. */
|
|
217
|
+
fastenTarget(part) {
|
|
218
|
+
const chip = deep('figure-fasten-dialog')[0]?.shadowRoot.querySelector('ox-input-select-buttons')?.shadowRoot?.querySelector(`md-filter-chip[data-value="${CSS.escape(part)}"]`)
|
|
219
|
+
return chip ? centreOf(chip) : null
|
|
220
|
+
},
|
|
221
|
+
/** The fastening window's popup, as the standard overlay holds it: its title, whether its head moves it, its backdrop. */
|
|
222
|
+
fastenPopup() {
|
|
223
|
+
const overlay = deep('figure-fasten-dialog')[0]?.closest('ox-floating-overlay')
|
|
224
|
+
return overlay ? { title: overlay.title, movable: overlay.movable, backdrop: !!overlay.backdrop, role: overlay.querySelector('figure-fasten-dialog').getAttribute('role') } : null
|
|
225
|
+
},
|
|
203
226
|
/** The page point of an input by its aria-label, or of the part name field. */
|
|
204
227
|
nameField() {
|
|
205
228
|
const input = deep('input[name-field]')[0]
|
|
@@ -324,17 +324,13 @@ describe('the robot arm: a drag on a part edits its joint (ADR-0093)', () => {
|
|
|
324
324
|
})
|
|
325
325
|
})
|
|
326
326
|
|
|
327
|
-
describe('the fastening
|
|
327
|
+
describe('the fastening window belongs to the part it is for', () => {
|
|
328
328
|
before(async () => {
|
|
329
329
|
await mount('arm')
|
|
330
330
|
})
|
|
331
331
|
|
|
332
|
-
/*
|
|
333
|
-
const
|
|
334
|
-
page().evaluateHandle(label => {
|
|
335
|
-
const deep = (root = document) => [...root.querySelectorAll('select')].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
|
|
336
|
-
return deep().find(s => s.previousElementSibling?.textContent?.trim() === label) ?? null
|
|
337
|
-
}, label)
|
|
332
|
+
/* What the fastening window's list has chosen: the part, or `__plane`. */
|
|
333
|
+
const targetChosen = () => read(() => probe.fastenTargets()?.find(t => t.selected)?.target ?? null)
|
|
338
334
|
/* Where the part sits across the target face, as the face drawing holds it (the drawing sets it since 2026-10-02). */
|
|
339
335
|
const placing = () =>
|
|
340
336
|
read(() => {
|
|
@@ -345,13 +341,13 @@ describe('the fastening form belongs to the part it is for', () => {
|
|
|
345
341
|
|
|
346
342
|
test('a choice made for one part is not carried to the next part picked; the next opens on its own fastening (셔터 커튼 2026-09-25, walk 1 2026-09-26)', async () => {
|
|
347
343
|
await click(await read(() => probe.row('forearm')))
|
|
348
|
-
/* A fastening the part has is said in one line; 「바꾸기」 opens it (
|
|
344
|
+
/* A fastening the part has is said in one line; 「바꾸기」 opens the window on it (user 2026-10-17). */
|
|
349
345
|
await click(await read(() => probe.button('change-fastening')))
|
|
350
|
-
assert.equal(await (
|
|
346
|
+
assert.equal(await targetChosen(), 'elbow', 'the forearm opens on what it is fastened to')
|
|
351
347
|
/* Placed off centre on the elbow's face, as a hand does: the face drawing focused, the arrows. */
|
|
352
348
|
const face = await read(() => {
|
|
353
349
|
const deep = (root = document) => [...root.querySelectorAll('figure-fasten-drawing')].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
|
|
354
|
-
const rect = deep()
|
|
350
|
+
const rect = deep().find(d => d.show === 'face')?.shadowRoot.querySelector('div[where] svg > rect')
|
|
355
351
|
if (!rect) return null
|
|
356
352
|
rect.scrollIntoView({ block: 'center' })
|
|
357
353
|
const r = rect.getBoundingClientRect()
|
|
@@ -365,9 +361,13 @@ describe('the fastening form belongs to the part it is for', () => {
|
|
|
365
361
|
const chosen = await placing()
|
|
366
362
|
assert.equal(chosen?.x, 'mm', `placed off centre for the forearm: ${JSON.stringify(chosen)}`)
|
|
367
363
|
assert.ok(chosen.mm > 0)
|
|
364
|
+
/* The window is a popup over the work: closed (nothing written) before another part is picked. */
|
|
365
|
+
await page().keyboard.press('Escape')
|
|
366
|
+
await sleep(600)
|
|
367
|
+
assert.equal(await read(() => !!probe.fastenTargets()), false, 'closed by Esc')
|
|
368
368
|
|
|
369
369
|
await click(await read(() => probe.row('elbow')))
|
|
370
|
-
/* Another part opens on the line again
|
|
370
|
+
/* Another part opens on the line again: 「바꾸기」 opens the elbow's own. */
|
|
371
371
|
assert.ok(await read(() => probe.button('change-fastening')), 'the elbow\'s fastening said in one line')
|
|
372
372
|
await click(await read(() => probe.button('change-fastening')))
|
|
373
373
|
/* The elbow opens on its own fastening (walk 1, 2026-09-26), not on the forearm's choices. */
|
|
@@ -375,7 +375,7 @@ describe('the fastening form belongs to the part it is for', () => {
|
|
|
375
375
|
const joint = JSON.parse(document.querySelector('figure-editor').session.source()).joints.find(j => j.body1.part === 'elbow')
|
|
376
376
|
return { target: joint.body0.part, x: Object.keys(joint.origin?.align?.x ?? {})[0] ?? 'centre' }
|
|
377
377
|
})
|
|
378
|
-
assert.equal(await (
|
|
378
|
+
assert.equal(await targetChosen(), own.target, 'the elbow opens on what it is fastened to')
|
|
379
379
|
const next = await placing()
|
|
380
380
|
assert.equal(next?.target, own.target)
|
|
381
381
|
assert.equal(next.x, own.x, 'and on its own placing, not the forearm\'s')
|
|
@@ -635,16 +635,13 @@ describe('the modeller made easier to find and to keep (2026-09-26 baseline)', (
|
|
|
635
635
|
await mount('door')
|
|
636
636
|
await click(await read(() => probe.row('leaf')))
|
|
637
637
|
const was = await body('leaf')
|
|
638
|
-
/* The floor is chosen as what the leaf is fastened to, in the fastening
|
|
638
|
+
/* The floor is chosen as what the leaf is fastened to, in the fastening window; a new fastening waits for 붙이기. */
|
|
639
639
|
await click(await centre(await find('button', 'figure.button.fasten-start')))
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
})
|
|
644
|
-
assert.ok(await target.evaluate(s => !!s), 'the form asks what the leaf is fastened to')
|
|
645
|
-
await target.select('__plane')
|
|
640
|
+
await sleep(300)
|
|
641
|
+
assert.ok(await read(() => !!probe.fastenTargets()), 'the window asks what the leaf is fastened to')
|
|
642
|
+
await click(await read(() => probe.fastenTarget('__plane')))
|
|
646
643
|
await sleep(500)
|
|
647
|
-
const fasten = await find('
|
|
644
|
+
const fasten = await find('[apply]', 'figure.button.fasten')
|
|
648
645
|
const start = await state()
|
|
649
646
|
await click(await centre(fasten))
|
|
650
647
|
const after = await state()
|
|
@@ -880,6 +877,8 @@ describe('a row that carries a part (ADR-0093, 2026-09-26)', () => {
|
|
|
880
877
|
/* 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). */
|
|
881
878
|
await click(await read(() => probe.button('change-fastening')))
|
|
882
879
|
assert.equal(await read(() => probe.fastenMode()), 'centre', 'the way it is fastened reads centre')
|
|
880
|
+
await page().keyboard.press('Escape')
|
|
881
|
+
await sleep(600)
|
|
883
882
|
/* And the row's part keeps a name of its own to change. */
|
|
884
883
|
const name = await read(() => probe.nameField())
|
|
885
884
|
assert.equal(name.disabled, false, 'the roller can be renamed')
|
|
@@ -4,36 +4,9 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import 'figure-ui-client/setting-let/figure-namespace-setting-let.js'
|
|
6
6
|
import 'figure-ui-client/modeller/figure-first-save.js'
|
|
7
|
-
import { html
|
|
7
|
+
import { html } from 'lit'
|
|
8
8
|
import { openPopup } from '@operato/layout'
|
|
9
|
-
import
|
|
10
|
-
import '@operato/popup/ox-floating-overlay.js'
|
|
11
|
-
|
|
12
|
-
/* Popups, drawn as the layout draws them (as product-page.js does): an overlay in the body, the result given when it leaves. */
|
|
13
|
-
let popups = 0
|
|
14
|
-
setOpenPopupImplementation((template, options = {}) => {
|
|
15
|
-
const name = `$popup-${popups++}`
|
|
16
|
-
const fragment = document.createDocumentFragment()
|
|
17
|
-
render(html`<ox-floating-overlay id=${name} name=${name} .backdrop=${!!options.backdrop} .title=${options.title || ''} .size=${options.size || 'small'} .historical=${false}>${template}</ox-floating-overlay>`, fragment)
|
|
18
|
-
document.body.appendChild(fragment)
|
|
19
|
-
const handle = {
|
|
20
|
-
name,
|
|
21
|
-
close: () => {
|
|
22
|
-
const popup = document.getElementById(name)
|
|
23
|
-
if (!popup) return false
|
|
24
|
-
popup.close()
|
|
25
|
-
return true
|
|
26
|
-
},
|
|
27
|
-
closed: false
|
|
28
|
-
}
|
|
29
|
-
document.addEventListener('overlay-closed', function listener(e) {
|
|
30
|
-
if (e.detail !== name) return
|
|
31
|
-
document.removeEventListener('overlay-closed', listener)
|
|
32
|
-
handle.closed = true
|
|
33
|
-
handle.onclosed && handle.onclosed()
|
|
34
|
-
})
|
|
35
|
-
return handle
|
|
36
|
-
})
|
|
9
|
+
import './popup-stand-in.js'
|
|
37
10
|
|
|
38
11
|
let rows = [
|
|
39
12
|
{ id: '1', name: 'acme.example', state: 'active', isDefault: true },
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Popups drawn as the layout draws them -- an overlay of their own in the body, the result given when it leaves -- for
|
|
3
|
+
* the pages that have no layout to append them to. The popup's own options are passed as the layout passes them: its
|
|
4
|
+
* title, size, where it hovers, its backdrop, whether it closes by X or Esc and whether its head moves it.
|
|
5
|
+
*/
|
|
6
|
+
import { html, render } from 'lit'
|
|
7
|
+
import { setOpenPopupImplementation } from '@operato/popup'
|
|
8
|
+
import '@operato/popup/ox-floating-overlay.js'
|
|
9
|
+
|
|
10
|
+
let popups = 0
|
|
11
|
+
setOpenPopupImplementation((template, options = {}) => {
|
|
12
|
+
const name = `$popup-${popups++}`
|
|
13
|
+
const fragment = document.createDocumentFragment()
|
|
14
|
+
render(
|
|
15
|
+
html`<ox-floating-overlay
|
|
16
|
+
id=${name}
|
|
17
|
+
name=${name}
|
|
18
|
+
.backdrop=${!!options.backdrop}
|
|
19
|
+
.hovering=${options.hovering}
|
|
20
|
+
.title=${options.title || ''}
|
|
21
|
+
.size=${options.size || 'small'}
|
|
22
|
+
.closable=${!!options.closable}
|
|
23
|
+
.escapable=${!!options.escapable}
|
|
24
|
+
.movable=${!!options.movable}
|
|
25
|
+
.historical=${false}
|
|
26
|
+
>${template}</ox-floating-overlay
|
|
27
|
+
>`,
|
|
28
|
+
fragment
|
|
29
|
+
)
|
|
30
|
+
document.body.appendChild(fragment)
|
|
31
|
+
const handle = {
|
|
32
|
+
name,
|
|
33
|
+
close: () => {
|
|
34
|
+
const popup = document.getElementById(name)
|
|
35
|
+
if (!popup) return false
|
|
36
|
+
popup.close()
|
|
37
|
+
return true
|
|
38
|
+
},
|
|
39
|
+
closed: false
|
|
40
|
+
}
|
|
41
|
+
document.addEventListener('overlay-closed', function listener(e) {
|
|
42
|
+
if (e.detail !== name) return
|
|
43
|
+
document.removeEventListener('overlay-closed', listener)
|
|
44
|
+
handle.closed = true
|
|
45
|
+
handle.onclosed && handle.onclosed()
|
|
46
|
+
})
|
|
47
|
+
return handle
|
|
48
|
+
})
|