@things-factory/figure-ui 10.1.169 → 10.1.171
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/affected-parts.ts +11 -0
- package/client/modeller/canvas.ts +15 -1
- package/client/modeller/editor-styles.ts +98 -6
- package/client/modeller/editor.ts +212 -53
- package/client/modeller/fasten-geometry.ts +19 -0
- package/client/modeller/figure-fasten-drawing.ts +4 -3
- package/client/modeller/figure-links.ts +80 -37
- package/client/modeller/figure-material-panel.ts +20 -1
- package/client/modeller/figure-preview.ts +121 -8
- package/client/modeller/figure-properties.ts +1 -2
- package/client/modeller/figure-section-panel.ts +5 -0
- package/client/modeller/figure-verdict.ts +9 -2
- package/client/modeller/gate-findings.ts +12 -4
- package/client/modeller/group-menu.ts +57 -0
- package/client/modeller/links.ts +22 -1
- package/client/modeller/modeller-keys.ts +6 -0
- package/client/modeller/preview-size.ts +32 -0
- package/client/modeller/stage-model.ts +11 -0
- package/client/pages/figure-modeller-page.ts +8 -2
- package/dist-client/modeller/affected-parts.d.ts +6 -0
- package/dist-client/modeller/affected-parts.js +12 -0
- package/dist-client/modeller/affected-parts.js.map +1 -0
- package/dist-client/modeller/canvas.d.ts +3 -0
- package/dist-client/modeller/canvas.js +12 -1
- package/dist-client/modeller/canvas.js.map +1 -1
- package/dist-client/modeller/clips.d.ts +1 -1
- package/dist-client/modeller/editor-styles.js +98 -6
- package/dist-client/modeller/editor-styles.js.map +1 -1
- package/dist-client/modeller/editor.d.ts +42 -1
- package/dist-client/modeller/editor.js +220 -51
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/fasten-geometry.d.ts +27 -0
- package/dist-client/modeller/fasten-geometry.js +14 -0
- package/dist-client/modeller/fasten-geometry.js.map +1 -1
- package/dist-client/modeller/figure-fasten-drawing.js +4 -3
- package/dist-client/modeller/figure-fasten-drawing.js.map +1 -1
- package/dist-client/modeller/figure-links.d.ts +2 -0
- package/dist-client/modeller/figure-links.js +84 -38
- package/dist-client/modeller/figure-links.js.map +1 -1
- package/dist-client/modeller/figure-material-panel.d.ts +7 -0
- package/dist-client/modeller/figure-material-panel.js +24 -1
- package/dist-client/modeller/figure-material-panel.js.map +1 -1
- package/dist-client/modeller/figure-preview.d.ts +12 -0
- package/dist-client/modeller/figure-preview.js +120 -5
- package/dist-client/modeller/figure-preview.js.map +1 -1
- package/dist-client/modeller/figure-properties.js +1 -2
- package/dist-client/modeller/figure-properties.js.map +1 -1
- package/dist-client/modeller/figure-section-panel.js +5 -0
- package/dist-client/modeller/figure-section-panel.js.map +1 -1
- package/dist-client/modeller/figure-verdict.d.ts +1 -0
- package/dist-client/modeller/figure-verdict.js +8 -2
- package/dist-client/modeller/figure-verdict.js.map +1 -1
- package/dist-client/modeller/gate-findings.d.ts +3 -1
- package/dist-client/modeller/gate-findings.js +8 -4
- package/dist-client/modeller/gate-findings.js.map +1 -1
- package/dist-client/modeller/group-menu.d.ts +18 -0
- package/dist-client/modeller/group-menu.js +52 -0
- package/dist-client/modeller/group-menu.js.map +1 -0
- package/dist-client/modeller/links.d.ts +5 -0
- package/dist-client/modeller/links.js +21 -1
- package/dist-client/modeller/links.js.map +1 -1
- package/dist-client/modeller/modeller-keys.js +7 -0
- package/dist-client/modeller/modeller-keys.js.map +1 -1
- package/dist-client/modeller/preview-size.d.ts +23 -0
- package/dist-client/modeller/preview-size.js +32 -0
- package/dist-client/modeller/preview-size.js.map +1 -0
- package/dist-client/modeller/stage-model.d.ts +11 -0
- package/dist-client/modeller/stage-model.js +11 -0
- package/dist-client/modeller/stage-model.js.map +1 -1
- package/dist-client/pages/figure-modeller-page.d.ts +3 -0
- package/dist-client/pages/figure-modeller-page.js +11 -2
- package/dist-client/pages/figure-modeller-page.js.map +1 -1
- package/dist-client/route.d.ts +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +8 -8
- package/test/browser/editing-page.js +3 -1
- package/test/browser/editing.check.mjs +2 -1
- package/test/browser/links-page.js +1 -0
- package/test/browser/links.check.mjs +20 -5
- package/test/browser/modeller.check.mjs +9 -1
- package/test/browser/more-open.js +11 -0
- package/test/browser/parts.check.mjs +11 -1
- package/test/browser/places-mesh.check.mjs +9 -3
- package/test/browser/product-page.check.mjs +9 -4
- package/test/browser/props.check.mjs +224 -0
- package/test/browser/screen-page.js +18 -0
- package/test/browser/screen.check.mjs +172 -29
- package/test/browser/section-page.js +1 -0
- package/test/browser/section.check.mjs +6 -0
- package/test/command-preview.test.ts +2 -2
- package/test/fasten-geometry.test.ts +15 -0
- package/test/fields.test.ts +4 -2
- package/test/first-minute.test.ts +15 -2
- package/test/layout.test.ts +1 -1
- package/test/preview-size.test.ts +33 -0
- package/test/refusals.test.ts +1 -1
- package/test/resize-with-placement.test.ts +1 -1
- package/test/stage-model.test.ts +11 -1
- package/test/verdict.test.ts +7 -0
- package/translations/en.json +37 -27
- package/translations/es.json +18 -8
- package/translations/ja.json +19 -9
- package/translations/ko.json +50 -40
- package/translations/ms.json +19 -9
- package/translations/zh.json +19 -9
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.171",
|
|
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",
|
|
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",
|
|
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",
|
|
@@ -27,8 +27,8 @@
|
|
|
27
27
|
"clean": "npm run clean:server && npm run clean:client"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@hatiolab/figure-model": "^0.7.
|
|
31
|
-
"@hatiolab/things-scene": "^10.1.
|
|
30
|
+
"@hatiolab/figure-model": "^0.7.2",
|
|
31
|
+
"@hatiolab/things-scene": "^10.1.91",
|
|
32
32
|
"@material/web": "^2.0.0",
|
|
33
33
|
"@operato/data-grist": "^10.0.0",
|
|
34
34
|
"@operato/graphql": "^10.0.0",
|
|
@@ -41,11 +41,11 @@
|
|
|
41
41
|
"@operato/slot-layout-editor": "^10.29.25",
|
|
42
42
|
"@operato/styles": "^10.0.0",
|
|
43
43
|
"@operato/utils": "^10.0.0",
|
|
44
|
-
"@things-factory/ai-assistant": "^10.1.
|
|
45
|
-
"@things-factory/figure-service": "^10.1.
|
|
46
|
-
"@things-factory/figure-viewer": "^10.1.
|
|
44
|
+
"@things-factory/ai-assistant": "^10.1.170",
|
|
45
|
+
"@things-factory/figure-service": "^10.1.171",
|
|
46
|
+
"@things-factory/figure-viewer": "^10.1.171",
|
|
47
47
|
"@things-factory/setting-base": "^10.1.167",
|
|
48
48
|
"three": "^0.185.1"
|
|
49
49
|
},
|
|
50
|
-
"gitHead": "
|
|
50
|
+
"gitHead": "e88536db0bc3cbb759fe58f9efeef66a67fef2c3"
|
|
51
51
|
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* what a person sees of them: a part as the canvas draws it now, the bar's gizmo tools, the app's notices and prompts.
|
|
5
5
|
* Only reading: every change the check makes goes through the mouse and the keyboard.
|
|
6
6
|
*/
|
|
7
|
+
import './more-open.js'
|
|
7
8
|
import './modeller-page.js'
|
|
8
9
|
import { i18next } from '@operato/i18n'
|
|
9
10
|
import { Raycaster, Vector2 } from 'three'
|
|
@@ -87,8 +88,9 @@ window.editing = {
|
|
|
87
88
|
return { at: [left, top, zPos].map(Math.round), size: [width, depth, height].map(Math.round), turn: [rotationX, rotation, rotationY].map(v => Math.round((v ?? 0) * 1000) / 1000), glow: glow ? glow.intensity : 0 }
|
|
88
89
|
},
|
|
89
90
|
/** The bar's move, turn and resize tools: pressed, offered, and what their tooltip says. */
|
|
91
|
+
/** The bar's move, turn and resize tools: the gizmo's group, not the bar's other icon buttons (the view, the history). */
|
|
90
92
|
tools() {
|
|
91
|
-
return deep('button[tool]').map(b => ({ label: b.getAttribute('aria-label'), title: b.title, disabled: b.disabled, pressed: b.getAttribute('aria-pressed') === 'true', ...centreOf(b) }))
|
|
93
|
+
return deep('.gizmo-tools button[tool]').map(b => ({ label: b.getAttribute('aria-label'), title: b.title, disabled: b.disabled, pressed: b.getAttribute('aria-pressed') === 'true', ...centreOf(b) }))
|
|
92
94
|
},
|
|
93
95
|
/** The labels of the number fields on the properties tab. */
|
|
94
96
|
propertyInputs() {
|
|
@@ -293,7 +293,8 @@ describe('the material: colours by purpose, one step each, a glow drawn while dr
|
|
|
293
293
|
test('a drum offers the four common segment counts as outlines, the rest folded away', async () => {
|
|
294
294
|
await pick('drum')
|
|
295
295
|
const offered = await read(() => {
|
|
296
|
-
|
|
296
|
+
/* Segments are in the material's rarer half, in 「더 보기」. */
|
|
297
|
+
const panel = document.querySelector('figure-editor').renderRoot.querySelector('figure-material-panel[show="more"]').shadowRoot
|
|
297
298
|
const free = panel.querySelector('details[free]')
|
|
298
299
|
return {
|
|
299
300
|
common: [...panel.querySelectorAll('div[segments] > button[seg]')].map(b => !!b.querySelector('svg polygon')),
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* an element found through every shadow root by what it says, what the canvas draws, what the panels tell the editor.
|
|
4
4
|
* Only reading, apart from `tell`: the event a panel sends the editor, for a command no control on the page offers.
|
|
5
5
|
*/
|
|
6
|
+
import './more-open.js'
|
|
6
7
|
import './modeller-page.js'
|
|
7
8
|
|
|
8
9
|
function deep(selector, root = document) {
|
|
@@ -90,6 +90,17 @@ 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
|
+
/** What the figure does is a folded group of the properties (chief architect 2026-10-16): its fold opened, as a person opens it. */
|
|
94
|
+
async function openCapability() {
|
|
95
|
+
const summary = await handle('section[capability] > details > summary')
|
|
96
|
+
if (!(await summary.evaluate(s => s.parentElement.open))) await summary.click()
|
|
97
|
+
}
|
|
98
|
+
/** A fastening the part has is said in one line; 「바꾸기」 opens its drawing and details. */
|
|
99
|
+
async function changeFastening() {
|
|
100
|
+
await until(() => look.all('button[change-fastening]').length === 1, [], 'the fastening said in one line')
|
|
101
|
+
await (await handle('button[change-fastening]')).click()
|
|
102
|
+
await until(() => look.all('button[change-fastening]').length === 0, [], 'the fastening opened')
|
|
103
|
+
}
|
|
93
104
|
/** An element handle found through every shadow root. */
|
|
94
105
|
const handle = (selector, test) =>
|
|
95
106
|
page().evaluateHandle(
|
|
@@ -202,7 +213,9 @@ describe('a part made to follow the figure size, and let go of it', () => {
|
|
|
202
213
|
await click(await at('button[follow-size]'), 'follow-size')
|
|
203
214
|
await oneStep(start)
|
|
204
215
|
await until(() => !look.at('button[follow-size]'), [], 'the button gone once every side follows')
|
|
205
|
-
|
|
216
|
+
/* Each side's row says it follows; no sentence under them says it again (user 2026-10-16). */
|
|
217
|
+
assert.equal((await all('p[hint]', 'figure.text.resize-all-following')).length, 0, 'no sentence repeats the rows')
|
|
218
|
+
assert.equal((await all('div[resize-now] span', 'figure.label.resize-following')).length, 3, 'every side said to follow, once')
|
|
206
219
|
const told = (await heard('notify')).filter(n => n?.level === 'info' && String(n.message).includes('figure.text.size-declared-to-follow'))
|
|
207
220
|
assert.deepEqual(told, [], 'the figure had its size: no line says it was declared')
|
|
208
221
|
})
|
|
@@ -291,7 +304,7 @@ describe('holding places on a part', () => {
|
|
|
291
304
|
test('a place needs kinds it takes; a figure that holds nothing says so, and declaring it holds things is one step', async () => {
|
|
292
305
|
await mount('box')
|
|
293
306
|
await pick('box')
|
|
294
|
-
await
|
|
307
|
+
await openCapability()
|
|
295
308
|
assert.equal((await at('button[act]', 'figure.button.add-place'))?.disabled, true, 'no kinds: no place')
|
|
296
309
|
await type(await handle('input[placeholder="BOX, TOTE"]'), 'BOX')
|
|
297
310
|
assert.equal((await at('button[act]', 'figure.button.add-place'))?.disabled, false, 'a kind: a place may be made')
|
|
@@ -327,6 +340,7 @@ describe('the fastening form on the robot arm', () => {
|
|
|
327
340
|
test('the upper arm opens on what it is fastened to; a part hanging from it is listed, not choosable, with the reason', async () => {
|
|
328
341
|
await mount('arm')
|
|
329
342
|
await pick('upper-arm')
|
|
343
|
+
await changeFastening()
|
|
330
344
|
const own = (await source()).joints.find(j => j.body1.part === 'upper-arm').body0.part
|
|
331
345
|
const target = await read(() => look.select('figure.label.target-part'))
|
|
332
346
|
assert.equal(target.value, own, 'its own fastening')
|
|
@@ -497,12 +511,13 @@ describe('a bench: what each part is fastened to, what detaching would do, a row
|
|
|
497
511
|
test('a fastened part picked: the face it is fastened to is lit, and taking it off is said to keep what hangs from it', async () => {
|
|
498
512
|
await pick('top')
|
|
499
513
|
await until(() => look.drawn().includes('__target-face'), [], 'the leg\'s top lit')
|
|
500
|
-
await until(() => look.all('
|
|
514
|
+
await until(() => look.all('section[fastening] [detach-words]', 'figure.text.detach-keeps-children').length === 1, [], 'said under the fastening')
|
|
501
515
|
})
|
|
502
516
|
|
|
503
|
-
test('a part that would stay where it stands: nothing is said
|
|
517
|
+
test('a part that would stay where it stands: nothing is said under its fastening', async () => {
|
|
504
518
|
await pick('lamp')
|
|
505
|
-
|
|
519
|
+
/* Taking it off is the place group's 「⋯」 (chief architect 2026-10-16). */
|
|
520
|
+
await until(() => look.all('section[fastening] button[group-menu]').length === 1, [], 'the lamp\'s place menu')
|
|
506
521
|
/* What detaching would do is worked out once the figure rests (150 ms after the pick). */
|
|
507
522
|
await sleep(500)
|
|
508
523
|
assert.deepEqual(await all('[detach-words]'), [])
|
|
@@ -343,6 +343,8 @@ describe('the fastening form belongs to the part it is for', () => {
|
|
|
343
343
|
|
|
344
344
|
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 () => {
|
|
345
345
|
await click(await read(() => probe.row('forearm')))
|
|
346
|
+
/* A fastening the part has is said in one line; 「바꾸기」 opens it (chief architect 2026-10-16). */
|
|
347
|
+
await click(await read(() => probe.button('change-fastening')))
|
|
346
348
|
assert.equal(await (await select('figure.label.target-part')).evaluate(s => s?.value), 'elbow', 'the forearm opens on what it is fastened to')
|
|
347
349
|
/* Placed off centre on the elbow's face, as a hand does: the face drawing focused, the arrows. */
|
|
348
350
|
const face = await read(() => {
|
|
@@ -363,6 +365,9 @@ describe('the fastening form belongs to the part it is for', () => {
|
|
|
363
365
|
assert.ok(chosen.mm > 0)
|
|
364
366
|
|
|
365
367
|
await click(await read(() => probe.row('elbow')))
|
|
368
|
+
/* Another part opens on the line again, the form closed: 「바꾸기」 opens the elbow's own. */
|
|
369
|
+
assert.ok(await read(() => probe.button('change-fastening')), 'the elbow\'s fastening said in one line')
|
|
370
|
+
await click(await read(() => probe.button('change-fastening')))
|
|
366
371
|
/* The elbow opens on its own fastening (walk 1, 2026-09-26), not on the forearm's choices. */
|
|
367
372
|
const own = await read(() => {
|
|
368
373
|
const joint = JSON.parse(document.querySelector('figure-editor').session.source()).joints.find(j => j.body1.part === 'elbow')
|
|
@@ -712,7 +717,9 @@ describe('a part follows the figure size in one step (follow-size, 2026-09-26)',
|
|
|
712
717
|
assert.equal(after.revision, start.revision + 1, 'one step')
|
|
713
718
|
assert.deepEqual(await read(() => probe.fields('size.')), { 'size.x': 400, 'size.y': 400, 'size.z': 400 }, 'the size is the box as it stands')
|
|
714
719
|
const notice = await read(() => (window.__told ?? []).filter(n => n.level === 'info').at(-1)?.message ?? null)
|
|
715
|
-
|
|
720
|
+
/* The box had no space declared either: the same step declared it, and the one line says both (ruling 2026-10-16, Q1 A). */
|
|
721
|
+
assert.ok(notice?.includes('figure.text.size-and-space-declared-to-follow'), `a line says so: ${notice}`)
|
|
722
|
+
assert.equal(await read(() => !!document.querySelector('figure-editor').session.revisionNow().asset.occupancy), true, 'the space declared by the same step')
|
|
716
723
|
const grown = await read(() => {
|
|
717
724
|
const editor = document.querySelector('figure-editor')
|
|
718
725
|
editor.session.apply([{ kind: 'set-design', input: 'size.x', unit: 'mm', value: 800 }])
|
|
@@ -866,6 +873,7 @@ describe('a row that carries a part (ADR-0093, 2026-09-26)', () => {
|
|
|
866
873
|
assert.equal((await state()).selected, 'roller')
|
|
867
874
|
assert.ok(await read(() => probe.hasInput('roller.repeat.pitch')), 'the roller offers its row')
|
|
868
875
|
/* 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). */
|
|
876
|
+
await click(await read(() => probe.button('change-fastening')))
|
|
869
877
|
assert.equal(await read(() => probe.fastenMode()), 'centre', 'the way it is fastened reads centre')
|
|
870
878
|
/* And the row's part keeps a name of its own to change. */
|
|
871
879
|
const name = await read(() => probe.nameField())
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The properties' 「더 보기」 open, as a person who opened it once finds it: the checks that drive what is inside it (the
|
|
3
|
+
* section, its inside, glow, segments) reach it the way that person does. Imported first by those checks' pages, before
|
|
4
|
+
* the editor is made; the acceptance page a person looks at does not, and opens as a new person's would. The fold's own
|
|
5
|
+
* default and its memory are props.check.mjs's.
|
|
6
|
+
*/
|
|
7
|
+
try {
|
|
8
|
+
localStorage.setItem('figure-properties-more-open', '1')
|
|
9
|
+
} catch {
|
|
10
|
+
/* A browser that keeps nothing opens the editor with the fold closed; the checks inside it then say so. */
|
|
11
|
+
}
|
|
@@ -344,8 +344,18 @@ describe('the long explanations open from their 「?」 beside the title, closed
|
|
|
344
344
|
await mount('box', 'doors')
|
|
345
345
|
await pick('left')
|
|
346
346
|
const seen = new Set()
|
|
347
|
-
for (const name of ['properties', '
|
|
347
|
+
for (const name of ['properties', 'motion']) {
|
|
348
348
|
await tab(name)
|
|
349
|
+
/* What the figure does is a folded group of the properties now (chief architect 2026-10-16): opened as a hand opens it. */
|
|
350
|
+
if (name === 'properties') {
|
|
351
|
+
const fold = await read(() => {
|
|
352
|
+
const summary = document.querySelector('figure-editor').renderRoot.querySelector('div[panel]:not([off]) section[capability] > details > summary')
|
|
353
|
+
summary.scrollIntoView({ block: 'center' })
|
|
354
|
+
const r = summary.getBoundingClientRect()
|
|
355
|
+
return { x: r.left + 20, y: r.top + r.height / 2 }
|
|
356
|
+
})
|
|
357
|
+
await click(fold, 'the capability fold')
|
|
358
|
+
}
|
|
349
359
|
const helps = await read(() =>
|
|
350
360
|
[...document.querySelector('figure-editor').renderRoot.querySelectorAll('figure-links, figure-clips')].flatMap((element, host) =>
|
|
351
361
|
[...element.shadowRoot.querySelectorAll('button[help]')].map((b, i) => ({ host, i, seen: b.getClientRects().length > 0 })).filter(b => b.seen)
|
|
@@ -198,9 +198,15 @@ describe('places made by a rule: the shared slot layout editor on a part\'s face
|
|
|
198
198
|
before(async () => {
|
|
199
199
|
await mount(addFigureBox(figure('Stacker'), { id: 'fork', size: { x: 1200, y: 50, z: 900 }, position: { x: 0, y: 25, z: 0 } }))
|
|
200
200
|
await pick('fork')
|
|
201
|
-
|
|
202
|
-
await
|
|
203
|
-
|
|
201
|
+
/* What the figure does is a folded group of the properties (chief architect 2026-10-16): opened as a hand opens it. */
|
|
202
|
+
const fold = await read(() => {
|
|
203
|
+
const summary = document.querySelector('figure-editor').renderRoot.querySelector('div[panel]:not([off]) section[capability] > details > summary')
|
|
204
|
+
summary.scrollIntoView({ block: 'center' })
|
|
205
|
+
const r = summary.getBoundingClientRect()
|
|
206
|
+
return { x: r.left + 20, y: r.top + r.height / 2 }
|
|
207
|
+
})
|
|
208
|
+
await page().mouse.click(fold.x, fold.y)
|
|
209
|
+
await until(() => !!document.querySelector('figure-editor').renderRoot.querySelector('div[panel]:not([off]) section[capability] > details[open]'), [], 'the capability fold open')
|
|
204
210
|
await settled()
|
|
205
211
|
})
|
|
206
212
|
|
|
@@ -265,6 +265,11 @@ describe('opening another figure', () => {
|
|
|
265
265
|
|
|
266
266
|
const PANEL = 'figure-modeller-page >>> figure-history-panel'
|
|
267
267
|
const TAB = name => `figure-editor >>> button[tab][aria-label="figure.label.tab-short-${name}"]`
|
|
268
|
+
/** The release history opens from the bar beside save (it was a tab); pressed only when it is not open already. */
|
|
269
|
+
const HISTORY = 'figure-editor >>> button[bar-overlay=history]'
|
|
270
|
+
async function openHistory() {
|
|
271
|
+
if ((await read(s => product.deep(s)[0]?.getAttribute('aria-pressed'), HISTORY)) !== 'true') await press(HISTORY, 'the history button')
|
|
272
|
+
}
|
|
268
273
|
const VERSION_1 = { version: 1, comment: 'first', state: 'released', updatedAt: null, updater: null, score: null, triangles: null, groups: null }
|
|
269
274
|
/** The revert choice popup's buttons: keep editing, let the edits go, save them first. */
|
|
270
275
|
const CHOICE = { cancel: 'figure-revert-choice >>> md-text-button[slot=cancel]', discard: 'figure-revert-choice >>> md-text-button[slot=start]', save: 'figure-revert-choice >>> md-filled-button[slot=confirm]' }
|
|
@@ -286,12 +291,12 @@ describe('the history panel on the page', () => {
|
|
|
286
291
|
await open('h')
|
|
287
292
|
})
|
|
288
293
|
|
|
289
|
-
test('the release beside the save leads to the release section of the history
|
|
294
|
+
test('the release beside the save leads to the release section of the history, opened from the bar', async () => {
|
|
290
295
|
await press('button[publish]', 'release')
|
|
291
296
|
await until(p => product.deep(p)[0]?.shadowRoot.activeElement?.closest('section[release]'), 'the release section focused', PANEL)
|
|
292
297
|
const panel = await read(p => { const panel = product.deep(p)[0]; return { slot: panel.assignedSlot?.name, visible: panel.checkVisibility() } }, PANEL)
|
|
293
298
|
assert.deepEqual(panel, { slot: 'history', visible: true })
|
|
294
|
-
assert.equal(await read(
|
|
299
|
+
assert.equal(await read(s => product.deep(s)[0]?.getAttribute('aria-pressed'), HISTORY), 'true', 'the bar\'s history pressed')
|
|
295
300
|
})
|
|
296
301
|
|
|
297
302
|
test('a released version downloads as a package by its full reference', async () => {
|
|
@@ -307,7 +312,7 @@ describe('the history panel on the page', () => {
|
|
|
307
312
|
await until(r => product.view().revision === r + 1, 'one step', before.revision)
|
|
308
313
|
await press(TAB('properties'), 'the properties tab')
|
|
309
314
|
await until(() => product.deep('figure-properties >>> div[row] > span')[0]?.textContent === 'figure.text.declared-space-declared', 'the room declared')
|
|
310
|
-
await
|
|
315
|
+
await openHistory()
|
|
311
316
|
})
|
|
312
317
|
|
|
313
318
|
test('with edits not saved, a revert asks first; kept editing, nothing is sent', async () => {
|
|
@@ -774,7 +779,7 @@ describe('the page at a narrow width', () => {
|
|
|
774
779
|
after(() => page().setViewport({ width: 1400, height: 900 }))
|
|
775
780
|
|
|
776
781
|
test('the page fits the width it is given: the history panel does not run off its right edge', async () => {
|
|
777
|
-
await
|
|
782
|
+
await openHistory()
|
|
778
783
|
await until(p => product.deep(p)[0]?.checkVisibility(), 'the history panel', PANEL)
|
|
779
784
|
const edges = await read(p => ({ panel: product.deep(p)[0].getBoundingClientRect().right, page: product.page().getBoundingClientRect().right, window: innerWidth }), PANEL)
|
|
780
785
|
assert.ok(edges.panel <= edges.page && edges.page <= edges.window, JSON.stringify(edges))
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The properties panel's order and its quieter parts in a real browser (chief architect 2026-10-16, after the user's
|
|
3
|
+
* 「너무 이해하기 어렵고 산만하고, 버튼 박스도 과하게」): four side tabs; a part's groups in one order whatever its state;
|
|
4
|
+
* a fastening said in one line until it is changed; a group's rarer actions in its 「⋯」, reached by the keyboard too;
|
|
5
|
+
* 「더 보기」 folded by default and left as the person left it; the view and the history opened from the bar.
|
|
6
|
+
*
|
|
7
|
+
* Run: node --test test/browser/props.check.mjs
|
|
8
|
+
*/
|
|
9
|
+
import { test, describe, before, after } from 'node:test'
|
|
10
|
+
import assert from 'node:assert/strict'
|
|
11
|
+
|
|
12
|
+
import { buildPage } from './build.mjs'
|
|
13
|
+
import { openPage } from './serve.mjs'
|
|
14
|
+
|
|
15
|
+
const sleep = ms => new Promise(done => setTimeout(done, ms))
|
|
16
|
+
|
|
17
|
+
let opened
|
|
18
|
+
before(async () => {
|
|
19
|
+
opened = await openPage(await buildPage('links-page.js'))
|
|
20
|
+
})
|
|
21
|
+
after(async () => {
|
|
22
|
+
await opened?.close()
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const page = () => opened.page
|
|
26
|
+
const read = (fn, ...args) => page().evaluate(fn, ...args)
|
|
27
|
+
const revision = () => read(() => probe.state().revision)
|
|
28
|
+
const MORE = 'figure-properties-more-open'
|
|
29
|
+
|
|
30
|
+
async function until(fn, args, what, ms = 8000) {
|
|
31
|
+
const deadline = Date.now() + ms
|
|
32
|
+
for (;;) {
|
|
33
|
+
const got = await read(fn, ...args)
|
|
34
|
+
if (got) return got
|
|
35
|
+
if (Date.now() > deadline) assert.fail(`waited for ${what}`)
|
|
36
|
+
await sleep(50)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
async function mount(kind) {
|
|
40
|
+
await read(kind => harness.mount(kind, null), kind)
|
|
41
|
+
await until(
|
|
42
|
+
() => {
|
|
43
|
+
try {
|
|
44
|
+
return probe.ids().some(id => !!probe.partOnPage(id))
|
|
45
|
+
} catch {
|
|
46
|
+
return false
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
[],
|
|
50
|
+
'the figure drawn in 3D',
|
|
51
|
+
15000
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
async function pick(part) {
|
|
55
|
+
const point = await read(id => probe.row(id), part)
|
|
56
|
+
assert.ok(point, `the row of ${part}`)
|
|
57
|
+
await page().mouse.click(point.x, point.y)
|
|
58
|
+
await until(id => probe.state().selected === id, [part], `${part} selected`)
|
|
59
|
+
await read(() => document.querySelector('figure-editor').updateComplete)
|
|
60
|
+
}
|
|
61
|
+
/** An element found through every shadow root; clicked where a person clicks it. */
|
|
62
|
+
async function press(selector, what) {
|
|
63
|
+
const handle = await page().evaluateHandle(s => {
|
|
64
|
+
const deep = (root = document) => [...root.querySelectorAll(s)].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
|
|
65
|
+
return deep()[0] ?? null
|
|
66
|
+
}, selector)
|
|
67
|
+
const element = handle.asElement()
|
|
68
|
+
assert.ok(element, `${what ?? selector} is on the page`)
|
|
69
|
+
await element.click()
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The groups of the panel shown, top to bottom, by what each is: the part's name, the size, the place and the row the
|
|
74
|
+
* links draw, the look, what the figure does, and 「더 보기」.
|
|
75
|
+
*/
|
|
76
|
+
const groupsInOrder = () =>
|
|
77
|
+
read(() => {
|
|
78
|
+
const editor = document.querySelector('figure-editor')
|
|
79
|
+
const panel = editor.shadowRoot.querySelector('div[panel]:not([off])')
|
|
80
|
+
const links = panel.querySelector('figure-links')?.shadowRoot
|
|
81
|
+
const marks = {
|
|
82
|
+
name: panel.querySelector('input[name-field]'),
|
|
83
|
+
size: links?.querySelector('section[size]'),
|
|
84
|
+
place: links?.querySelector('section[fastening]'),
|
|
85
|
+
row: panel.querySelector('section[slot=row]') ?? links?.querySelector('section[row]'),
|
|
86
|
+
look: panel.querySelector('figure-material-panel:not([show])'),
|
|
87
|
+
capability: panel.querySelector('section[capability]'),
|
|
88
|
+
more: panel.querySelector('section[more]')
|
|
89
|
+
}
|
|
90
|
+
return Object.entries(marks)
|
|
91
|
+
.filter(([, e]) => e && e.getBoundingClientRect().height > 0)
|
|
92
|
+
.sort((a, b) => a[1].getBoundingClientRect().top - b[1].getBoundingClientRect().top)
|
|
93
|
+
.map(([name]) => name)
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
describe('the side: four tabs, the view and the history from the bar', () => {
|
|
97
|
+
before(() => mount('rollers'))
|
|
98
|
+
|
|
99
|
+
test('properties, motion, data and the verdict -- the four tabs, in that order', async () => {
|
|
100
|
+
const tabs = await read(() => [...document.querySelector('figure-editor').shadowRoot.querySelectorAll('aside button[tab]')].map(b => b.getAttribute('aria-label')))
|
|
101
|
+
assert.deepEqual(tabs, ['properties', 'motion', 'data', 'verdict'].map(t => `figure.label.tab-short-${t}`))
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
test('the view opens over the work from the bar, pressed while open; Esc closes it', async () => {
|
|
105
|
+
const shown = which => read(w => document.querySelector('figure-editor').shadowRoot.querySelector(`div[bar-overlay-panel=${w}]`).getBoundingClientRect().height > 0, which)
|
|
106
|
+
assert.equal(await shown('view'), false, 'closed at first')
|
|
107
|
+
await press('ox-canvas-bar button[bar-overlay=view]', 'the view button')
|
|
108
|
+
await until(() => document.querySelector('figure-editor').shadowRoot.querySelector('div[bar-overlay-panel=view]').getBoundingClientRect().height > 0, [], 'the view open')
|
|
109
|
+
assert.equal(await read(() => document.querySelector('figure-editor').shadowRoot.querySelector('button[bar-overlay=view]').getAttribute('aria-pressed')), 'true')
|
|
110
|
+
assert.equal(await read(() => !!document.querySelector('figure-editor').shadowRoot.querySelector('div[bar-overlay-panel=view] figure-settings')), true, 'the view settings in it')
|
|
111
|
+
await read(() => document.querySelector('figure-editor').shadowRoot.querySelector('div[bar-overlay-panel=view] button[quiet]').focus())
|
|
112
|
+
await page().keyboard.press('Escape')
|
|
113
|
+
await until(() => document.querySelector('figure-editor').shadowRoot.querySelector('div[bar-overlay-panel=view]').getBoundingClientRect().height === 0, [], 'closed by Esc')
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
test('the history opens from the bar beside save, and the view closes for it', async () => {
|
|
117
|
+
await press('ox-canvas-bar button[bar-overlay=view]', 'the view button')
|
|
118
|
+
await press('ox-canvas-bar button[bar-overlay=history]', 'the history button')
|
|
119
|
+
const open = await read(() => ['view', 'history'].map(w => document.querySelector('figure-editor').shadowRoot.querySelector(`div[bar-overlay-panel=${w}]`).getBoundingClientRect().height > 0))
|
|
120
|
+
assert.deepEqual(open, [false, true], 'one at a time')
|
|
121
|
+
assert.equal(await read(() => !!document.querySelector('figure-editor').shadowRoot.querySelector('div[bar-overlay-panel=history] slot[name=history]')), true, 'the page\'s history goes in it')
|
|
122
|
+
await press('ox-canvas-bar button[bar-overlay=history]', 'the history button')
|
|
123
|
+
})
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
describe('a part\'s groups in one order, whatever its state', () => {
|
|
127
|
+
before(() => mount('rollers'))
|
|
128
|
+
|
|
129
|
+
test('a part fastened in a row: name, size, place, row, look, what it does, more', async () => {
|
|
130
|
+
await pick('roller')
|
|
131
|
+
assert.deepEqual(await groupsInOrder(), ['name', 'size', 'place', 'row', 'look', 'capability', 'more'])
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
test('a part on the plane, not in a row: the same order, the row folded where a row would be', async () => {
|
|
135
|
+
await pick('bed')
|
|
136
|
+
assert.deepEqual(await groupsInOrder(), ['name', 'size', 'place', 'row', 'look', 'capability', 'more'])
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
test('a fastened part\'s place is one line and 「바꾸기」: no locked cells, no drawing, until it is changed', async () => {
|
|
140
|
+
await pick('mark')
|
|
141
|
+
const place = () =>
|
|
142
|
+
read(() => {
|
|
143
|
+
const editor = document.querySelector('figure-editor')
|
|
144
|
+
const links = editor.shadowRoot.querySelector('div[panel]:not([off]) figure-links').shadowRoot
|
|
145
|
+
return {
|
|
146
|
+
sentence: !!links.querySelector('div[fastened-summary] p[fasten-sentence]'),
|
|
147
|
+
change: !!links.querySelector('button[change-fastening]'),
|
|
148
|
+
drawing: !!links.querySelector('figure-fasten-drawing'),
|
|
149
|
+
cells: !!editor.shadowRoot.querySelector('div[panel]:not([off]) div[slot=place]')
|
|
150
|
+
}
|
|
151
|
+
})
|
|
152
|
+
assert.deepEqual(await place(), { sentence: true, change: true, drawing: false, cells: false })
|
|
153
|
+
await press('button[change-fastening]', '「바꾸기」')
|
|
154
|
+
await until(() => !!document.querySelector('figure-editor').shadowRoot.querySelector('div[panel]:not([off]) figure-links').shadowRoot.querySelector('figure-fasten-drawing'), [], 'the drawing')
|
|
155
|
+
/* Another part chosen and back: one line again. */
|
|
156
|
+
await pick('bed')
|
|
157
|
+
await pick('mark')
|
|
158
|
+
assert.equal((await place()).change, true, 'said in one line again')
|
|
159
|
+
})
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
describe('a group\'s 「⋯」: its rarer actions, by the mouse or the keyboard', () => {
|
|
163
|
+
before(() => mount('rollers'))
|
|
164
|
+
|
|
165
|
+
test('the place group\'s 「⋯」 takes the fastening off, opened and chosen by the keyboard, in one step', async () => {
|
|
166
|
+
/* The bed stands on the plane; nothing it carries is in the way of taking it off. */
|
|
167
|
+
await pick('bed')
|
|
168
|
+
const start = await revision()
|
|
169
|
+
await read(() => document.querySelector('figure-editor').shadowRoot.querySelector('div[panel]:not([off]) figure-links').shadowRoot.querySelector('section[fastening] button[group-menu]').focus())
|
|
170
|
+
await page().keyboard.press('Enter')
|
|
171
|
+
/* The menu takes the focus on its next frame (ox-popup); the keys go to it from then. */
|
|
172
|
+
await until(() => document.activeElement?.localName === 'ox-popup-menu', [], 'the menu open, with the focus')
|
|
173
|
+
const items = await read(() => [...document.querySelectorAll('ox-popup-menu[active] ox-popup-menuitem')].map(i => i.getAttribute('label')))
|
|
174
|
+
assert.deepEqual(items, ['figure.button.detach'])
|
|
175
|
+
await page().keyboard.press('ArrowDown')
|
|
176
|
+
/* The item takes the focus on the menu's next update; Enter goes to it from then. */
|
|
177
|
+
await until(() => document.activeElement?.localName === 'ox-popup-menuitem', [], 'the item chosen')
|
|
178
|
+
await page().keyboard.press('Enter')
|
|
179
|
+
await until(r => probe.state().revision === r + 1, [start], 'one step')
|
|
180
|
+
assert.equal(await read(() => JSON.parse(document.querySelector('figure-editor').session.source()).joints.some(j => j.body1.part === 'bed')), false, 'taken off')
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
test('Esc closes the menu and nothing is done', async () => {
|
|
184
|
+
await pick('roller')
|
|
185
|
+
const start = await revision()
|
|
186
|
+
await press('section[row] button[group-menu]', 'the row\'s 「⋯」')
|
|
187
|
+
await until(() => document.activeElement?.localName === 'ox-popup-menu', [], 'the menu open, with the focus')
|
|
188
|
+
assert.deepEqual(await read(() => [...document.querySelectorAll('ox-popup-menu[active] ox-popup-menuitem')].map(i => i.getAttribute('label'))), ['figure.button.remove-repeat'])
|
|
189
|
+
await page().keyboard.press('Escape')
|
|
190
|
+
await until(() => !document.querySelector('ox-popup-menu[active]'), [], 'the menu closed')
|
|
191
|
+
await sleep(200)
|
|
192
|
+
assert.equal(await revision(), start, 'nothing done')
|
|
193
|
+
})
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
describe('「더 보기」: folded by default, left as the person left it', () => {
|
|
197
|
+
const fold = () => read(() => document.querySelector('figure-editor').shadowRoot.querySelector('div[panel]:not([off]) section[more] > details')?.open ?? null)
|
|
198
|
+
before(() => read(key => localStorage.removeItem(key), MORE))
|
|
199
|
+
after(() => read(key => localStorage.setItem(key, '1'), MORE))
|
|
200
|
+
|
|
201
|
+
test('a person who never opened it finds it folded, the rare eight inside', async () => {
|
|
202
|
+
await mount('box')
|
|
203
|
+
await pick('box')
|
|
204
|
+
assert.equal(await fold(), false)
|
|
205
|
+
const inside = await read(() => {
|
|
206
|
+
const more = document.querySelector('figure-editor').shadowRoot.querySelector('div[panel]:not([off]) section[more]')
|
|
207
|
+
return { section: !!more.querySelector('figure-section-panel[embedded]'), material: !!more.querySelector('figure-material-panel[show=more]'), tools: !!more.querySelector('div[tools]') }
|
|
208
|
+
})
|
|
209
|
+
assert.deepEqual(inside, { section: true, material: true, tools: true })
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
test('opened, it stays open for the next part, and for the next figure opened in this browser', async () => {
|
|
213
|
+
await press('section[more] > details > summary', 'the 「더 보기」 fold')
|
|
214
|
+
await until(() => document.querySelector('figure-editor').shadowRoot.querySelector('section[more] > details').open, [], 'open')
|
|
215
|
+
/* The fold's toggle event comes a task after it opens. */
|
|
216
|
+
await until(key => localStorage.getItem(key) === '1', [MORE], 'kept in this browser')
|
|
217
|
+
await mount('box')
|
|
218
|
+
await pick('box')
|
|
219
|
+
assert.equal(await fold(), true, 'open again')
|
|
220
|
+
await press('section[more] > details > summary', 'the 「더 보기」 fold')
|
|
221
|
+
await until(() => !document.querySelector('figure-editor').shadowRoot.querySelector('section[more] > details').open, [], 'folded')
|
|
222
|
+
await until(key => localStorage.getItem(key) === '0', [MORE], 'folded, kept so')
|
|
223
|
+
})
|
|
224
|
+
})
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* `window.editorHarness` -- the modeller page's harness takes `window.harness` after it), the figures the check opens,
|
|
4
4
|
* and what the app shows that is not in the editor: its notices, the card's picture, the scene's registered figures.
|
|
5
5
|
*/
|
|
6
|
+
import './more-open.js'
|
|
6
7
|
import './modeller-page.js'
|
|
7
8
|
import { captureFigureThumbnail } from 'figure-ui-client/modeller/thumbnail.js'
|
|
8
9
|
import { drawableFigureParts } from 'figure-ui-client/modeller/renderer.js'
|
|
@@ -16,6 +17,7 @@ import {
|
|
|
16
17
|
compileFigureAsset,
|
|
17
18
|
createFigureAsset,
|
|
18
19
|
editFigurePart,
|
|
20
|
+
proposeFigureOccupancy,
|
|
19
21
|
robotArmFigure,
|
|
20
22
|
serializeFigureAsset
|
|
21
23
|
} from '@hatiolab/figure-model'
|
|
@@ -58,6 +60,22 @@ const FIGURES = {
|
|
|
58
60
|
a = applyFigureAuthoring(a, { kind: 'attach', part: 'mast', face: 'bottom', to: { plane: 'mounting-plane' } })
|
|
59
61
|
return applyFigureAuthoring(a, { kind: 'declare-instance-size', range: { x: { min: 200, max: 200 }, y: { min: 1000, max: 3000 }, z: { min: 300, max: 300 } } })
|
|
60
62
|
},
|
|
63
|
+
/* A block fastened on a base: a part with a fastening to take back, under a figure with no size of its own yet. */
|
|
64
|
+
stack: () => {
|
|
65
|
+
let a = addFigureBox(named('Stack'), { id: 'base', size: { x: 1000, y: 200, z: 600 }, position: { x: 0, y: 100, z: 0 } })
|
|
66
|
+
a = addFigureBox(a, { id: 'top', size: { x: 200, y: 200, z: 200 }, position: { x: 0, y: 300, z: 0 } })
|
|
67
|
+
return applyFigureAuthoring(a, { kind: 'attach', part: 'top', face: 'bottom', to: { part: 'base', face: 'top' } })
|
|
68
|
+
},
|
|
69
|
+
/* A conveyor that follows its width, a roller every 200 mm, and the space it takes declared: the one figure stretched. */
|
|
70
|
+
stretch: () => {
|
|
71
|
+
let a = addFigureBox(named('Stretch'), { id: 'frame', size: { x: 2000, y: 100, z: 600 }, position: { x: 0, y: 50, z: 0 } })
|
|
72
|
+
a = addFigurePrimitive(a, { id: 'roller', kind: 'cylinder', dimensions: { radius: 30, length: 560 }, position: { x: 0, y: 0, z: 0 }, rotation: { x: 90, y: 0, z: 0 } })
|
|
73
|
+
a = applyFigureAuthoring(a, { kind: 'attach', part: 'roller', face: 'bottom', to: { part: 'frame', face: 'top' } })
|
|
74
|
+
a = applyFigureAuthoring(a, { kind: 'follow-size', part: 'frame', axes: ['x'] })
|
|
75
|
+
a = applyFigureAuthoring(a, { kind: 'make-repeat', part: 'roller', axis: 'x', pitch: 200 })
|
|
76
|
+
/* A space wider than the parts, as a spin swept or a padding gives: the box is the space, not the size (2026-10-16). */
|
|
77
|
+
return applyFigureAuthoring(a, { kind: 'declare-occupancy', proposal: proposeFigureOccupancy(a, { over: 'range' }), padding: { x: 100, y: 0, z: 0 } })
|
|
78
|
+
},
|
|
61
79
|
/* The robot arm with a clip that turns its base: controls and a clip in the preview. */
|
|
62
80
|
'arm-clip': () => editFigurePart(robotArmFigure(), { kind: 'set-driver', state: 'j1-base', clip: 'cycle', duration: 4, keys: [{ seconds: 0, value: 0 }, { seconds: 4, value: 60 }] })
|
|
63
81
|
}
|