@things-factory/figure-ui 10.1.147 → 10.1.149
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/bootstrap.ts +10 -2
- package/client/graphql/index.ts +20 -0
- package/client/modeller/canvas.ts +6 -2
- package/client/modeller/carrier-declaration.ts +63 -0
- package/client/modeller/editor.ts +62 -27
- package/client/modeller/figure-carrier.ts +117 -0
- package/client/modeller/figure-cavity-drawing.ts +3 -9
- package/client/modeller/figure-first-save.ts +4 -7
- package/client/modeller/figure-history-panel.ts +2 -3
- package/client/modeller/figure-links.ts +46 -60
- package/client/modeller/figure-material-panel.ts +4 -14
- package/client/modeller/figure-mesh-choice.ts +68 -0
- package/client/modeller/figure-parts.ts +23 -1
- package/client/modeller/figure-preview.ts +2 -2
- package/client/modeller/figure-properties.ts +5 -3
- package/client/modeller/figure-section-editor.ts +12 -33
- package/client/modeller/figure-section-panel.ts +6 -7
- package/client/modeller/figure-slot-layout-panel.ts +208 -0
- package/client/modeller/figure-verdict.ts +21 -31
- package/client/modeller/first-save.ts +23 -0
- package/client/modeller/links.ts +52 -1
- package/client/modeller/material-edit.ts +38 -0
- package/client/modeller/maturity.ts +28 -0
- package/client/modeller/mesh-import.ts +77 -0
- package/client/modeller/modeller-route.ts +9 -0
- package/client/modeller/mounting-part.ts +15 -0
- package/client/modeller/newest-answer.ts +21 -0
- package/client/modeller/preview-model.ts +5 -0
- package/client/modeller/refusals.ts +168 -82
- package/client/modeller/section-controls.ts +25 -1
- package/client/modeller/section-drawing.ts +43 -1
- package/client/modeller/slot-layout-host.ts +140 -0
- package/client/pages/figure-modeller-page.ts +6 -5
- package/client/viewparts/detail-route.ts +7 -0
- package/client/viewparts/figure-detail-dock.ts +1 -3
- package/dist-client/bootstrap.d.ts +1 -1
- package/dist-client/bootstrap.js +9 -2
- package/dist-client/bootstrap.js.map +1 -1
- package/dist-client/graphql/index.d.ts +5 -0
- package/dist-client/graphql/index.js +19 -0
- package/dist-client/graphql/index.js.map +1 -1
- package/dist-client/modeller/canvas.d.ts +2 -0
- package/dist-client/modeller/canvas.js +10 -2
- package/dist-client/modeller/canvas.js.map +1 -1
- package/dist-client/modeller/carrier-declaration.d.ts +31 -0
- package/dist-client/modeller/carrier-declaration.js +53 -0
- package/dist-client/modeller/carrier-declaration.js.map +1 -0
- package/dist-client/modeller/editor.d.ts +13 -0
- package/dist-client/modeller/editor.js +62 -27
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/figure-carrier.d.ts +26 -0
- package/dist-client/modeller/figure-carrier.js +123 -0
- package/dist-client/modeller/figure-carrier.js.map +1 -0
- package/dist-client/modeller/figure-cavity-drawing.js +3 -11
- package/dist-client/modeller/figure-cavity-drawing.js.map +1 -1
- package/dist-client/modeller/figure-first-save.d.ts +4 -0
- package/dist-client/modeller/figure-first-save.js +5 -8
- package/dist-client/modeller/figure-first-save.js.map +1 -1
- package/dist-client/modeller/figure-history-panel.js +2 -4
- package/dist-client/modeller/figure-history-panel.js.map +1 -1
- package/dist-client/modeller/figure-links.d.ts +4 -1
- package/dist-client/modeller/figure-links.js +47 -57
- package/dist-client/modeller/figure-links.js.map +1 -1
- package/dist-client/modeller/figure-material-panel.d.ts +0 -1
- package/dist-client/modeller/figure-material-panel.js +4 -14
- package/dist-client/modeller/figure-material-panel.js.map +1 -1
- package/dist-client/modeller/figure-mesh-choice.d.ts +16 -0
- package/dist-client/modeller/figure-mesh-choice.js +75 -0
- package/dist-client/modeller/figure-mesh-choice.js.map +1 -0
- package/dist-client/modeller/figure-parts.d.ts +1 -1
- package/dist-client/modeller/figure-parts.js +22 -0
- package/dist-client/modeller/figure-parts.js.map +1 -1
- package/dist-client/modeller/figure-preview.js +2 -2
- package/dist-client/modeller/figure-preview.js.map +1 -1
- package/dist-client/modeller/figure-properties.d.ts +2 -1
- package/dist-client/modeller/figure-properties.js +5 -3
- package/dist-client/modeller/figure-properties.js.map +1 -1
- package/dist-client/modeller/figure-section-editor.d.ts +2 -2
- package/dist-client/modeller/figure-section-editor.js +12 -33
- package/dist-client/modeller/figure-section-editor.js.map +1 -1
- package/dist-client/modeller/figure-section-panel.js +6 -7
- package/dist-client/modeller/figure-section-panel.js.map +1 -1
- package/dist-client/modeller/figure-slot-layout-panel.d.ts +36 -0
- package/dist-client/modeller/figure-slot-layout-panel.js +198 -0
- package/dist-client/modeller/figure-slot-layout-panel.js.map +1 -0
- package/dist-client/modeller/figure-verdict.d.ts +1 -1
- package/dist-client/modeller/figure-verdict.js +19 -37
- package/dist-client/modeller/figure-verdict.js.map +1 -1
- package/dist-client/modeller/first-save.d.ts +10 -0
- package/dist-client/modeller/first-save.js +16 -0
- package/dist-client/modeller/first-save.js.map +1 -1
- package/dist-client/modeller/links.d.ts +26 -1
- package/dist-client/modeller/links.js +43 -0
- package/dist-client/modeller/links.js.map +1 -1
- package/dist-client/modeller/material-edit.d.ts +11 -0
- package/dist-client/modeller/material-edit.js +28 -0
- package/dist-client/modeller/material-edit.js.map +1 -0
- package/dist-client/modeller/maturity.d.ts +15 -0
- package/dist-client/modeller/maturity.js +23 -0
- package/dist-client/modeller/maturity.js.map +1 -1
- package/dist-client/modeller/mesh-import.d.ts +47 -0
- package/dist-client/modeller/mesh-import.js +47 -0
- package/dist-client/modeller/mesh-import.js.map +1 -0
- package/dist-client/modeller/modeller-route.d.ts +5 -0
- package/dist-client/modeller/modeller-route.js +9 -0
- package/dist-client/modeller/modeller-route.js.map +1 -1
- package/dist-client/modeller/mounting-part.d.ts +14 -0
- package/dist-client/modeller/mounting-part.js +10 -0
- package/dist-client/modeller/mounting-part.js.map +1 -0
- package/dist-client/modeller/newest-answer.d.ts +12 -0
- package/dist-client/modeller/newest-answer.js +21 -0
- package/dist-client/modeller/newest-answer.js.map +1 -0
- package/dist-client/modeller/preview-model.d.ts +2 -0
- package/dist-client/modeller/preview-model.js +4 -0
- package/dist-client/modeller/preview-model.js.map +1 -1
- package/dist-client/modeller/refusals.d.ts +26 -5
- package/dist-client/modeller/refusals.js +184 -86
- package/dist-client/modeller/refusals.js.map +1 -1
- package/dist-client/modeller/section-controls.d.ts +17 -1
- package/dist-client/modeller/section-controls.js +18 -0
- package/dist-client/modeller/section-controls.js.map +1 -1
- package/dist-client/modeller/section-drawing.d.ts +24 -1
- package/dist-client/modeller/section-drawing.js +42 -0
- package/dist-client/modeller/section-drawing.js.map +1 -1
- package/dist-client/modeller/slot-layout-host.d.ts +81 -0
- package/dist-client/modeller/slot-layout-host.js +105 -0
- package/dist-client/modeller/slot-layout-host.js.map +1 -0
- package/dist-client/pages/figure-modeller-page.js +5 -4
- package/dist-client/pages/figure-modeller-page.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/viewparts/detail-route.d.ts +5 -0
- package/dist-client/viewparts/detail-route.js +8 -0
- package/dist-client/viewparts/detail-route.js.map +1 -0
- package/dist-client/viewparts/figure-detail-dock.d.ts +0 -1
- package/dist-client/viewparts/figure-detail-dock.js +1 -3
- package/dist-client/viewparts/figure-detail-dock.js.map +1 -1
- package/package.json +9 -8
- package/test/align.test.ts +0 -9
- package/test/browser/assistant-stand-in.js +32 -0
- package/test/browser/build.mjs +6 -1
- package/test/browser/editing-page.js +224 -0
- package/test/browser/editing.check.mjs +550 -0
- package/test/browser/links-page.js +113 -0
- package/test/browser/links.check.mjs +568 -0
- package/test/browser/modeller.check.mjs +21 -0
- package/test/browser/parts-page.js +120 -0
- package/test/browser/parts.check.mjs +328 -0
- package/test/browser/places-mesh-page.js +36 -0
- package/test/browser/places-mesh.check.mjs +239 -0
- package/test/browser/product-page.check.mjs +682 -0
- package/test/browser/product-page.js +276 -0
- package/test/browser/screen-page.js +112 -0
- package/test/browser/screen-product-page.js +62 -0
- package/test/browser/screen.check.mjs +1003 -0
- package/test/browser/section-page.js +256 -0
- package/test/browser/section.check.mjs +719 -0
- package/test/browser/serve.mjs +20 -12
- package/test/carrier-declaration.test.ts +69 -0
- package/test/cavity-drawing.test.ts +2 -18
- package/test/clip-ai-proposal.test.ts +1 -5
- package/test/clip-consent.test.ts +0 -5
- package/test/clips.test.ts +0 -7
- package/test/command-preview.test.ts +0 -68
- package/test/controls.test.ts +0 -9
- package/test/detail-dock.test.ts +6 -25
- package/test/face-mapping.test.ts +1 -20
- package/test/fasten-drawing.test.ts +0 -23
- package/test/fields.test.ts +6 -33
- package/test/figure-assistant-surface.test.ts +32 -38
- package/test/first-minute.test.ts +3 -55
- package/test/first-save-suggest.test.ts +11 -8
- package/test/first-save.test.ts +4 -26
- package/test/gizmo.test.ts +0 -25
- package/test/joint-fan.test.ts +0 -10
- package/test/keys.test.ts +0 -7
- package/test/layout.test.ts +4 -73
- package/test/links.test.ts +48 -67
- package/test/live.test.ts +0 -34
- package/test/material.test.ts +17 -26
- package/test/maturity.test.ts +13 -16
- package/test/mesh-import.test.ts +76 -0
- package/test/modeller-route.test.ts +3 -19
- package/test/package-download.test.ts +6 -7
- package/test/parts-wiring.test.ts +9 -51
- package/test/preview.test.ts +5 -46
- package/test/properties.test.ts +11 -26
- package/test/proposal-record.test.ts +2 -19
- package/test/recipes.test.ts +0 -15
- package/test/refusals.test.ts +257 -151
- package/test/release-entry.test.ts +1 -9
- package/test/release-panel.test.ts +1 -22
- package/test/resize-with-placement.test.ts +0 -32
- package/test/revert-choice.test.ts +1 -22
- package/test/revert-keeping.test.ts +1 -12
- package/test/save-landed.test.ts +16 -14
- package/test/scene-canvas.test.ts +2 -13
- package/test/section-editor.test.ts +30 -83
- package/test/section-panel.test.ts +22 -17
- package/test/slot-layout-host.test.ts +117 -0
- package/test/theme.test.ts +5 -9
- package/test/verdict.test.ts +44 -41
- package/test/view.test.ts +5 -12
- package/translations/en.json +61 -1
- package/translations/es.json +61 -1
- package/translations/ja.json +61 -1
- package/translations/ko.json +61 -1
- package/translations/ms.json +61 -1
- package/translations/zh.json +61 -1
- package/test/thumbnail.test.ts +0 -29
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The page `parts.check.mjs` drives: the editor of `editing-page.js`, with the figures the parts list needs -- three
|
|
3
|
+
* boxes in a row, a frame with a leaf fastened to it, a rounded box -- and what a person sees of the list: its rows,
|
|
4
|
+
* their buttons, the palette, the copies the canvas shows selected.
|
|
5
|
+
* Only reading: every change the check makes goes through the mouse and the keyboard.
|
|
6
|
+
*/
|
|
7
|
+
import './editing-page.js'
|
|
8
|
+
import { addFigureBox, applyFigureAuthoring, createFigureAsset, serializeFigureAsset } from '@hatiolab/figure-model'
|
|
9
|
+
|
|
10
|
+
const PALETTE = {
|
|
11
|
+
'palette.dark': '#2b2b2b',
|
|
12
|
+
'palette.neutral': '#9a9a9a',
|
|
13
|
+
'palette.primary': '#477b9e',
|
|
14
|
+
'palette.accent': '#f79c40',
|
|
15
|
+
'palette.light': '#dddddd'
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function figure(name) {
|
|
19
|
+
const a = createFigureAsset(name)
|
|
20
|
+
a.palette = { ...a.palette, ...PALETTE }
|
|
21
|
+
return a
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
Object.assign(window.figures, {
|
|
25
|
+
/* Three boxes side by side on the floor. */
|
|
26
|
+
three: () => {
|
|
27
|
+
let a = figure('Three')
|
|
28
|
+
for (const [id, x] of [['a', -600], ['b', 0], ['c', 600]]) a = addFigureBox(a, { id, size: { x: 400, y: 400, z: 400 }, position: { x, y: 200, z: 0 } })
|
|
29
|
+
return serializeFigureAsset(a)
|
|
30
|
+
},
|
|
31
|
+
/* A frame with a leaf fastened on its top: taking the frame away lets the leaf go. */
|
|
32
|
+
fastened: () => {
|
|
33
|
+
let a = figure('Fastened')
|
|
34
|
+
a = addFigureBox(a, { id: 'frame', size: { x: 1600, y: 100, z: 150 }, position: { x: 0, y: 50, z: 0 } })
|
|
35
|
+
a = addFigureBox(a, { id: 'leaf', size: { x: 1400, y: 1000, z: 40 }, position: { x: 0, y: 600, z: 0 } })
|
|
36
|
+
return serializeFigureAsset(applyFigureAuthoring(a, { kind: 'attach', part: 'leaf', face: 'bottom', to: { part: 'frame', face: 'top' } }))
|
|
37
|
+
},
|
|
38
|
+
/* A box with rounded corners: made from the box, still called what it is. */
|
|
39
|
+
rounded: () => {
|
|
40
|
+
let a = figure('Rounded')
|
|
41
|
+
a = addFigureBox(a, { id: 'tray', size: { x: 600, y: 200, z: 400 }, position: { x: 0, y: 100, z: 0 } })
|
|
42
|
+
return serializeFigureAsset(applyFigureAuthoring(a, { kind: 'set-section-corner', part: 'tray', corner: 40 }))
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
function deep(selector, root = document) {
|
|
47
|
+
const found = [...root.querySelectorAll(selector)]
|
|
48
|
+
for (const element of root.querySelectorAll('*')) if (element.shadowRoot) found.push(...deep(selector, element.shadowRoot))
|
|
49
|
+
return found
|
|
50
|
+
}
|
|
51
|
+
function centreOf(element) {
|
|
52
|
+
element.scrollIntoView({ block: 'center' })
|
|
53
|
+
const box = element.getBoundingClientRect()
|
|
54
|
+
return { x: box.left + box.width / 2, y: box.top + box.height / 2 }
|
|
55
|
+
}
|
|
56
|
+
const panel = () => deep('figure-parts')[0]
|
|
57
|
+
const canvas = () => deep('figure-canvas')[0]
|
|
58
|
+
const rowOf = id => [...panel().shadowRoot.querySelectorAll('li')].find(li => li.querySelector('span[label]')?.getAttribute('title') === id)
|
|
59
|
+
|
|
60
|
+
window.parts = {
|
|
61
|
+
/** The list's rows as a person reads them: the name shown, its icon, whether it is selected or hidden. */
|
|
62
|
+
rows() {
|
|
63
|
+
return [...panel().shadowRoot.querySelectorAll('li')]
|
|
64
|
+
.filter(li => li.querySelector('span[label]'))
|
|
65
|
+
.map(li => ({
|
|
66
|
+
id: li.querySelector('span[label]').getAttribute('title'),
|
|
67
|
+
shown: li.querySelector('span[label]').childNodes[0]?.textContent.trim(),
|
|
68
|
+
icon: [...li.querySelectorAll(':scope > md-icon')].map(i => i.textContent.trim())[0],
|
|
69
|
+
selected: li.hasAttribute('selected'),
|
|
70
|
+
hidden: li.hasAttribute('off')
|
|
71
|
+
}))
|
|
72
|
+
},
|
|
73
|
+
/** The page point of a row's name (a click on it selects the part). */
|
|
74
|
+
row(id) {
|
|
75
|
+
const li = rowOf(id)
|
|
76
|
+
return li ? centreOf(li.querySelector('span[label]')) : null
|
|
77
|
+
},
|
|
78
|
+
/** The page point of a row's eye. */
|
|
79
|
+
eye(id) {
|
|
80
|
+
const b = rowOf(id)?.querySelector('button[eye]')
|
|
81
|
+
return b ? centreOf(b) : null
|
|
82
|
+
},
|
|
83
|
+
/** The page point of the selected row's operation (`move-part-up`, `duplicate-part`, ...). */
|
|
84
|
+
op(name) {
|
|
85
|
+
const b = panel().shadowRoot.querySelector(`li[selected] button[op="${name}"]`)
|
|
86
|
+
return b ? { ...centreOf(b), disabled: b.disabled } : null
|
|
87
|
+
},
|
|
88
|
+
/** The palette's shape buttons: what each says (keys: no translations are loaded) and whether it can be pressed. */
|
|
89
|
+
palette() {
|
|
90
|
+
return [...panel().shadowRoot.querySelectorAll('div[palette] button[shape]')].map(b => ({ label: b.querySelector('span').textContent.trim(), disabled: b.disabled, ...centreOf(b) }))
|
|
91
|
+
},
|
|
92
|
+
/** The detail level choice in the list's head. */
|
|
93
|
+
levelChoice() {
|
|
94
|
+
const select = panel().shadowRoot.querySelector('select')
|
|
95
|
+
return select ? centreOf(select) : null
|
|
96
|
+
},
|
|
97
|
+
/** How the selected row lays out: its name and its buttons, as boxes. */
|
|
98
|
+
selectedRowBoxes() {
|
|
99
|
+
const li = panel().shadowRoot.querySelector('li[selected]')
|
|
100
|
+
if (!li) return null
|
|
101
|
+
li.scrollIntoView({ block: 'center' })
|
|
102
|
+
const box = e => {
|
|
103
|
+
const r = e.getBoundingClientRect()
|
|
104
|
+
return { left: r.left, right: r.right, top: r.top, bottom: r.bottom }
|
|
105
|
+
}
|
|
106
|
+
return { row: box(li), name: box(li.querySelector('span[label]')), ops: box(li.querySelector('div[ops]')) }
|
|
107
|
+
},
|
|
108
|
+
/** The list made this wide (the start pane narrowed by the author). */
|
|
109
|
+
narrow(px) {
|
|
110
|
+
panel().style.width = px ? `${px}px` : ''
|
|
111
|
+
},
|
|
112
|
+
/** The names of the drawn parts the canvas holds selected now. */
|
|
113
|
+
canvasSelected() {
|
|
114
|
+
return (canvas().scene.root.root.selected ?? []).map(c => c.state.name)
|
|
115
|
+
},
|
|
116
|
+
/** The names of the parts the canvas draws now. */
|
|
117
|
+
drawn() {
|
|
118
|
+
return canvas().scene.root.components.map(c => c.state.name).filter(n => !n.startsWith('__'))
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The parts list and what hangs on it, in a real browser: every button the list shows does what it says, as one step
|
|
3
|
+
* of the session; hiding is the workbench's, not the figure's; what a removal lets go of or is refused for is told
|
|
4
|
+
* through the app's notices; the narrow list keeps the selected row's buttons in sight; the palette offers the
|
|
5
|
+
* starting shapes and locks at the detail level's limit; a row is one part, selected whole, its pitch one value; the
|
|
6
|
+
* long explanations open from their 「?」.
|
|
7
|
+
*
|
|
8
|
+
* Run: node --test test/browser/parts.check.mjs
|
|
9
|
+
*/
|
|
10
|
+
import { test, describe, before, after } from 'node:test'
|
|
11
|
+
import assert from 'node:assert/strict'
|
|
12
|
+
|
|
13
|
+
import { buildPage } from './build.mjs'
|
|
14
|
+
import { openPage } from './serve.mjs'
|
|
15
|
+
|
|
16
|
+
const sleep = ms => new Promise(done => setTimeout(done, ms))
|
|
17
|
+
|
|
18
|
+
let opened
|
|
19
|
+
before(async () => {
|
|
20
|
+
opened = await openPage(await buildPage('parts-page.js'))
|
|
21
|
+
})
|
|
22
|
+
after(async () => {
|
|
23
|
+
await opened?.close()
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const page = () => opened.page
|
|
27
|
+
const read = (fn, ...args) => page().evaluate(fn, ...args)
|
|
28
|
+
const state = () => read(() => probe.state())
|
|
29
|
+
const revision = async () => (await state()).revision
|
|
30
|
+
const rows = () => read(() => parts.rows())
|
|
31
|
+
const ids = async () => (await rows()).map(r => r.id)
|
|
32
|
+
const UNDO = ['Control', 'KeyZ']
|
|
33
|
+
|
|
34
|
+
/** Waits until `check` holds, reading again every 50 ms; fails with `what` after `ms`. */
|
|
35
|
+
async function until(check, what, ms = 3000) {
|
|
36
|
+
for (const end = Date.now() + ms; Date.now() < end; await sleep(50)) if (await check()) return
|
|
37
|
+
assert.fail(`waited for ${what}`)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Mounts one of the harness's figures (`rollers`) or one of the page's, and waits for it to be drawn. */
|
|
41
|
+
async function mount(kind, figure) {
|
|
42
|
+
await read((kind, figure) => harness.mount(kind, figure ? figures[figure]() : null), kind, figure)
|
|
43
|
+
await until(
|
|
44
|
+
() =>
|
|
45
|
+
read(() => {
|
|
46
|
+
/* Not drawn yet is not an error: until the canvas stands, reading it throws. */
|
|
47
|
+
try {
|
|
48
|
+
return probe.ids().some(id => !!probe.partOnPage(id))
|
|
49
|
+
} catch {
|
|
50
|
+
return false
|
|
51
|
+
}
|
|
52
|
+
}),
|
|
53
|
+
'the figure drawn',
|
|
54
|
+
10000
|
|
55
|
+
)
|
|
56
|
+
await sleep(800)
|
|
57
|
+
await read(() => editing.told())
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function click(point, what = 'a point') {
|
|
61
|
+
assert.ok(point, `nothing on the page to click: ${what}`)
|
|
62
|
+
await page().mouse.click(point.x, point.y)
|
|
63
|
+
await sleep(300)
|
|
64
|
+
}
|
|
65
|
+
const pick = async id => {
|
|
66
|
+
await click(await read(id => parts.row(id), id), `the row ${id}`)
|
|
67
|
+
assert.equal((await state()).selected, id, `${id} is picked from the list`)
|
|
68
|
+
}
|
|
69
|
+
async function press(...keys) {
|
|
70
|
+
await read(() => {
|
|
71
|
+
const deepest = root => (root.activeElement?.shadowRoot ? deepest(root.activeElement.shadowRoot) : root.activeElement)
|
|
72
|
+
deepest(document)?.blur?.()
|
|
73
|
+
})
|
|
74
|
+
for (const key of keys.slice(0, -1)) await page().keyboard.down(key)
|
|
75
|
+
await page().keyboard.press(keys.at(-1))
|
|
76
|
+
for (const key of keys.slice(0, -1).reverse()) await page().keyboard.up(key)
|
|
77
|
+
await sleep(400)
|
|
78
|
+
}
|
|
79
|
+
async function tab(name) {
|
|
80
|
+
await click(await read(name => probe.tab(name), `figure.label.tab-short-${name}`), `the ${name} tab`)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
describe('the parts list: each button does what it says, one step of the session each', () => {
|
|
84
|
+
before(() => mount('box', 'three'))
|
|
85
|
+
|
|
86
|
+
test('a row picked is the part selected, on the list and on the canvas', async () => {
|
|
87
|
+
await pick('b')
|
|
88
|
+
assert.deepEqual((await rows()).filter(r => r.selected).map(r => r.id), ['b'])
|
|
89
|
+
assert.deepEqual(await read(() => parts.canvasSelected()), ['b'])
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
test('moved up: the order changes in one step, and one undo puts it back', async () => {
|
|
93
|
+
const before = await revision()
|
|
94
|
+
await click(await read(() => parts.op('move-part-up')), 'move up')
|
|
95
|
+
assert.deepEqual(await ids(), ['b', 'a', 'c'])
|
|
96
|
+
assert.equal(await revision(), before + 1)
|
|
97
|
+
await press(...UNDO)
|
|
98
|
+
assert.deepEqual(await ids(), ['a', 'b', 'c'])
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
test('moved down: the same, the other way', async () => {
|
|
102
|
+
await pick('b')
|
|
103
|
+
const before = await revision()
|
|
104
|
+
await click(await read(() => parts.op('move-part-down')), 'move down')
|
|
105
|
+
assert.deepEqual(await ids(), ['a', 'c', 'b'])
|
|
106
|
+
assert.equal(await revision(), before + 1)
|
|
107
|
+
await press(...UNDO)
|
|
108
|
+
assert.deepEqual(await ids(), ['a', 'b', 'c'])
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
test('duplicated: a new row in one step, and the copy is the part selected', async () => {
|
|
112
|
+
await pick('b')
|
|
113
|
+
const before = await revision()
|
|
114
|
+
await click(await read(() => parts.op('duplicate-part')), 'duplicate')
|
|
115
|
+
const now = await ids()
|
|
116
|
+
assert.equal(now.length, 4, now.join())
|
|
117
|
+
const copy = now.find(id => !['a', 'b', 'c'].includes(id))
|
|
118
|
+
assert.equal((await state()).selected, copy)
|
|
119
|
+
assert.equal(await revision(), before + 1)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
test('removed: the row goes in one step', async () => {
|
|
123
|
+
const copy = (await state()).selected
|
|
124
|
+
const before = await revision()
|
|
125
|
+
await click(await read(() => parts.op('remove-part')), 'remove')
|
|
126
|
+
assert.deepEqual(await ids(), ['a', 'b', 'c'])
|
|
127
|
+
assert.ok(!(await read(() => probe.ids())).includes(copy))
|
|
128
|
+
assert.equal(await revision(), before + 1)
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
test('a shape from the palette is a new part in one step, selected', async () => {
|
|
132
|
+
const before = await revision()
|
|
133
|
+
const box = (await read(() => parts.palette())).find(b => b.label === 'figure.label.primitive-cube')
|
|
134
|
+
await click(box, 'the palette box')
|
|
135
|
+
const now = await ids()
|
|
136
|
+
assert.equal(now.length, 4, now.join())
|
|
137
|
+
assert.equal((await state()).selected, now.find(id => !['a', 'b', 'c'].includes(id)))
|
|
138
|
+
assert.equal(await revision(), before + 1)
|
|
139
|
+
await press(...UNDO)
|
|
140
|
+
assert.deepEqual(await ids(), ['a', 'b', 'c'])
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
describe('hidden on the workbench: off the canvas, not out of the figure, and let go when another figure opens', () => {
|
|
145
|
+
before(() => mount('box', 'three'))
|
|
146
|
+
|
|
147
|
+
test('a row\'s eye takes its part off the canvas and back, writing nothing', async () => {
|
|
148
|
+
const before = await revision()
|
|
149
|
+
await click(await read(() => parts.eye('a')), 'the eye of a')
|
|
150
|
+
await until(async () => !(await read(() => parts.drawn())).includes('a'), 'a taken off the canvas')
|
|
151
|
+
assert.equal((await rows()).find(r => r.id === 'a').hidden, true)
|
|
152
|
+
assert.ok((await read(() => probe.ids())).includes('a'), 'still in the figure')
|
|
153
|
+
assert.equal(await revision(), before, 'nothing written')
|
|
154
|
+
await click(await read(() => parts.eye('a')), 'the eye of a')
|
|
155
|
+
await until(async () => (await read(() => parts.drawn())).includes('a'), 'a drawn again')
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
test('the list head\'s eye hides every part, and shows them again', async () => {
|
|
159
|
+
await click(await read(() => editing.at('button[eye][all]', 'figure-parts')), 'hide all')
|
|
160
|
+
await until(async () => (await read(() => parts.drawn())).length === 0, 'nothing drawn')
|
|
161
|
+
await click(await read(() => editing.at('button[eye][all]', 'figure-parts')), 'show all')
|
|
162
|
+
await until(async () => (await read(() => parts.drawn())).length === 3, 'all three drawn')
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
test('another figure opened in the same editor starts with nothing hidden', async () => {
|
|
166
|
+
await click(await read(() => parts.eye('a')), 'the eye of a')
|
|
167
|
+
await until(async () => !(await read(() => parts.drawn())).includes('a'), 'a taken off the canvas')
|
|
168
|
+
await read(async () => {
|
|
169
|
+
const editor = document.querySelector('figure-editor')
|
|
170
|
+
await editor.session.open('another')
|
|
171
|
+
editor.revision = editor.revision + 1
|
|
172
|
+
await editor.updateComplete
|
|
173
|
+
})
|
|
174
|
+
await until(async () => (await read(() => parts.drawn())).includes('a'), 'a drawn in the figure opened next')
|
|
175
|
+
assert.equal((await rows()).find(r => r.id === 'a').hidden, false)
|
|
176
|
+
})
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
describe('a removal is told: what it let go of as a notice, a refusal as an error', () => {
|
|
180
|
+
test('the frame removed: the leaf fastened to it stays, and the notice names what was let go', async () => {
|
|
181
|
+
await mount('box', 'fastened')
|
|
182
|
+
await pick('frame')
|
|
183
|
+
await click(await read(() => parts.op('remove-part')), 'remove')
|
|
184
|
+
assert.deepEqual(await ids(), ['leaf'])
|
|
185
|
+
const told = await read(() => editing.told())
|
|
186
|
+
assert.equal(told.length, 1, JSON.stringify(told))
|
|
187
|
+
assert.equal(told[0].level, 'info')
|
|
188
|
+
assert.match(told[0].message, /^figure\.text\.removal-/)
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
test('a part the row is carried on is refused, said as an error the author closes, and nothing is removed', async () => {
|
|
192
|
+
await mount('rollers')
|
|
193
|
+
const before = await revision()
|
|
194
|
+
await pick('roller')
|
|
195
|
+
await click(await read(() => parts.op('remove-part')), 'remove')
|
|
196
|
+
assert.ok((await ids()).includes('roller'), 'still there')
|
|
197
|
+
assert.equal(await revision(), before)
|
|
198
|
+
const told = await read(() => editing.told())
|
|
199
|
+
assert.equal(told.length, 1, JSON.stringify(told))
|
|
200
|
+
assert.equal(told[0].level, 'error')
|
|
201
|
+
assert.match(told[0].message, /^figure\.text\./, 'in words, not the kernel\'s sentence')
|
|
202
|
+
})
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
describe('the list narrowed: the selected row keeps its name and its buttons in sight', () => {
|
|
206
|
+
before(() => mount('box', 'three'))
|
|
207
|
+
after(() => read(() => parts.narrow(0)))
|
|
208
|
+
|
|
209
|
+
test('wide, the buttons stand beside the name; under 200 px they go to a line of their own, inside the row', async () => {
|
|
210
|
+
await pick('b')
|
|
211
|
+
await read(() => parts.narrow(320))
|
|
212
|
+
await sleep(200)
|
|
213
|
+
let at = await read(() => parts.selectedRowBoxes())
|
|
214
|
+
assert.ok(at.ops.top < at.name.bottom, `beside the name: ${JSON.stringify(at)}`)
|
|
215
|
+
await read(() => parts.narrow(180))
|
|
216
|
+
await sleep(200)
|
|
217
|
+
at = await read(() => parts.selectedRowBoxes())
|
|
218
|
+
assert.ok(at.ops.top >= at.name.bottom - 1, `under the name: ${JSON.stringify(at)}`)
|
|
219
|
+
assert.ok(at.ops.right <= at.row.right + 1 && at.name.right <= at.row.right + 1, `inside the row: ${JSON.stringify(at)}`)
|
|
220
|
+
})
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
describe('the palette: the starting shapes, locked at the detail level\'s limit', () => {
|
|
224
|
+
test('nine starting shapes, in order; a rounded box or a drum is made from the box', async () => {
|
|
225
|
+
await mount('box', 'three')
|
|
226
|
+
assert.deepEqual(
|
|
227
|
+
(await read(() => parts.palette())).map(b => b.label),
|
|
228
|
+
['cube', 'cylinder', 'sphere', 'frustum', 'polygon', 'pyramid', 'plate', 'wall', 'portal'].map(k => `figure.label.primitive-${k}`)
|
|
229
|
+
)
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
test('at the level\'s part limit every shape is locked; with no level, none is', async () => {
|
|
233
|
+
await mount('box', 'three')
|
|
234
|
+
const choice = await read(() => parts.levelChoice())
|
|
235
|
+
await read(() => {
|
|
236
|
+
const select = [...document.querySelector('figure-editor').renderRoot.querySelector('figure-parts').shadowRoot.querySelectorAll('select')][0]
|
|
237
|
+
select.value = 'S'
|
|
238
|
+
select.dispatchEvent(new Event('change', { bubbles: true }))
|
|
239
|
+
})
|
|
240
|
+
assert.ok(choice, 'the level is chosen in the list head')
|
|
241
|
+
await until(async () => (await read(() => parts.palette())).every(b => b.disabled), 'the palette locked at S (three parts)')
|
|
242
|
+
await press(...UNDO)
|
|
243
|
+
await until(async () => (await read(() => parts.palette())).every(b => !b.disabled), 'the palette open again')
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
test('a box with rounded corners is listed with its own icon', async () => {
|
|
247
|
+
await mount('box', 'rounded')
|
|
248
|
+
assert.equal((await rows()).find(r => r.id === 'tray').icon, 'crop_square')
|
|
249
|
+
})
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
describe('a row is one part: selected whole, its pitch one value set by the row (2026-09-24)', () => {
|
|
253
|
+
before(() => mount('rollers'))
|
|
254
|
+
|
|
255
|
+
test('the roller picked: every copy of it is selected on the canvas, and nothing else', async () => {
|
|
256
|
+
await pick('roller')
|
|
257
|
+
const copies = (await read(() => probe.ids())).filter(id => /^\["roller\.repeat",\d+,"roller\./.test(id))
|
|
258
|
+
assert.ok(copies.length > 1, copies.join())
|
|
259
|
+
assert.deepEqual((await read(() => parts.canvasSelected())).sort(), copies.sort())
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
test('the pitch is offered once, and typed it is one step that sets the row again', async () => {
|
|
263
|
+
await pick('roller')
|
|
264
|
+
await tab('properties')
|
|
265
|
+
const fields = await read(() => editing.propertyInputs())
|
|
266
|
+
assert.equal(fields.filter(label => /pitch|repeat/i.test(label ?? '')).length, 1, fields.join())
|
|
267
|
+
const label = fields.find(label => /pitch|repeat/i.test(label ?? ''))
|
|
268
|
+
const rollers = async () => (await read(() => probe.ids())).filter(id => /^\["roller\.repeat",\d+,"roller\./.test(id)).length
|
|
269
|
+
const copiesBefore = await rollers()
|
|
270
|
+
const before = await revision()
|
|
271
|
+
const at = await read(label => probe.labelled(label), label)
|
|
272
|
+
await page().mouse.click(at.x, at.y, { clickCount: 3 })
|
|
273
|
+
await page().keyboard.type('400')
|
|
274
|
+
await page().keyboard.press('Enter')
|
|
275
|
+
await sleep(500)
|
|
276
|
+
assert.equal(await revision(), before + 1, 'one step')
|
|
277
|
+
const copiesAfter = await rollers()
|
|
278
|
+
assert.ok(copiesAfter < copiesBefore, `a wider pitch makes fewer copies on the same length: ${copiesBefore} -> ${copiesAfter}`)
|
|
279
|
+
assert.equal(Number(await read(label => editing.fieldValue(label), label)), 400, 'the field shows the pitch the row holds')
|
|
280
|
+
})
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
describe('the long explanations open from their 「?」 beside the title, closed until asked', () => {
|
|
284
|
+
test('motion, place, fastening and clips: each shut, opened by its 「?」, shut again', async () => {
|
|
285
|
+
await mount('box', 'doors')
|
|
286
|
+
await pick('left')
|
|
287
|
+
const seen = new Set()
|
|
288
|
+
for (const name of ['properties', 'capability', 'motion']) {
|
|
289
|
+
await tab(name)
|
|
290
|
+
const helps = await read(() =>
|
|
291
|
+
[...document.querySelector('figure-editor').renderRoot.querySelectorAll('figure-links, figure-clips')].flatMap((element, host) =>
|
|
292
|
+
[...element.shadowRoot.querySelectorAll('button[help]')].map((b, i) => ({ host, i, seen: b.getClientRects().length > 0 })).filter(b => b.seen)
|
|
293
|
+
)
|
|
294
|
+
)
|
|
295
|
+
for (const { host, i } of helps) {
|
|
296
|
+
const hint = () =>
|
|
297
|
+
read(
|
|
298
|
+
(host, i) => {
|
|
299
|
+
const b = document.querySelector('figure-editor').renderRoot.querySelectorAll('figure-links, figure-clips')[host].shadowRoot.querySelectorAll('button[help]')[i]
|
|
300
|
+
return [...b.closest('section').querySelectorAll(':scope > p[hint]')].map(p => p.textContent.trim())
|
|
301
|
+
},
|
|
302
|
+
host,
|
|
303
|
+
i
|
|
304
|
+
)
|
|
305
|
+
const button = () =>
|
|
306
|
+
read(
|
|
307
|
+
(host, i) => {
|
|
308
|
+
const b = document.querySelector('figure-editor').renderRoot.querySelectorAll('figure-links, figure-clips')[host].shadowRoot.querySelectorAll('button[help]')[i]
|
|
309
|
+
b.scrollIntoView({ block: 'center' })
|
|
310
|
+
const r = b.getBoundingClientRect()
|
|
311
|
+
return { x: r.left + r.width / 2, y: r.top + r.height / 2 }
|
|
312
|
+
},
|
|
313
|
+
host,
|
|
314
|
+
i
|
|
315
|
+
)
|
|
316
|
+
const shut = await hint()
|
|
317
|
+
await click(await button(), 'the ?')
|
|
318
|
+
const said = (await hint()).filter(text => !shut.includes(text))
|
|
319
|
+
assert.equal(said.length, 1, `${host} ${i}: one explanation opened (${said.join(', ')})`)
|
|
320
|
+
assert.ok(!shut.includes(said[0]), `${host} ${i}: shut until asked`)
|
|
321
|
+
seen.add(said[0])
|
|
322
|
+
await click(await button(), 'the ?')
|
|
323
|
+
assert.deepEqual(await hint(), shut, `${host} ${i}: shut again`)
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
for (const key of ['figure.text.motion-explained', 'figure.text.place-explained', 'figure.text.fastening-explained', 'figure.text.clips-explained']) assert.ok(seen.has(key), `${key} among ${[...seen].join(', ')}`)
|
|
327
|
+
})
|
|
328
|
+
})
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The page `places-mesh.check.mjs` drives: the modeller's editor with popups drawn as the layout draws them
|
|
3
|
+
* (`section-page.js`) and the readings of `links-page.js`, a server for GLB uploads answered in memory, and the client
|
|
4
|
+
* bootstrap as the app runs it. Only reading here, apart from the answer the server is told to give.
|
|
5
|
+
*/
|
|
6
|
+
import './section-page.js'
|
|
7
|
+
import './links-page.js'
|
|
8
|
+
import { client } from '@operato/graphql'
|
|
9
|
+
import { figureMeshUrlOf } from '@hatiolab/things-scene'
|
|
10
|
+
import bootstrap from 'figure-ui-client/bootstrap.js'
|
|
11
|
+
|
|
12
|
+
/* The uploads the editor sent, and what the server answers the next one: the read GLB, or a refusal in its words. */
|
|
13
|
+
const uploads = []
|
|
14
|
+
let next = null
|
|
15
|
+
client.mutate = async ({ mutation, variables }) => {
|
|
16
|
+
const field = mutation.definitions.find(d => d.kind === 'OperationDefinition').selectionSet.selections[0].name.value
|
|
17
|
+
if (field !== 'uploadFigureMesh') return { data: { [field]: null } }
|
|
18
|
+
uploads.push({ name: variables.file.name, size: variables.file.size })
|
|
19
|
+
return next.refused ? { errors: [{ message: next.refused }] } : { data: { uploadFigureMesh: next.upload } }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
window.meshServer = {
|
|
23
|
+
answer(answer) {
|
|
24
|
+
next = answer
|
|
25
|
+
},
|
|
26
|
+
uploads() {
|
|
27
|
+
return uploads
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Where things-scene fetches a mesh file once the app has bootstrapped, at an address of the page's. */
|
|
32
|
+
window.meshUrlAt = path => {
|
|
33
|
+
history.replaceState(null, '', path)
|
|
34
|
+
bootstrap()
|
|
35
|
+
return figureMeshUrlOf('c'.repeat(64))
|
|
36
|
+
}
|