@things-factory/figure-ui 10.1.193 → 10.1.195
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 +10 -3
- package/client/modeller/editor.ts +61 -47
- package/client/modeller/figure-fasten-dialog.ts +11 -5
- package/client/modeller/refusal-teller.ts +8 -0
- package/client/pages/figure-list-page.ts +27 -40
- package/client/pages/figure-public-list-page.ts +21 -0
- package/client/route.ts +4 -0
- package/dist-client/modeller/editor-styles.js +10 -3
- package/dist-client/modeller/editor-styles.js.map +1 -1
- package/dist-client/modeller/editor.d.ts +18 -9
- package/dist-client/modeller/editor.js +62 -46
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/figure-fasten-dialog.d.ts +3 -3
- package/dist-client/modeller/figure-fasten-dialog.js +11 -5
- package/dist-client/modeller/figure-fasten-dialog.js.map +1 -1
- package/dist-client/modeller/refusal-teller.d.ts +8 -0
- package/dist-client/modeller/refusal-teller.js +7 -0
- package/dist-client/modeller/refusal-teller.js.map +1 -1
- package/dist-client/pages/figure-list-page.d.ts +6 -3
- package/dist-client/pages/figure-list-page.js +25 -42
- package/dist-client/pages/figure-list-page.js.map +1 -1
- package/dist-client/pages/figure-public-list-page.d.ts +16 -0
- package/dist-client/pages/figure-public-list-page.js +21 -0
- package/dist-client/pages/figure-public-list-page.js.map +1 -0
- package/dist-client/route.d.ts +1 -1
- package/dist-client/route.js +3 -0
- package/dist-client/route.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/test/align.test.ts +13 -5
- package/test/browser/editing.check.mjs +9 -13
- package/test/browser/links.check.mjs +36 -22
- package/test/browser/list-page.js +35 -0
- package/test/browser/list.check.mjs +68 -0
- package/test/browser/modeller-page.js +48 -4
- package/test/browser/product-page.js +6 -0
- package/test/browser/props.check.mjs +2 -2
- package/test/figure-released.test.ts +1 -1
- package/things-factory.config.js +1 -0
- package/translations/en.json +4 -6
- package/translations/es.json +4 -6
- package/translations/ja.json +4 -6
- package/translations/ko.json +4 -6
- package/translations/ms.json +4 -6
- package/translations/zh.json +4 -6
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.195",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public",
|
|
6
6
|
"@things-factory:registry": "https://registry.npmjs.org"
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"scripts": {
|
|
19
19
|
"build": "npm run build:server && npm run build:client",
|
|
20
20
|
"test": "FIGURE_FREEZE_COMPILED=1 node --import ./test/resolve-ts-siblings.mjs --test test/*.test.ts",
|
|
21
|
-
"test:browser": "node --test test/browser/build.check.mjs test/browser/modeller.check.mjs test/browser/modeller-revision.check.mjs test/browser/editing.check.mjs test/browser/links.check.mjs test/browser/section.check.mjs test/browser/screen.check.mjs test/browser/product-page.check.mjs test/browser/parts.check.mjs test/browser/places-mesh.check.mjs test/browser/part-ids.check.mjs test/browser/fix-alignment.check.mjs test/browser/mesh-select.check.mjs test/browser/row-preview.check.mjs test/browser/detail-tabs.check.mjs test/browser/props.check.mjs test/browser/retire.check.mjs test/browser/namespace.check.mjs test/browser/public.check.mjs test/browser/category.check.mjs test/browser/property-section.check.mjs",
|
|
21
|
+
"test:browser": "node --test test/browser/build.check.mjs test/browser/modeller.check.mjs test/browser/modeller-revision.check.mjs test/browser/editing.check.mjs test/browser/links.check.mjs test/browser/section.check.mjs test/browser/screen.check.mjs test/browser/product-page.check.mjs test/browser/parts.check.mjs test/browser/places-mesh.check.mjs test/browser/part-ids.check.mjs test/browser/fix-alignment.check.mjs test/browser/mesh-select.check.mjs test/browser/row-preview.check.mjs test/browser/detail-tabs.check.mjs test/browser/props.check.mjs test/browser/retire.check.mjs test/browser/namespace.check.mjs test/browser/public.check.mjs test/browser/category.check.mjs test/browser/property-section.check.mjs test/browser/list.check.mjs",
|
|
22
22
|
"copy:files": "copyfiles -e \"./client/**/*.{ts,js,json}\" -u 1 \"./client/**/*\" dist-client",
|
|
23
23
|
"build:client": "npm run copy:files && npx tsc --p ./client/tsconfig.json",
|
|
24
24
|
"build:server": "npx tsc --p ./server/tsconfig.json",
|
|
@@ -48,5 +48,5 @@
|
|
|
48
48
|
"@things-factory/setting-base": "^10.1.192",
|
|
49
49
|
"three": "^0.185.1"
|
|
50
50
|
},
|
|
51
|
-
"gitHead": "
|
|
51
|
+
"gitHead": "e22e6dec8be22ba1d8db7bf511f3bacee721ff8f"
|
|
52
52
|
}
|
package/test/align.test.ts
CHANGED
|
@@ -95,13 +95,21 @@ test('a tilted part is told it touches by a point or an edge, a ball by its lowe
|
|
|
95
95
|
assert.ok(!/기울|기운/.test(ko['figure.text.aligned-extreme-floor'] + ko['figure.text.align-to-floor-extreme']), 'a ball is not said to be tilted')
|
|
96
96
|
})
|
|
97
97
|
|
|
98
|
-
/*
|
|
99
|
-
|
|
98
|
+
/*
|
|
99
|
+
User 2026-10-19: the size drag's refusal told the face kept while dragging and the one written, and was not understood.
|
|
100
|
+
One line, why only -- attached turned, or tilted -- in every language; the way on is the notice's button.
|
|
101
|
+
*/
|
|
102
|
+
test('the size drag\'s refusal is one line saying why, in every language', () => {
|
|
100
103
|
for (const lang of ['en', 'ja', 'ko', 'ms', 'zh', 'es']) {
|
|
101
104
|
const t = JSON.parse(read(`../translations/${lang}.json`))
|
|
102
|
-
|
|
103
|
-
|
|
105
|
+
for (const key of ['figure.text.gizmo-size-turned-fastening', 'figure.text.gizmo-size-tilted']) {
|
|
106
|
+
assert.ok(t[key]?.includes('{part}'), `${lang} ${key}`)
|
|
107
|
+
assert.ok(!t[key].includes('{extreme}'), `${lang} ${key}: no face kept is told`)
|
|
108
|
+
assert.equal(t[key].split(/[.。]\s*/).filter(Boolean).length, 1, `${lang} ${key}: one sentence`)
|
|
109
|
+
}
|
|
110
|
+
assert.ok(t['figure.button.to-size-fields'], lang)
|
|
104
111
|
}
|
|
105
112
|
const ko = JSON.parse(read('../translations/ko.json'))
|
|
106
|
-
assert.equal(ko['figure.
|
|
113
|
+
assert.equal(ko['figure.text.gizmo-size-turned-fastening'], '{part} 부품은 돌려 붙어 있어 끌어서 크기를 바꿀 수 없습니다.')
|
|
114
|
+
assert.equal(ko['figure.button.to-size-fields'], '크기 칸으로')
|
|
107
115
|
})
|
|
@@ -532,31 +532,27 @@ describe('the recipe catalog says nothing where a template has no guide', () =>
|
|
|
532
532
|
})
|
|
533
533
|
|
|
534
534
|
/*
|
|
535
|
-
The size gizmo
|
|
536
|
-
|
|
535
|
+
The size gizmo withheld from a tilted part says why in one line (user 2026-10-19: the faces kept while dragging and
|
|
536
|
+
when written were not understood), as a notice, not the error's red. The page loads no translations; the sentence is
|
|
537
|
+
given words here, so the part it names can be read.
|
|
537
538
|
*/
|
|
538
|
-
describe('the size gizmo withheld from a tilted part says
|
|
539
|
+
describe('the size gizmo withheld from a tilted part says why, in one line, as a notice', () => {
|
|
539
540
|
before(async () => {
|
|
540
541
|
await mount('box', 'tilted-ceiling')
|
|
541
|
-
await read(() =>
|
|
542
|
-
editing.words({
|
|
543
|
-
'figure.text.gizmo-size-turned-fastening': '{part} is tilted: its size is typed, keeping {extreme}',
|
|
544
|
-
'figure.label.kept-extreme-ceiling': 'its highest point',
|
|
545
|
-
'figure.label.kept-extreme-floor': 'its lowest point'
|
|
546
|
-
})
|
|
547
|
-
)
|
|
542
|
+
await read(() => editing.words({ 'figure.text.gizmo-size-tilted': '{part} is tilted, so its size cannot be dragged.' }))
|
|
548
543
|
await pick('box')
|
|
549
544
|
})
|
|
550
545
|
after(() => read(() => editing.words(null)))
|
|
551
546
|
|
|
552
|
-
test('hung from the ceiling, the tooltip and the refusal of R
|
|
547
|
+
test('hung from the ceiling, the tooltip and the refusal of R say the same line; nothing is said of the faces it keeps', async () => {
|
|
553
548
|
const resize = (await read(() => editing.tools())).find(t => t.label === 'figure.button.gizmo-scale')
|
|
554
549
|
assert.equal(resize.disabled, true, 'not offered')
|
|
555
|
-
assert.ok(resize.title.endsWith('box is tilted
|
|
550
|
+
assert.ok(resize.title.endsWith('box is tilted, so its size cannot be dragged.'), resize.title)
|
|
556
551
|
await read(() => editing.told())
|
|
557
552
|
await press('KeyR')
|
|
558
|
-
assert.deepEqual(
|
|
553
|
+
assert.deepEqual(await read(() => editing.told()), [{ level: 'info', message: 'box is tilted, so its size cannot be dragged.' }])
|
|
559
554
|
assert.equal((await state()).attached, null, 'no gizmo on it')
|
|
555
|
+
assert.equal((await state()).error, '', 'no error')
|
|
560
556
|
})
|
|
561
557
|
})
|
|
562
558
|
|
|
@@ -370,7 +370,7 @@ describe('the fastening window on the robot arm', () => {
|
|
|
370
370
|
|
|
371
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
372
|
const said = await read(() => {
|
|
373
|
-
const root =
|
|
373
|
+
const root = window.fastenDialog().shadowRoot
|
|
374
374
|
const chip = root.querySelector('ox-input-select-buttons').shadowRoot.querySelector('md-filter-chip')
|
|
375
375
|
const label = chip.shadowRoot.querySelector('.label-text') ?? chip.shadowRoot.querySelector('.label')
|
|
376
376
|
return { lines: root.querySelectorAll('p[loops]').length, transform: getComputedStyle(label).textTransform }
|
|
@@ -388,14 +388,17 @@ describe('a new fastening for a door leaf: chosen in the window, seen before it
|
|
|
388
388
|
})
|
|
389
389
|
const ghosts = () => read(() => look.drawn().filter(n => n.startsWith('__preview')))
|
|
390
390
|
|
|
391
|
-
test('started, the window opens
|
|
391
|
+
test('started, the window opens over the side panel, asking what to fasten to, nothing chosen; the canvas stays in sight', async () => {
|
|
392
392
|
await pick('leaf')
|
|
393
393
|
await press('button[fasten-start]')
|
|
394
394
|
const list = await until(() => probe.fastenTargets(), [], 'the fastening window')
|
|
395
395
|
assert.deepEqual(list.filter(t => t.selected), [], 'nothing chosen')
|
|
396
396
|
assert.ok(list.some(t => t.target === 'frame' && !t.disabled), 'the frame offered')
|
|
397
397
|
assert.deepEqual((await all('[apply]')).map(b => b.disabled), [true], 'nothing to apply yet')
|
|
398
|
-
|
|
398
|
+
/* Not the modal popup (user 2026-10-19: it covered the 3D it was choosing for): over the side panel, at most its edge over the canvas. */
|
|
399
|
+
const panel = await read(() => probe.fastenPanel())
|
|
400
|
+
assert.deepEqual({ ...panel, overCanvas: undefined }, { title: 'figure.label.fastening', role: 'dialog', popup: false, overCanvas: undefined })
|
|
401
|
+
assert.ok(panel.overCanvas <= 0.1, `the canvas stays in sight: ${panel.overCanvas} of its width covered`)
|
|
399
402
|
})
|
|
400
403
|
|
|
401
404
|
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 () => {
|
|
@@ -421,14 +424,15 @@ describe('a new fastening for a door leaf: chosen in the window, seen before it
|
|
|
421
424
|
assert.deepEqual(await ghosts(), [], 'nothing of the earlier result drawn')
|
|
422
425
|
})
|
|
423
426
|
|
|
424
|
-
test('
|
|
427
|
+
test('another part picked in the list closes the window with nothing written, and nothing it tried is drawn', async () => {
|
|
425
428
|
const start = await state()
|
|
426
|
-
/*
|
|
427
|
-
|
|
429
|
+
/* No backdrop over the page: the parts list stays reachable. The window stands over the side panel, its tabs too, as the view settings do. */
|
|
430
|
+
assert.ok((await read(() => probe.fastenPanel())).overCanvas <= 0.1)
|
|
431
|
+
await pick('frame')
|
|
428
432
|
await until(() => !probe.fastenTargets(), [], 'the window closed')
|
|
429
433
|
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
434
|
assert.equal((await state()).revision, start.revision, 'nothing written')
|
|
435
|
+
await pick('leaf')
|
|
432
436
|
})
|
|
433
437
|
|
|
434
438
|
test('"fasten it first" from the motion tab opens this part\'s window, on what it has, on the properties tab', async () => {
|
|
@@ -599,23 +603,32 @@ function twisted() {
|
|
|
599
603
|
}
|
|
600
604
|
|
|
601
605
|
describe('a part fastened twisted is not sized by a drag', () => {
|
|
602
|
-
test('the resize tool is off, and a size drag that still arrives is not written
|
|
606
|
+
test('the resize tool is off, and a size drag that still arrives is not written: a notice says why, in one line, with the way to its size', async () => {
|
|
603
607
|
await mount('twisted', twisted())
|
|
604
608
|
await pick('blade')
|
|
605
609
|
assert.equal((await at('button[tool]', 'figure.button.gizmo-scale'))?.disabled, true, 'the resize tool is off')
|
|
610
|
+
await read(() => probe.told())
|
|
606
611
|
const start = await state()
|
|
607
612
|
await read(() => look.transformed('blade', { scale: [1.5, 1, 1] }))
|
|
608
613
|
await settled()
|
|
609
614
|
assert.equal((await state()).revision, start.revision, 'not written')
|
|
610
|
-
|
|
615
|
+
/* A notice, not the error's red (user 2026-10-19: the reason read as a fault, and was not understood). */
|
|
616
|
+
assert.deepEqual(await read(() => probe.told()), [{ level: 'info', message: 'figure.text.gizmo-size-turned-fastening', action: 'figure.button.to-size-fields' }])
|
|
617
|
+
assert.equal((await state()).error, '', 'no error')
|
|
611
618
|
})
|
|
612
619
|
|
|
613
|
-
test('the R key says why', async () => {
|
|
620
|
+
test('the R key says why, and the notice\'s button puts the cursor in the part\'s size field', async () => {
|
|
614
621
|
await mount('twisted', twisted())
|
|
615
622
|
await pick('blade')
|
|
616
|
-
|
|
623
|
+
await read(() => probe.told())
|
|
617
624
|
await page().keyboard.press('KeyR')
|
|
618
|
-
await until(() => probe.
|
|
625
|
+
await until(() => probe.told().some(n => n.message === 'figure.text.gizmo-size-turned-fastening' && n.action === 'figure.button.to-size-fields'), [], 'the reason said with its way on')
|
|
626
|
+
assert.equal((await state()).error, '', 'no error')
|
|
627
|
+
/* From another tab: the button brings the properties tab and the size field. */
|
|
628
|
+
await tab('motion')
|
|
629
|
+
assert.equal(await read(() => probe.pressNoticeButton()), true)
|
|
630
|
+
await until(() => probe.focused()?.label?.startsWith('blade.size.'), [], 'the blade\'s size field focused')
|
|
631
|
+
assert.equal(await read(() => look.tab()), 'figure.label.tab-short-properties')
|
|
619
632
|
})
|
|
620
633
|
})
|
|
621
634
|
|
|
@@ -741,26 +754,27 @@ describe('the fastening window: tried freely, written once', () => {
|
|
|
741
754
|
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
755
|
})
|
|
743
756
|
|
|
744
|
-
test('
|
|
757
|
+
test('the canvas is turned with the window open: nothing written, still open, the window where it was', async () => {
|
|
745
758
|
await pick('lamp')
|
|
746
759
|
await changeFastening()
|
|
747
760
|
const rect = () => read(() => {
|
|
748
|
-
const r =
|
|
761
|
+
const r = window.fastenDialog()?.getBoundingClientRect()
|
|
749
762
|
return r ? { x: Math.round(r.left), y: Math.round(r.top) } : null
|
|
750
763
|
})
|
|
751
764
|
const was = await rect()
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
765
|
+
/* A drag on the empty canvas, left of the parts: the camera turns, nothing is picked. */
|
|
766
|
+
const box = await read(() => {
|
|
767
|
+
const r = document.querySelector('figure-editor').shadowRoot.querySelector('figure-canvas').getBoundingClientRect()
|
|
768
|
+
return { x: r.left + 40, y: r.top + r.height / 2 }
|
|
755
769
|
})
|
|
756
770
|
const start = await state()
|
|
757
|
-
await page().mouse.move(
|
|
771
|
+
await page().mouse.move(box.x, box.y)
|
|
758
772
|
await page().mouse.down()
|
|
759
|
-
for (let i = 1; i <= 6; i++) await page().mouse.move(
|
|
773
|
+
for (let i = 1; i <= 6; i++) await page().mouse.move(box.x + 20 * i, box.y + 5 * i)
|
|
760
774
|
await page().mouse.up()
|
|
761
|
-
|
|
762
|
-
assert.deepEqual([moved.x - was.x, moved.y - was.y], [-180, 120], `moved with the pointer: ${JSON.stringify([was, moved])}`)
|
|
775
|
+
await settled()
|
|
763
776
|
assert.equal(await windowOpen(), true, 'still open')
|
|
777
|
+
assert.deepEqual(await rect(), was, 'the window where it was')
|
|
764
778
|
assert.equal((await state()).revision, start.revision, 'nothing written')
|
|
765
779
|
await press('[cancel]')
|
|
766
780
|
await until(() => !probe.fastenTargets(), [], 'the window closed')
|
|
@@ -771,7 +785,7 @@ describe('the fastening window: tried freely, written once', () => {
|
|
|
771
785
|
await changeFastening()
|
|
772
786
|
const litAt = y => Math.abs((document.querySelector('figure-editor').shadowRoot.querySelector('figure-canvas').highlight?.pose.t[1] ?? 0) - y) < 2
|
|
773
787
|
/* The list has the focus, on the top it is fastened to. */
|
|
774
|
-
await until(() =>
|
|
788
|
+
await until(() => window.fastenDialog()?.shadowRoot.querySelector('ox-input-select-buttons')?.shadowRoot.activeElement?.getAttribute('data-value') === 'top', [], 'the focus on the chosen chip')
|
|
775
789
|
await page().keyboard.press('ArrowLeft')
|
|
776
790
|
/* The leg's top, 700 high (the lit plate a little over it); moving the focus lights, it does not choose. */
|
|
777
791
|
await until(litAt, [702], 'the leg\'s top lit')
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The catalogue and the public figures, as the rail opens them (user 2026-10-19): the domain's own figures with the
|
|
3
|
+
* new-figure button and the draft · released choice, and other domains' public figures with neither. The server is a
|
|
4
|
+
* stand-in that answers each list with one card and says which list was asked for.
|
|
5
|
+
*/
|
|
6
|
+
import 'figure-ui-client/pages/figure-list-page.js'
|
|
7
|
+
import 'figure-ui-client/pages/figure-public-list-page.js'
|
|
8
|
+
import { client } from '@operato/graphql'
|
|
9
|
+
|
|
10
|
+
const asked = []
|
|
11
|
+
const card = { id: 'one', name: 'Mixer', type: 'mixer', namespace: 'acme.example', state: 'released', version: 1, public: true, tags: [] }
|
|
12
|
+
client.query = async ({ query }) => {
|
|
13
|
+
const field = query.definitions.find(d => d.kind === 'OperationDefinition')?.selectionSet.selections[0]?.name.value
|
|
14
|
+
asked.push(field)
|
|
15
|
+
if (field === 'figures' || field === 'publicFigures')
|
|
16
|
+
return { data: { [field]: { items: [field === 'publicFigures' ? { ...card, domain: { id: 'd2', name: 'Other works', subdomain: 'other' } } : card], total: 1 } } }
|
|
17
|
+
/* No kept preferences: the page as it first opens. */
|
|
18
|
+
if (field === 'myPageAllPreferences') return { data: { myPageAllPreferences: [] } }
|
|
19
|
+
return { data: {} }
|
|
20
|
+
}
|
|
21
|
+
client.mutate = async () => ({ data: {} })
|
|
22
|
+
|
|
23
|
+
window.harness = {
|
|
24
|
+
asked,
|
|
25
|
+
mount(tag) {
|
|
26
|
+
document.body.replaceChildren()
|
|
27
|
+
const page = document.createElement(tag)
|
|
28
|
+
page.style.cssText = 'display:block;height:800px'
|
|
29
|
+
/* What the shell gives a page it routes to: the page's name, read for its kept preferences. */
|
|
30
|
+
page.lifecycle = { page: tag === 'figure-public-list-page' ? 'figure-public-list' : 'figure-list' }
|
|
31
|
+
document.body.append(page)
|
|
32
|
+
page.active = true
|
|
33
|
+
return page.localName
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The rail's two lists (user 2026-10-19): the catalogue is where figures are made and changed, the public figures of
|
|
3
|
+
* other domains only looked at. Each is a screen of its own; the catalogue has no switch between them, and the public
|
|
4
|
+
* list offers no new figure and no draft · released choice.
|
|
5
|
+
*
|
|
6
|
+
* Run: node --test test/browser/list.check.mjs
|
|
7
|
+
*/
|
|
8
|
+
import { test, describe, before, after } from 'node:test'
|
|
9
|
+
import assert from 'node:assert/strict'
|
|
10
|
+
|
|
11
|
+
import { buildPage } from './build.mjs'
|
|
12
|
+
import { openPage } from './serve.mjs'
|
|
13
|
+
|
|
14
|
+
const sleep = ms => new Promise(done => setTimeout(done, ms))
|
|
15
|
+
|
|
16
|
+
let opened
|
|
17
|
+
before(async () => {
|
|
18
|
+
opened = await openPage(await buildPage('list-page.js'), { width: 1400, height: 900 })
|
|
19
|
+
})
|
|
20
|
+
after(async () => {
|
|
21
|
+
await opened?.close()
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
const page = () => opened.page
|
|
25
|
+
const read = (fn, ...args) => page().evaluate(fn, ...args)
|
|
26
|
+
async function until(fn, what, ms = 15000) {
|
|
27
|
+
const deadline = Date.now() + ms
|
|
28
|
+
for (;;) {
|
|
29
|
+
if (await read(fn).catch(() => false)) return
|
|
30
|
+
if (Date.now() > deadline) assert.fail(`waited for ${what}`)
|
|
31
|
+
await sleep(50)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/* What a list page shows: its title, its FAB, the choices in its filter bar, a scope switch, and the list it asked for. */
|
|
35
|
+
const look = () =>
|
|
36
|
+
read(() => {
|
|
37
|
+
const host = document.body.firstElementChild
|
|
38
|
+
const root = host.shadowRoot
|
|
39
|
+
const deep = (s, r = root, o = []) => {
|
|
40
|
+
o.push(...r.querySelectorAll(s))
|
|
41
|
+
for (const e of r.querySelectorAll('*')) if (e.shadowRoot) deep(s, e.shadowRoot, o)
|
|
42
|
+
return o
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
title: host.context.title,
|
|
46
|
+
fab: !!root.querySelector('button[fab]'),
|
|
47
|
+
scopeSwitch: !!root.querySelector('button[scope]'),
|
|
48
|
+
stateChoice: deep('ox-input-select-buttons[name=state]').length,
|
|
49
|
+
asked: harness.asked.at(-1)
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
describe('the rail\'s two lists', () => {
|
|
54
|
+
test('the catalogue: its own figures, the new-figure button and the draft · released choice, no switch to the public ones', async () => {
|
|
55
|
+
await read(() => harness.mount('figure-list-page'))
|
|
56
|
+
await until(() => harness.asked.includes('figures'), 'the own list asked for')
|
|
57
|
+
await until(() => !!document.body.firstElementChild.shadowRoot.querySelector('ox-grist'), 'the grid')
|
|
58
|
+
await sleep(500)
|
|
59
|
+
assert.deepEqual(await look(), { title: 'figure.title.figures', fab: true, scopeSwitch: false, stateChoice: 1, asked: 'figures' })
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('the public figures: other domains\' list, no new figure, no draft · released choice', async () => {
|
|
63
|
+
await read(() => harness.mount('figure-public-list-page'))
|
|
64
|
+
await until(() => harness.asked.includes('publicFigures'), 'the public list asked for')
|
|
65
|
+
await sleep(500)
|
|
66
|
+
assert.deepEqual(await look(), { title: 'figure.title.public-figures', fab: false, scopeSwitch: false, stateChoice: 0, asked: 'publicFigures' })
|
|
67
|
+
})
|
|
68
|
+
})
|
|
@@ -87,6 +87,20 @@ function deep(selector, root = document) {
|
|
|
87
87
|
|
|
88
88
|
const editor = () => document.querySelector('figure-editor')
|
|
89
89
|
const canvas = () => deep('figure-canvas')[0]
|
|
90
|
+
/** The fastening window, wherever the editor draws it. */
|
|
91
|
+
window.fastenDialog = () => deep('figure-fasten-dialog')[0] ?? null
|
|
92
|
+
|
|
93
|
+
/*
|
|
94
|
+
* What the app's notification surface is handed (`notify`), in order. The page draws no snackbar, so a notice's button
|
|
95
|
+
* is pressed here, as the snackbar's would call it.
|
|
96
|
+
*/
|
|
97
|
+
const notices = []
|
|
98
|
+
const pressable = []
|
|
99
|
+
document.addEventListener('notify', e => {
|
|
100
|
+
notices.push(e.detail)
|
|
101
|
+
const action = e.detail?.option?.action
|
|
102
|
+
if (action?.callback) pressable.push(action.callback)
|
|
103
|
+
})
|
|
90
104
|
const three = () => canvas().scene.root._threeCapability
|
|
91
105
|
const controls = () => three().gizmoManager.controls
|
|
92
106
|
|
|
@@ -218,10 +232,40 @@ window.probe = {
|
|
|
218
232
|
const chip = deep('figure-fasten-dialog')[0]?.shadowRoot.querySelector('ox-input-select-buttons')?.shadowRoot?.querySelector(`md-filter-chip[data-value="${CSS.escape(part)}"]`)
|
|
219
233
|
return chip ? centreOf(chip) : null
|
|
220
234
|
},
|
|
221
|
-
/**
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
235
|
+
/**
|
|
236
|
+
* The fastening window as it stands: its title, its role, whether a popup or its backdrop is anywhere on the page, and
|
|
237
|
+
* how much of the canvas it covers (a share of the canvas's width).
|
|
238
|
+
*/
|
|
239
|
+
fastenPanel() {
|
|
240
|
+
const dialog = deep('figure-fasten-dialog')[0]
|
|
241
|
+
const panel = dialog?.closest('[fasten-panel]')
|
|
242
|
+
if (!panel) return null
|
|
243
|
+
const p = panel.getBoundingClientRect(),
|
|
244
|
+
c = canvas().getBoundingClientRect()
|
|
245
|
+
const across = Math.max(0, Math.min(p.right, c.right) - Math.max(p.left, c.left))
|
|
246
|
+
return {
|
|
247
|
+
title: panel.querySelector('h3')?.textContent.trim(),
|
|
248
|
+
role: dialog.getAttribute('role'),
|
|
249
|
+
popup: deep('ox-floating-overlay').length > 0,
|
|
250
|
+
overCanvas: Math.round((across / c.width) * 100) / 100
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
/** What the notification surface was handed since the last call: its level, its words, its button's label. */
|
|
254
|
+
told() {
|
|
255
|
+
return notices.splice(0).map(n => ({ level: n.level, message: n.message, action: n.option?.action?.label ?? null }))
|
|
256
|
+
},
|
|
257
|
+
/** The button of the last notice that had one, pressed. */
|
|
258
|
+
pressNoticeButton() {
|
|
259
|
+
const last = [...pressable].pop()
|
|
260
|
+
if (!last) return false
|
|
261
|
+
last()
|
|
262
|
+
return true
|
|
263
|
+
},
|
|
264
|
+
/** The element that has the focus, through shadow roots: its tag and aria-label. */
|
|
265
|
+
focused() {
|
|
266
|
+
let at = document.activeElement
|
|
267
|
+
while (at?.shadowRoot?.activeElement) at = at.shadowRoot.activeElement
|
|
268
|
+
return at ? { tag: at.localName, label: at.getAttribute('aria-label') } : null
|
|
225
269
|
},
|
|
226
270
|
/** The page point of an input by its aria-label, or of the part name field. */
|
|
227
271
|
nameField() {
|
|
@@ -155,6 +155,12 @@ function shellMoves(move) {
|
|
|
155
155
|
shellMoving = true
|
|
156
156
|
try {
|
|
157
157
|
move()
|
|
158
|
+
/*
|
|
159
|
+
The address layer adopts a raw History write the next time the address is read or written, and tells its
|
|
160
|
+
subscribers then (@operato/utils 10.29.43, \`adoptForeignWrite\`). Read it here, so the shell's own move is
|
|
161
|
+
adopted while it is still known as the shell's -- not at the product's next step, recorded as the product's.
|
|
162
|
+
*/
|
|
163
|
+
void addressController().entry
|
|
158
164
|
} finally {
|
|
159
165
|
shellMoving = false
|
|
160
166
|
}
|
|
@@ -184,11 +184,11 @@ describe('a part\'s groups in one order, whatever its state', () => {
|
|
|
184
184
|
})
|
|
185
185
|
assert.deepEqual(await place(), { sentence: true, change: true, drawing: false, cells: false })
|
|
186
186
|
await press('button[change-fastening]', '「바꾸기」')
|
|
187
|
-
await until(() => !!
|
|
187
|
+
await until(() => !!window.fastenDialog(), [], 'the window')
|
|
188
188
|
assert.deepEqual(await place(), { sentence: true, change: true, drawing: false, cells: false }, 'the panel still says the result')
|
|
189
189
|
/* Closed with nothing written; another part chosen and back: one line. */
|
|
190
190
|
await page().keyboard.press('Escape')
|
|
191
|
-
await until(() => !
|
|
191
|
+
await until(() => !window.fastenDialog(), [], 'the window closed')
|
|
192
192
|
await pick('bed')
|
|
193
193
|
await pick('mark')
|
|
194
194
|
assert.equal((await place()).change, true, 'said in one line again')
|
|
@@ -49,6 +49,6 @@ test("another domain's public figure: its card offers no delete and names its do
|
|
|
49
49
|
assert.match(thumb, /badge=\$\{this\.readonly \? figure\.domain\?\.name/)
|
|
50
50
|
for (const lang of ['en', 'ko', 'ja', 'zh', 'ms', 'es']) {
|
|
51
51
|
const words = JSON.parse(read(`../translations/${lang}.json`))
|
|
52
|
-
for (const key of ['figure.label.public', 'figure.button.make-public', 'figure.button.make-private', 'figure.
|
|
52
|
+
for (const key of ['figure.label.public', 'figure.button.make-public', 'figure.button.make-private', 'figure.title.public-figures']) assert.ok(words[key], `${lang} ${key}`)
|
|
53
53
|
}
|
|
54
54
|
})
|
package/things-factory.config.js
CHANGED
|
@@ -5,6 +5,7 @@ export default {
|
|
|
5
5
|
route,
|
|
6
6
|
routes: [
|
|
7
7
|
{ tagname: 'figure-list-page', page: 'figure-list' },
|
|
8
|
+
{ tagname: 'figure-public-list-page', page: 'figure-public-list' },
|
|
8
9
|
/* The shell builds the DOM from this table. */
|
|
9
10
|
{ tagname: 'figure-modeller-page', page: 'figure-modeller' }
|
|
10
11
|
],
|
package/translations/en.json
CHANGED
|
@@ -507,6 +507,7 @@
|
|
|
507
507
|
"figure.text.what-a-figure-is": "a Figure is a 3D component you author. compose it from simple shapes and it becomes a component you can place on a drawing, sharing shape and material across every instance.",
|
|
508
508
|
"figure.text.what-changed-in-this-release": "what changed in this release",
|
|
509
509
|
"figure.title.figures": "figure catalog",
|
|
510
|
+
"figure.title.public-figures": "public figures",
|
|
510
511
|
"figure.title.new-figure": "new figure",
|
|
511
512
|
"figure.title.save-new-figure": "save figure draft",
|
|
512
513
|
"figure.text.no-parts-yet": "No parts yet. Pick a shape above to place one.",
|
|
@@ -842,11 +843,9 @@
|
|
|
842
843
|
"figure.text.held-on-part": "Attached by its {face} to the {otherFace} of {other}",
|
|
843
844
|
"figure.button.open-fastening": "Open attachment settings",
|
|
844
845
|
"figure.button.detach-held": "Detach",
|
|
845
|
-
"figure.text.gizmo-size-turned-fastening": "{part} is
|
|
846
|
-
"figure.
|
|
847
|
-
"figure.
|
|
848
|
-
"figure.label.kept-extreme-wall": "backmost point",
|
|
849
|
-
"figure.label.kept-extreme-center": "centre",
|
|
846
|
+
"figure.text.gizmo-size-turned-fastening": "{part} is attached turned, so its size cannot be dragged.",
|
|
847
|
+
"figure.text.gizmo-size-tilted": "{part} is tilted, so its size cannot be dragged.",
|
|
848
|
+
"figure.button.to-size-fields": "Go to size",
|
|
850
849
|
"figure.label.align-input": "Follows its field ({field})",
|
|
851
850
|
"figure.label.tab-short-properties": "Properties",
|
|
852
851
|
"figure.label.tab-short-capability": "Capability",
|
|
@@ -1207,7 +1206,6 @@
|
|
|
1207
1206
|
"figure.label.public": "public",
|
|
1208
1207
|
"figure.button.make-public": "make public",
|
|
1209
1208
|
"figure.button.make-private": "make private",
|
|
1210
|
-
"figure.button.public-figures": "public figures of other domains",
|
|
1211
1209
|
"figure.text.category-choose": "choose a category",
|
|
1212
1210
|
"figure.text.property-section-failed": "the section {section} could not be drawn: {reason}"
|
|
1213
1211
|
}
|
package/translations/es.json
CHANGED
|
@@ -507,6 +507,7 @@
|
|
|
507
507
|
"figure.text.what-a-figure-is": "una Figure es un componente 3D que usted crea. compóngala a partir de formas sencillas y se convierte en un componente que puede colocar en un plano, compartiendo forma y material entre todas las instancias.",
|
|
508
508
|
"figure.text.what-changed-in-this-release": "qué cambió en esta publicación",
|
|
509
509
|
"figure.title.figures": "catálogo de figuras",
|
|
510
|
+
"figure.title.public-figures": "figuras públicas",
|
|
510
511
|
"figure.title.new-figure": "nueva figura",
|
|
511
512
|
"figure.title.save-new-figure": "guardar borrador de figura",
|
|
512
513
|
"figure.text.no-parts-yet": "Aún no hay piezas. Elija una forma arriba para colocar una.",
|
|
@@ -842,11 +843,9 @@
|
|
|
842
843
|
"figure.text.held-on-part": "Unida por su {face} a {otherFace} de {other}",
|
|
843
844
|
"figure.button.open-fastening": "Abrir la configuración de unión",
|
|
844
845
|
"figure.button.detach-held": "Separar",
|
|
845
|
-
"figure.text.gizmo-size-turned-fastening": "{part} está
|
|
846
|
-
"figure.
|
|
847
|
-
"figure.
|
|
848
|
-
"figure.label.kept-extreme-wall": "punto más trasero",
|
|
849
|
-
"figure.label.kept-extreme-center": "centro",
|
|
846
|
+
"figure.text.gizmo-size-turned-fastening": "{part} está unida girada, así que su tamaño no se puede arrastrar.",
|
|
847
|
+
"figure.text.gizmo-size-tilted": "{part} está inclinada, así que su tamaño no se puede arrastrar.",
|
|
848
|
+
"figure.button.to-size-fields": "Ir al tamaño",
|
|
850
849
|
"figure.label.align-input": "Sigue su campo ({field})",
|
|
851
850
|
"figure.label.tab-short-properties": "Propiedades",
|
|
852
851
|
"figure.label.tab-short-capability": "Capacidad",
|
|
@@ -1207,7 +1206,6 @@
|
|
|
1207
1206
|
"figure.label.public": "pública",
|
|
1208
1207
|
"figure.button.make-public": "hacer pública",
|
|
1209
1208
|
"figure.button.make-private": "dejar de publicar",
|
|
1210
|
-
"figure.button.public-figures": "figuras públicas de otros dominios",
|
|
1211
1209
|
"figure.text.category-choose": "elegir categoría",
|
|
1212
1210
|
"figure.text.property-section-failed": "no se pudo dibujar la sección {section}: {reason}"
|
|
1213
1211
|
}
|
package/translations/ja.json
CHANGED
|
@@ -507,6 +507,7 @@
|
|
|
507
507
|
"figure.text.what-a-figure-is": "a Figure is a 3D component you author. Compose it from simple shapes and it becomes a component you can place on a drawing, sharing shape and material across every instance.",
|
|
508
508
|
"figure.text.what-changed-in-this-release": "このバージョンで変わったこと",
|
|
509
509
|
"figure.title.figures": "Figure カタログ",
|
|
510
|
+
"figure.title.public-figures": "公開 Figure",
|
|
510
511
|
"figure.title.new-figure": "new figure",
|
|
511
512
|
"figure.title.save-new-figure": "Figure の下書きを保存",
|
|
512
513
|
"figure.text.no-parts-yet": "No parts yet. Pick a shape above to place one.",
|
|
@@ -842,11 +843,9 @@
|
|
|
842
843
|
"figure.text.held-on-part": "{other}の{otherFace}に{face}で取り付け",
|
|
843
844
|
"figure.button.open-fastening": "取り付け設定を開く",
|
|
844
845
|
"figure.button.detach-held": "取り付けを外す",
|
|
845
|
-
"figure.text.gizmo-size-turned-fastening": "{part}
|
|
846
|
-
"figure.
|
|
847
|
-
"figure.
|
|
848
|
-
"figure.label.kept-extreme-wall": "最も奥の点",
|
|
849
|
-
"figure.label.kept-extreme-center": "中心",
|
|
846
|
+
"figure.text.gizmo-size-turned-fastening": "{part} は回して取り付けられているため、ドラッグでサイズを変えられません。",
|
|
847
|
+
"figure.text.gizmo-size-tilted": "{part} は傾いているため、ドラッグでサイズを変えられません。",
|
|
848
|
+
"figure.button.to-size-fields": "サイズ欄へ",
|
|
850
849
|
"figure.label.align-input": "位置欄に従う ({field})",
|
|
851
850
|
"figure.label.tab-short-properties": "属性",
|
|
852
851
|
"figure.label.tab-short-capability": "能力",
|
|
@@ -1207,7 +1206,6 @@
|
|
|
1207
1206
|
"figure.label.public": "公開",
|
|
1208
1207
|
"figure.button.make-public": "公開する",
|
|
1209
1208
|
"figure.button.make-private": "公開をやめる",
|
|
1210
|
-
"figure.button.public-figures": "他ドメインの公開 Figure",
|
|
1211
1209
|
"figure.text.category-choose": "分類を選ぶ",
|
|
1212
1210
|
"figure.text.property-section-failed": "{section} の区画を描けませんでした: {reason}"
|
|
1213
1211
|
}
|
package/translations/ko.json
CHANGED
|
@@ -507,6 +507,7 @@
|
|
|
507
507
|
"figure.text.what-a-figure-is": "Figure 는 직접 저작한 3D 컴포넌트입니다. 단순한 도형을 조합해 만들면 도면에 배치할 수 있는 컴포넌트가 되고, 여러 개를 배치해도 형상과 재질을 공유합니다.",
|
|
508
508
|
"figure.text.what-changed-in-this-release": "이번 버전에서 바뀐 것",
|
|
509
509
|
"figure.title.figures": "Figure 카탈로그",
|
|
510
|
+
"figure.title.public-figures": "공개 Figure",
|
|
510
511
|
"figure.title.new-figure": "새 Figure",
|
|
511
512
|
"figure.title.save-new-figure": "Figure 초안 저장",
|
|
512
513
|
"figure.text.no-parts-yet": "아직 부품이 없습니다. 위에서 도형을 선택하여 놓으세요.",
|
|
@@ -842,11 +843,9 @@
|
|
|
842
843
|
"figure.text.held-on-part": "{other}의 {otherFace}에 {face}으로 붙어 있음",
|
|
843
844
|
"figure.button.open-fastening": "붙임 설정 열기",
|
|
844
845
|
"figure.button.detach-held": "붙임 풀기",
|
|
845
|
-
"figure.text.gizmo-size-turned-fastening": "{part} 부품은
|
|
846
|
-
"figure.
|
|
847
|
-
"figure.
|
|
848
|
-
"figure.label.kept-extreme-wall": "가장 뒤쪽 점",
|
|
849
|
-
"figure.label.kept-extreme-center": "중심",
|
|
846
|
+
"figure.text.gizmo-size-turned-fastening": "{part} 부품은 돌려 붙어 있어 끌어서 크기를 바꿀 수 없습니다.",
|
|
847
|
+
"figure.text.gizmo-size-tilted": "{part} 부품은 기울어 있어 끌어서 크기를 바꿀 수 없습니다.",
|
|
848
|
+
"figure.button.to-size-fields": "크기 칸으로",
|
|
850
849
|
"figure.label.align-input": "위치 칸을 따름 ({field})",
|
|
851
850
|
"figure.label.tab-short-properties": "속성",
|
|
852
851
|
"figure.label.tab-short-capability": "능력",
|
|
@@ -1207,7 +1206,6 @@
|
|
|
1207
1206
|
"figure.label.public": "공개",
|
|
1208
1207
|
"figure.button.make-public": "공개하기",
|
|
1209
1208
|
"figure.button.make-private": "공개 끄기",
|
|
1210
|
-
"figure.button.public-figures": "다른 도메인의 공개 Figure",
|
|
1211
1209
|
"figure.text.category-choose": "분류 고르기",
|
|
1212
1210
|
"figure.text.property-section-failed": "{section} 구역을 그리지 못했습니다: {reason}"
|
|
1213
1211
|
}
|
package/translations/ms.json
CHANGED
|
@@ -507,6 +507,7 @@
|
|
|
507
507
|
"figure.text.what-a-figure-is": "a Figure is a 3D component you author. Compose it from simple shapes and it becomes a component you can place on a drawing, sharing shape and material across every instance.",
|
|
508
508
|
"figure.text.what-changed-in-this-release": "apa yang berubah dalam terbitan ini",
|
|
509
509
|
"figure.title.figures": "katalog figure",
|
|
510
|
+
"figure.title.public-figures": "figure awam",
|
|
510
511
|
"figure.title.new-figure": "new figure",
|
|
511
512
|
"figure.title.save-new-figure": "simpan draf figure",
|
|
512
513
|
"figure.text.no-parts-yet": "No parts yet. Pick a shape above to place one.",
|
|
@@ -842,11 +843,9 @@
|
|
|
842
843
|
"figure.text.held-on-part": "Dilekatkan dengan {face} pada {otherFace} {other}",
|
|
843
844
|
"figure.button.open-fastening": "Buka tetapan lekatan",
|
|
844
845
|
"figure.button.detach-held": "Tanggalkan",
|
|
845
|
-
"figure.text.gizmo-size-turned-fastening": "{part}
|
|
846
|
-
"figure.
|
|
847
|
-
"figure.
|
|
848
|
-
"figure.label.kept-extreme-wall": "titik paling belakang",
|
|
849
|
-
"figure.label.kept-extreme-center": "tengah",
|
|
846
|
+
"figure.text.gizmo-size-turned-fastening": "{part} dilekatkan dalam keadaan berpusing, jadi saiznya tidak boleh diseret.",
|
|
847
|
+
"figure.text.gizmo-size-tilted": "{part} condong, jadi saiznya tidak boleh diseret.",
|
|
848
|
+
"figure.button.to-size-fields": "Ke medan saiz",
|
|
850
849
|
"figure.label.align-input": "Mengikut medannya ({field})",
|
|
851
850
|
"figure.label.tab-short-properties": "Sifat",
|
|
852
851
|
"figure.label.tab-short-capability": "Keupayaan",
|
|
@@ -1207,7 +1206,6 @@
|
|
|
1207
1206
|
"figure.label.public": "awam",
|
|
1208
1207
|
"figure.button.make-public": "jadikan awam",
|
|
1209
1208
|
"figure.button.make-private": "jadikan peribadi",
|
|
1210
|
-
"figure.button.public-figures": "figure awam domain lain",
|
|
1211
1209
|
"figure.text.category-choose": "pilih kategori",
|
|
1212
1210
|
"figure.text.property-section-failed": "bahagian {section} tidak dapat dilukis: {reason}"
|
|
1213
1211
|
}
|