@things-factory/figure-ui 10.1.205 → 10.1.207
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 +16 -0
- package/client/modeller/editor.ts +53 -137
- package/client/modeller/fasten-form.ts +50 -11
- package/client/modeller/figure-facts.ts +1 -1
- package/client/modeller/figure-fasten-dialog.ts +3 -2
- package/client/modeller/figure-history-panel.ts +1 -29
- package/client/modeller/figure-links.ts +2 -2
- package/client/modeller/figure-preview.ts +10 -12
- package/client/modeller/figure-verdict.ts +2 -17
- package/client/modeller/gizmo.ts +10 -0
- package/client/modeller/keys.ts +10 -1
- package/client/modeller/links.ts +32 -41
- package/client/modeller/maturity.ts +11 -27
- package/client/modeller/palette.ts +1 -1
- package/client/modeller/product-session.ts +16 -4
- package/client/modeller/refusal-teller.ts +3 -25
- package/client/modeller/refusals.ts +20 -33
- package/client/pages/figure-modeller-page.ts +7 -22
- package/dist-client/modeller/editor-styles.js +16 -0
- package/dist-client/modeller/editor-styles.js.map +1 -1
- package/dist-client/modeller/editor.d.ts +8 -11
- package/dist-client/modeller/editor.js +57 -138
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/fasten-form.d.ts +16 -7
- package/dist-client/modeller/fasten-form.js +45 -11
- package/dist-client/modeller/fasten-form.js.map +1 -1
- package/dist-client/modeller/figure-facts.js +1 -1
- package/dist-client/modeller/figure-facts.js.map +1 -1
- package/dist-client/modeller/figure-fasten-dialog.js +3 -3
- package/dist-client/modeller/figure-fasten-dialog.js.map +1 -1
- package/dist-client/modeller/figure-history-panel.d.ts +0 -16
- package/dist-client/modeller/figure-history-panel.js +1 -27
- package/dist-client/modeller/figure-history-panel.js.map +1 -1
- package/dist-client/modeller/figure-links.js +2 -2
- package/dist-client/modeller/figure-links.js.map +1 -1
- package/dist-client/modeller/figure-preview.js +8 -11
- package/dist-client/modeller/figure-preview.js.map +1 -1
- package/dist-client/modeller/figure-verdict.d.ts +0 -5
- package/dist-client/modeller/figure-verdict.js +2 -21
- package/dist-client/modeller/figure-verdict.js.map +1 -1
- package/dist-client/modeller/gizmo.d.ts +6 -0
- package/dist-client/modeller/gizmo.js +11 -0
- package/dist-client/modeller/gizmo.js.map +1 -1
- package/dist-client/modeller/keys.d.ts +1 -1
- package/dist-client/modeller/keys.js +6 -0
- package/dist-client/modeller/keys.js.map +1 -1
- package/dist-client/modeller/links.d.ts +16 -22
- package/dist-client/modeller/links.js +26 -39
- package/dist-client/modeller/links.js.map +1 -1
- package/dist-client/modeller/maturity.d.ts +2 -4
- package/dist-client/modeller/maturity.js +12 -26
- package/dist-client/modeller/maturity.js.map +1 -1
- package/dist-client/modeller/palette.js +1 -1
- package/dist-client/modeller/palette.js.map +1 -1
- package/dist-client/modeller/product-session.d.ts +15 -13
- package/dist-client/modeller/product-session.js +15 -5
- package/dist-client/modeller/product-session.js.map +1 -1
- package/dist-client/modeller/refusal-teller.d.ts +1 -12
- package/dist-client/modeller/refusal-teller.js +3 -22
- package/dist-client/modeller/refusal-teller.js.map +1 -1
- package/dist-client/modeller/refusals.d.ts +7 -17
- package/dist-client/modeller/refusals.js +17 -34
- package/dist-client/modeller/refusals.js.map +1 -1
- package/dist-client/pages/figure-modeller-page.d.ts +1 -3
- package/dist-client/pages/figure-modeller-page.js +7 -23
- package/dist-client/pages/figure-modeller-page.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -7
- package/test/browser/acceptance-page.js +4 -4
- package/test/browser/detail-page.js +1 -3
- package/test/browser/detail-tabs-page.js +1 -2
- package/test/browser/links.check.mjs +32 -50
- package/test/browser/modeller.check.mjs +46 -19
- package/test/browser/parts-page.js +2 -2
- package/test/browser/parts.check.mjs +51 -12
- package/test/browser/product-page.check.mjs +6 -9
- package/test/browser/product-page.js +1 -2
- package/test/browser/screen-page.js +2 -6
- package/test/browser/screen.check.mjs +8 -65
- package/test/browser/section.check.mjs +27 -0
- package/test/fasten-form.test.ts +42 -2
- package/test/first-minute.test.ts +2 -3
- package/test/fixtures/joint-column.json +1 -66
- package/test/fixtures/joint-roller.json +1 -66
- package/test/fixtures/robot-arm-6dof.json +2 -67
- package/test/gizmo.test.ts +12 -1
- package/test/keys.test.ts +11 -0
- package/test/links.test.ts +39 -34
- package/test/parts-wiring.test.ts +1 -1
- package/test/properties.test.ts +9 -12
- package/test/refusals.test.ts +20 -42
- package/test/resize-with-placement.test.ts +13 -15
- package/test/revision.test.ts +1 -2
- package/test/verdict.test.ts +1 -15
- package/translations/en.json +8 -32
- package/translations/es.json +8 -32
- package/translations/ja.json +8 -32
- package/translations/ko.json +8 -32
- package/translations/ms.json +8 -32
- package/translations/zh.json +8 -32
- package/client/modeller/occupancy.ts +0 -51
- package/dist-client/modeller/occupancy.d.ts +0 -31
- package/dist-client/modeller/occupancy.js +0 -36
- package/dist-client/modeller/occupancy.js.map +0 -1
- package/test/release-panel.test.ts +0 -53
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.207",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public",
|
|
6
6
|
"@things-factory:registry": "https://registry.npmjs.org"
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"clean": "npm run clean:server && npm run clean:client"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@hatiolab/figure-model": "^0.
|
|
30
|
+
"@hatiolab/figure-model": "^0.10.0",
|
|
31
31
|
"@hatiolab/things-scene": "^10.1.95",
|
|
32
32
|
"@material/web": "^2.0.0",
|
|
33
33
|
"@operato/data-grist": "^10.29.43",
|
|
@@ -42,11 +42,11 @@
|
|
|
42
42
|
"@operato/slot-layout-editor": "^10.29.43",
|
|
43
43
|
"@operato/styles": "^10.0.0",
|
|
44
44
|
"@operato/utils": "^10.29.43",
|
|
45
|
-
"@things-factory/ai-assistant": "^10.1.
|
|
46
|
-
"@things-factory/figure-service": "^10.1.
|
|
47
|
-
"@things-factory/figure-viewer": "^10.1.
|
|
48
|
-
"@things-factory/setting-base": "^10.1.
|
|
45
|
+
"@things-factory/ai-assistant": "^10.1.207",
|
|
46
|
+
"@things-factory/figure-service": "^10.1.207",
|
|
47
|
+
"@things-factory/figure-viewer": "^10.1.207",
|
|
48
|
+
"@things-factory/setting-base": "^10.1.206",
|
|
49
49
|
"three": "^0.185.1"
|
|
50
50
|
},
|
|
51
|
-
"gitHead": "
|
|
51
|
+
"gitHead": "e6e3fbf5c353629adbeb8850093150b14254e059"
|
|
52
52
|
}
|
|
@@ -10,7 +10,7 @@ import 'figure-ui-client/modeller/figure-properties.js'
|
|
|
10
10
|
import { createFigureProductSession } from 'figure-ui-client/modeller/product-session.js'
|
|
11
11
|
import { runFigureProposal, takeFigureOffer } from 'figure-ui-client/modeller/ai-proposal.js'
|
|
12
12
|
import { i18next } from '@operato/i18n'
|
|
13
|
-
import { addFigureBox, addFigurePrimitive, applyFigureAuthoring, createFigureAsset, editFigurePart,
|
|
13
|
+
import { addFigureBox, addFigurePrimitive, applyFigureAuthoring, createFigureAsset, editFigurePart, serializeFigureAsset } from '@hatiolab/figure-model'
|
|
14
14
|
|
|
15
15
|
const PALETTE = { 'palette.dark': '#2b2b2b', 'palette.neutral': '#9a9a9a', 'palette.primary': '#477b9e', 'palette.accent': '#f79c40', 'palette.light': '#dddddd' }
|
|
16
16
|
|
|
@@ -19,7 +19,7 @@ function box(placement, position) {
|
|
|
19
19
|
let a = createFigureAsset(`Probe ${placement}`)
|
|
20
20
|
a.palette = { ...a.palette, ...PALETTE }
|
|
21
21
|
a = addFigureBox(a, { id: 'box', size: { x: 400, y: 400, z: 400 }, position })
|
|
22
|
-
return serializeFigureAsset(applyFigureAuthoring(a, { kind: '
|
|
22
|
+
return serializeFigureAsset(applyFigureAuthoring(a, { kind: 'set-placement', placement }))
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
function turned(rotation, placement = 'floor', position = { x: 0, y: 400, z: 0 }) {
|
|
@@ -91,7 +91,7 @@ export const MODELS = {
|
|
|
91
91
|
a.palette = { ...a.palette, ...PALETTE }
|
|
92
92
|
a = addFigureBox(a, { id: 'leg', size: { x: 60, y: 700, z: 60 }, position: { x: -400, y: 350, z: 0 } })
|
|
93
93
|
a = addFigureBox(a, { id: 'top', size: { x: 1200, y: 40, z: 600 }, position: { x: 0, y: 720, z: 0 } })
|
|
94
|
-
return serializeFigureAsset(applyFigureAuthoring(a, { kind: '
|
|
94
|
+
return serializeFigureAsset(applyFigureAuthoring(a, { kind: 'set-placement', placement: 'floor' }))
|
|
95
95
|
},
|
|
96
96
|
/* A ceiling figure's part off its ceiling, for the one-off align (to the ceiling, by its top). */
|
|
97
97
|
'ceiling-off': () => box('ceiling', { x: 0, y: -600, z: 0 }),
|
|
@@ -162,7 +162,7 @@ export const MODELS = {
|
|
|
162
162
|
ball: () => {
|
|
163
163
|
let a = createFigureAsset('Ball')
|
|
164
164
|
a.palette = { ...a.palette, ...PALETTE }
|
|
165
|
-
a = addFigurePrimitive(a, { id: 'ball', kind: 'sphere', dimensions: {
|
|
165
|
+
a = addFigurePrimitive(a, { id: 'ball', kind: 'sphere', dimensions: { radius: 200, radiusY: 200 }, position: { x: 0, y: 500, z: 0 } })
|
|
166
166
|
return serializeFigureAsset(applyFigureAuthoring(a, { kind: 'set-placement', placement: 'floor' }))
|
|
167
167
|
},
|
|
168
168
|
/* 500 mm above the floor: keep the gap, or be brought onto it. */
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* Isolated product detail: fixtures are in memory, never saved to the running app. */
|
|
2
2
|
import 'figure-ui-client/viewparts/figure-detail-dock.js'
|
|
3
3
|
import { i18next } from '@operato/i18n'
|
|
4
|
-
import { addFigureBox, createFigureAsset, editFigurePart, applyFigureAuthoring,
|
|
4
|
+
import { addFigureBox, createFigureAsset, editFigurePart, applyFigureAuthoring, serializeFigureAsset } from '@hatiolab/figure-model'
|
|
5
5
|
|
|
6
6
|
async function start() {
|
|
7
7
|
i18next.addResourceBundle('ko', 'translations', await fetch('/ko.json').then(r => r.json()), true, true)
|
|
@@ -14,8 +14,6 @@ async function start() {
|
|
|
14
14
|
id: 'block', type: 'prismatic', body0: { plane: 'mounting-plane' }, body1: { part: 'block' }, axis: 'X', lowerLimit: 0, upperLimit: 200, state: { id: 'shift', start: 0 }
|
|
15
15
|
} })
|
|
16
16
|
asset = editFigurePart(asset, { kind: 'set-driver', state: 'shift', clip: 'cycle', duration: 4, keys: [{ seconds: 0, value: 0 }, { seconds: 2, value: 200 }, { seconds: 4, value: 0 }] })
|
|
17
|
-
/* Its room declared from its parts over the whole motion, as the modeller does before a release. */
|
|
18
|
-
asset = applyFigureAuthoring(asset, { kind: 'declare-occupancy', proposal: proposeFigureOccupancy(asset, { over: 'range' }) })
|
|
19
17
|
const source = serializeFigureAsset(asset)
|
|
20
18
|
const dock = document.createElement('figure-detail-dock')
|
|
21
19
|
let fail = true
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* are in memory; nothing is saved.
|
|
5
5
|
*/
|
|
6
6
|
import 'figure-ui-client/viewparts/figure-detail-dock.js'
|
|
7
|
-
import { addFigureBox, applyFigureAuthoring, createFigureAsset, editFigurePart,
|
|
7
|
+
import { addFigureBox, applyFigureAuthoring, createFigureAsset, editFigurePart, robotDogFigure, serializeFigureAsset } from '@hatiolab/figure-model'
|
|
8
8
|
|
|
9
9
|
const PARTS = 12
|
|
10
10
|
|
|
@@ -17,7 +17,6 @@ for (let i = 0; i < PARTS; i++) {
|
|
|
17
17
|
} })
|
|
18
18
|
asset = editFigurePart(asset, { kind: 'set-driver', state: `${id}-z`, clip: `cycle-${i}`, duration: 2, keys: [{ seconds: 0, value: 0 }, { seconds: 1, value: 100 }, { seconds: 2, value: 0 }] })
|
|
19
19
|
}
|
|
20
|
-
asset = applyFigureAuthoring(asset, { kind: 'declare-occupancy', proposal: proposeFigureOccupancy(asset, { over: 'range' }) })
|
|
21
20
|
const source = serializeFigureAsset(asset)
|
|
22
21
|
|
|
23
22
|
const dock = document.createElement('figure-detail-dock')
|
|
@@ -17,7 +17,6 @@ import {
|
|
|
17
17
|
applyFigureProposal,
|
|
18
18
|
createFigureAsset,
|
|
19
19
|
planFigureResize,
|
|
20
|
-
proposeFigureOccupancy,
|
|
21
20
|
serializeFigureAsset
|
|
22
21
|
} from '@hatiolab/figure-model'
|
|
23
22
|
|
|
@@ -241,7 +240,7 @@ describe('a part made to follow the figure size, and let go of it', () => {
|
|
|
241
240
|
})
|
|
242
241
|
})
|
|
243
242
|
|
|
244
|
-
/* A base and a flange, sized
|
|
243
|
+
/* A base and a flange, sized, saved with ends on every axis: the height from 90 mm, the others from 1 mm. */
|
|
245
244
|
function sizedPair(turning) {
|
|
246
245
|
let a = box(createFigureAsset('Probe'), 'base', [200, 100, 200], [0, 50, 0])
|
|
247
246
|
a = addFigurePrimitive(a, { id: 'flange', kind: 'cylinder', dimensions: { radius: 20, length: 10 }, position: { x: 0, y: 105, z: 0 } })
|
|
@@ -256,16 +255,11 @@ function sizedPair(turning) {
|
|
|
256
255
|
}
|
|
257
256
|
|
|
258
257
|
describe('the sizes a figure may be placed at, and how it grows', () => {
|
|
259
|
-
test('
|
|
258
|
+
test('a figure saved with ends reads them as typed, and nothing is offered to measure again', async () => {
|
|
260
259
|
await mount('sized-pair', sizedPair(false))
|
|
261
260
|
/* The figure's range is the 「반응형」 tab's. */
|
|
262
261
|
await tab('response')
|
|
263
|
-
|
|
264
|
-
await click(await at('button', 'figure.button.remeasure-range'), 'remeasure')
|
|
265
|
-
await oneStep(start)
|
|
266
|
-
const y = (await source()).document.model.inputs.find(i => i.id === 'size.y')
|
|
267
|
-
assert.deepEqual([y.min, y.max], [1, 1000], 'the height from 1 mm, as measured')
|
|
268
|
-
assert.equal(await at('button', 'figure.button.remeasure-range'), null, 'nothing left to measure again')
|
|
262
|
+
assert.equal(await at('button', 'figure.button.remeasure-range'), null, 'nothing to measure again')
|
|
269
263
|
})
|
|
270
264
|
|
|
271
265
|
test('a figure with no joint is not urged to grow by one ratio; declared, the rule is said', async () => {
|
|
@@ -282,6 +276,26 @@ describe('the sizes a figure may be placed at, and how it grows', () => {
|
|
|
282
276
|
})
|
|
283
277
|
})
|
|
284
278
|
|
|
279
|
+
/*
|
|
280
|
+
A link that makes the height impossible below 95 mm is made, not refused (user 2026-10-21): the smallest height the
|
|
281
|
+
author narrowed to, 90 mm, moves in to 95 in the same step, and one line says from what to what (ADR-0087 decision 6).
|
|
282
|
+
*/
|
|
283
|
+
describe('a part change a narrowed end cannot hold', () => {
|
|
284
|
+
test('is one step; the end moves in, said in one line', async () => {
|
|
285
|
+
await mount('sized-pair-moved', sizedPair(false))
|
|
286
|
+
await pick('flange')
|
|
287
|
+
await read(() => look.hear('notify'))
|
|
288
|
+
const start = await state()
|
|
289
|
+
await read(() => look.tell('shape', 'command', { action: { kind: 'link-dimension', part: 'flange', axis: 'y', source: { of: 'instance', axis: 'y' }, plus: -95 } }))
|
|
290
|
+
await oneStep(start)
|
|
291
|
+
assert.equal((await state()).error, '', 'not refused')
|
|
292
|
+
const y = (await source()).document.model.inputs.find(i => i.id === 'size.y')
|
|
293
|
+
assert.ok(y.min > 95 && y.min < 95.001 && y.max === 1000, `the smallest height moved in: ${y.min}`)
|
|
294
|
+
const told = (await heard('notify')).filter(n => n?.level === 'info' && String(n.message).includes('figure.text.size-range-moved-min'))
|
|
295
|
+
assert.equal(told.length, 1, 'said once')
|
|
296
|
+
})
|
|
297
|
+
})
|
|
298
|
+
|
|
285
299
|
/* A conveyor's rollers in a row across its frame, 200 mm apart. */
|
|
286
300
|
function conveyor() {
|
|
287
301
|
let a = fasten(box(createFigureAsset('Conveyor'), 'frame', [2000, 100, 600], [0, 50, 0]), 'frame', 'plane')
|
|
@@ -399,9 +413,11 @@ describe('a new fastening for a door leaf: chosen in the window, seen before it
|
|
|
399
413
|
assert.deepEqual(await read(() => probe.fastenPopup()), { title: 'figure.label.fastening', movable: true, backdrop: false, role: 'dialog' })
|
|
400
414
|
})
|
|
401
415
|
|
|
402
|
-
|
|
416
|
+
/* Chosen in the window, the gap is the distance the leaf stands at: it is fastened where it is (ADR-0140 stage 4, 「현재 상태로 붙이기」). */
|
|
417
|
+
test('the frame chosen: the editor is told the command, and the canvas draws the face it meets; the leaf stays where it stands', async () => {
|
|
403
418
|
await aimAt('frame')
|
|
404
|
-
await until(() => look.drawn().includes('
|
|
419
|
+
await until(() => look.drawn().includes('__target-face') && look.all('[preview-words]', 'figure.text.preview-stays').length > 0, [], 'the face drawn and the leaf said to stay')
|
|
420
|
+
assert.deepEqual(await ghosts(), [], 'nothing drawn where it would go: it does not move')
|
|
405
421
|
const told = (await heard('preview-command')).filter(Boolean).at(-1)
|
|
406
422
|
assert.equal(told?.action.kind, 'attach')
|
|
407
423
|
assert.equal((await source()).joints?.some(j => j.body1.part === 'leaf') ?? false, false, 'nothing written')
|
|
@@ -443,7 +459,7 @@ describe('a new fastening for a door leaf: chosen in the window, seen before it
|
|
|
443
459
|
|
|
444
460
|
test('Esc closes the window and tells the editor to clear what it tried; the leaf stays selected', async () => {
|
|
445
461
|
await aimAt('frame')
|
|
446
|
-
await until(() => look.drawn().includes('
|
|
462
|
+
await until(() => look.drawn().includes('__target-face'), [], 'the face it would meet drawn')
|
|
447
463
|
await read(() => look.hear('preview-command', 'dialog'))
|
|
448
464
|
await page().keyboard.press('Escape')
|
|
449
465
|
await until(() => !probe.fastenTargets(), [], 'the window closed')
|
|
@@ -459,9 +475,9 @@ describe('a new fastening for a door leaf: chosen in the window, seen before it
|
|
|
459
475
|
await aimAt('frame')
|
|
460
476
|
/*
|
|
461
477
|
The frame's faces are offered once the drawing has the frame: a press before that is not a choice. And the press is
|
|
462
|
-
aimed once the window has drawn what the choice brings -- the
|
|
478
|
+
aimed once the window has drawn what the choice brings -- the face it would meet -- or it lands where the face was.
|
|
463
479
|
*/
|
|
464
|
-
await until(() => look.face('front')?.pickable && look.drawn().includes('
|
|
480
|
+
await until(() => look.face('front')?.pickable && look.drawn().includes('__target-face'), [], 'the front face offered, the face it would meet drawn')
|
|
465
481
|
await settled()
|
|
466
482
|
await sleep(200)
|
|
467
483
|
await click(await at('path[face="front"]'), 'the front face in the drawing')
|
|
@@ -666,52 +682,18 @@ describe('a part fastened twisted is sized by a drag: its size alone, its fasten
|
|
|
666
682
|
describe('a box made to follow the figure size on the floor', () => {
|
|
667
683
|
before(async () => {
|
|
668
684
|
let a = box(createFigureAsset('Floor'), 'box', [100, 100, 100], [20, 50, 30])
|
|
669
|
-
a = applyFigureAuthoring(a, { kind: '
|
|
685
|
+
a = applyFigureAuthoring(a, { kind: 'set-placement', placement: 'floor' })
|
|
670
686
|
await mount('floor', applyFigureProposal(a, planFigureResize(a, { part: 'box' }).steps))
|
|
671
687
|
await pick('box')
|
|
672
688
|
})
|
|
673
689
|
|
|
674
|
-
test('its
|
|
675
|
-
const roomOnly = await all('input[aria-label="box.position.y"]')
|
|
676
|
-
assert.equal(roomOnly.length, 1, 'one field for it, apart from the position row')
|
|
677
|
-
assert.equal(roomOnly[0].disabled, true, 'not typed into')
|
|
678
|
-
assert.equal((await all('section[room-only] input[aria-label="box.position.y"]')).length, 1, 'in the room\'s section')
|
|
690
|
+
test('its row shows the number the floor gives; a size drag is drawn by the modeller', async () => {
|
|
679
691
|
const held = (await all('div[vector-row] input[readonly]')).filter(i => i.label?.startsWith('position.y'))
|
|
680
692
|
assert.deepEqual(held.map(i => i.value), ['50'], 'the height the floor gives, read-only')
|
|
681
693
|
assert.equal(await read(() => look.scalePlacement('box')), 'owner', 'its place in a size drag is the modeller\'s (ADR-0140 4.4)')
|
|
682
694
|
})
|
|
683
|
-
|
|
684
|
-
test('the room is declared again only when the author confirms', async () => {
|
|
685
|
-
const prompt = id =>
|
|
686
|
-
read(id => {
|
|
687
|
-
const deep = (root = document) => [...root.querySelectorAll('ox-prompt')].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
|
|
688
|
-
const button = deep().at(-1)?.shadowRoot?.querySelector(`#${id}`)
|
|
689
|
-
if (!button) return null
|
|
690
|
-
const r = button.getBoundingClientRect()
|
|
691
|
-
return r.width ? { x: r.left + r.width / 2, y: r.top + r.height / 2 } : null
|
|
692
|
-
}, id)
|
|
693
|
-
const start = await state()
|
|
694
|
-
await click(await at('button[room-refit]'), 'declare the room again')
|
|
695
|
-
await until(() => !!document.querySelector('ox-prompt'), [], 'asked')
|
|
696
|
-
await click(await poll(() => prompt('cancel')), 'cancel')
|
|
697
|
-
await until(() => !document.querySelector('ox-prompt'), [], 'the question gone')
|
|
698
|
-
assert.equal((await state()).revision, start.revision, 'cancelled: nothing written')
|
|
699
|
-
await click(await at('button[room-refit]'), 'declare the room again')
|
|
700
|
-
await click(await poll(() => prompt('confirm')), 'confirm')
|
|
701
|
-
await until(r => probe.state().revision === r + 1, [start.revision], 'confirmed: one step')
|
|
702
|
-
})
|
|
703
695
|
})
|
|
704
696
|
|
|
705
|
-
/** Asks until the answer is something, for a few seconds. */
|
|
706
|
-
async function poll(ask, ms = 5000) {
|
|
707
|
-
const deadline = Date.now() + ms
|
|
708
|
-
for (;;) {
|
|
709
|
-
const answer = await ask()
|
|
710
|
-
if (answer || Date.now() > deadline) return answer
|
|
711
|
-
await sleep(50)
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
|
|
715
697
|
/*
|
|
716
698
|
* The fastening window writes once (user 2026-10-17): whatever is tried in it is a preview, 「적용」 is one step of the
|
|
717
699
|
* history with all of it, and 「취소」 or Esc leave the history as it was.
|
|
@@ -452,8 +452,8 @@ describe('a figure made in the modeller is given its size from its parts (user 2
|
|
|
452
452
|
assert.equal((await declared())['size.x'][1], 3000, 'the largest width is what was typed')
|
|
453
453
|
await type('size.x.min', '500')
|
|
454
454
|
assert.notEqual((await state()).error, '', 'the figure is 400 wide: a smallest of 500 is refused')
|
|
455
|
-
assert.equal((await declared())['size.x'][0]
|
|
456
|
-
assert.
|
|
455
|
+
assert.equal((await declared())['size.x'][0] ?? null, null, 'and the smallest is still open (ADR-0087 decision 6)')
|
|
456
|
+
assert.deepEqual(await (await field('size.x.min')).evaluate(i => [i.value, i.placeholder]), ['', '0'], 'the field empty again, what the parts allow written faintly in it')
|
|
457
457
|
})
|
|
458
458
|
|
|
459
459
|
/*
|
|
@@ -467,6 +467,34 @@ describe('a figure made in the modeller is given its size from its parts (user 2
|
|
|
467
467
|
assert.notEqual(said, 'figure.text.edit-refused', `said in its own words: ${said}`)
|
|
468
468
|
})
|
|
469
469
|
|
|
470
|
+
test('a field emptied opens its end again, in one step: the largest is what the parts allow, nothing', async () => {
|
|
471
|
+
const field = () =>
|
|
472
|
+
page().evaluateHandle(() => {
|
|
473
|
+
const deep = (root = document) => [...root.querySelectorAll('input')].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
|
|
474
|
+
return deep().find(i => i.getAttribute('aria-label') === 'size.x.max') ?? null
|
|
475
|
+
})
|
|
476
|
+
/* Selected where it is drawn, as a person does, then typed over. */
|
|
477
|
+
const typeOver = async keys => {
|
|
478
|
+
const at = await (await field()).evaluate(i => {
|
|
479
|
+
i.scrollIntoView({ block: 'center' })
|
|
480
|
+
const r = i.getBoundingClientRect()
|
|
481
|
+
return { x: r.left + r.width / 2, y: r.top + r.height / 2 }
|
|
482
|
+
})
|
|
483
|
+
await page().mouse.click(at.x, at.y, { clickCount: 3 })
|
|
484
|
+
await keys()
|
|
485
|
+
await page().keyboard.press('Tab')
|
|
486
|
+
await sleep(600)
|
|
487
|
+
}
|
|
488
|
+
await typeOver(() => page().keyboard.type('3000'))
|
|
489
|
+
assert.equal(await (await field()).evaluate(i => i.value), '3000', 'narrowed first')
|
|
490
|
+
const start = await state()
|
|
491
|
+
await typeOver(() => page().keyboard.press('Backspace'))
|
|
492
|
+
const after = await state()
|
|
493
|
+
assert.equal(after.error, '', 'not refused')
|
|
494
|
+
assert.equal(after.revision, start.revision + 1, 'one step')
|
|
495
|
+
assert.deepEqual(await (await field()).evaluate(i => [i.value, i.placeholder]), ['', '∞'], 'empty, and nothing limits it')
|
|
496
|
+
})
|
|
497
|
+
|
|
470
498
|
test('a part follows the figure\'s width less a difference: chosen and typed, the width is the size less it', async () => {
|
|
471
499
|
await click(await read(() => probe.partOnPage('box')))
|
|
472
500
|
assert.equal((await state()).selected, 'box')
|
|
@@ -500,23 +528,17 @@ describe('a figure made in the modeller is given its size from its parts (user 2
|
|
|
500
528
|
await page().keyboard.type('-50')
|
|
501
529
|
await page().keyboard.press('Tab')
|
|
502
530
|
await sleep(600)
|
|
503
|
-
/*
|
|
504
|
-
|
|
505
|
-
|
|
531
|
+
/*
|
|
532
|
+
The sizes the figure may be drawn at are worked out from its parts (ADR-0087 decision 6), not refused against a
|
|
533
|
+
range nobody chose: the link is taken as typed, with nothing asked.
|
|
534
|
+
*/
|
|
535
|
+
assert.equal((await state()).error, '', 'taken')
|
|
536
|
+
assert.equal((await body('box')).dimensions['size.x'], 350, 'the figure\'s 400 less 50')
|
|
506
537
|
const asked = await read(() => {
|
|
507
538
|
const deep = (root = document) => [...root.querySelectorAll('ox-prompt')].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
|
|
508
|
-
|
|
509
|
-
const confirm = prompt?.shadowRoot?.querySelector('#confirm')
|
|
510
|
-
if (!confirm) return null
|
|
511
|
-
const r = confirm.getBoundingClientRect()
|
|
512
|
-
return { text: prompt.text, label: confirm.textContent.trim(), x: r.left + r.width / 2, y: r.top + r.height / 2 }
|
|
539
|
+
return deep().length
|
|
513
540
|
})
|
|
514
|
-
assert.
|
|
515
|
-
assert.match(asked.label, /figure.button.narrow-range-from/, 'the same link is offered with the sizes narrowed')
|
|
516
|
-
await click(asked)
|
|
517
|
-
await sleep(600)
|
|
518
|
-
assert.equal((await state()).error, '', 'taken')
|
|
519
|
-
assert.equal((await body('box')).dimensions['size.x'], 350, 'the figure\'s 400 less 50')
|
|
541
|
+
assert.equal(asked, 0, 'nothing asked')
|
|
520
542
|
})
|
|
521
543
|
|
|
522
544
|
test('grown by one ratio, every part is made to follow it in one step, and the offer goes', async () => {
|
|
@@ -534,8 +556,15 @@ describe('a figure made in the modeller is given its size from its parts (user 2
|
|
|
534
556
|
return { x: r.left + r.width / 2, y: r.top + r.height / 2 }
|
|
535
557
|
}))
|
|
536
558
|
}
|
|
559
|
+
/* Back to the whole figure: a key in a field is the field's, so the field typed in is left first, then Esc. */
|
|
560
|
+
await page().evaluate(() => {
|
|
561
|
+
let focused = document.activeElement
|
|
562
|
+
while (focused?.shadowRoot?.activeElement) focused = focused.shadowRoot.activeElement
|
|
563
|
+
focused?.blur()
|
|
564
|
+
})
|
|
537
565
|
await page().keyboard.press('Escape')
|
|
538
566
|
await sleep(300)
|
|
567
|
+
assert.equal((await state()).selected, '', 'nothing picked')
|
|
539
568
|
await press('figure.button.scale-uniformly')
|
|
540
569
|
assert.equal((await state()).error, '', 'grows by one ratio')
|
|
541
570
|
const start = await state()
|
|
@@ -740,9 +769,7 @@ describe('a part follows the figure size in one step (follow-size, 2026-09-26)',
|
|
|
740
769
|
assert.equal(after.revision, start.revision + 1, 'one step')
|
|
741
770
|
assert.deepEqual(await read(() => probe.fields('size.')), { 'size.x': 400, 'size.y': 400, 'size.z': 400 }, 'the size is the box as it stands')
|
|
742
771
|
const notice = await read(() => (window.__told ?? []).filter(n => n.level === 'info').at(-1)?.message ?? null)
|
|
743
|
-
|
|
744
|
-
assert.ok(notice?.includes('figure.text.size-and-space-declared-to-follow'), `a line says so: ${notice}`)
|
|
745
|
-
assert.equal(await read(() => !!document.querySelector('figure-editor').session.revisionNow().asset.occupancy), true, 'the space declared by the same step')
|
|
772
|
+
assert.ok(notice?.includes('figure.text.size-declared-to-follow'), `a line says so: ${notice}`)
|
|
746
773
|
const grown = await read(() => {
|
|
747
774
|
const editor = document.querySelector('figure-editor')
|
|
748
775
|
editor.session.apply([{ kind: 'set-design', input: 'size.x', unit: 'mm', value: 800 }])
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Only reading: every change the check makes goes through the mouse and the keyboard.
|
|
6
6
|
*/
|
|
7
7
|
import './editing-page.js'
|
|
8
|
-
import { addFigureBox, applyFigureAuthoring, createFigureAsset,
|
|
8
|
+
import { addFigureBox, applyFigureAuthoring, createFigureAsset, serializeFigureAsset } from '@hatiolab/figure-model'
|
|
9
9
|
|
|
10
10
|
const PALETTE = {
|
|
11
11
|
'palette.dark': '#2b2b2b',
|
|
@@ -42,7 +42,7 @@ Object.assign(window.figures, {
|
|
|
42
42
|
a = addFigureBox(a, { id: 'housing', size: { x: 300, y: 1000, z: 300 }, position: { x: 0, y: 500, z: 0 } })
|
|
43
43
|
a = addFigureBox(a, { id: 'stray', size: { x: 100, y: 100, z: 100 }, position: { x: 600, y: 50, z: 0 } })
|
|
44
44
|
for (const part of ['housing', 'stray']) a = applyFigureAuthoring(a, { kind: 'attach', part, to: { plane: 'mounting-plane' } })
|
|
45
|
-
return serializeFigureAsset(applyFigureAuthoring(a, { kind: '
|
|
45
|
+
return serializeFigureAsset(applyFigureAuthoring(a, { kind: 'set-placement', placement: 'floor' }))
|
|
46
46
|
},
|
|
47
47
|
rounded: () => {
|
|
48
48
|
let a = figure('Rounded')
|
|
@@ -141,6 +141,55 @@ describe('the parts list: each button does what it says, one step of the session
|
|
|
141
141
|
})
|
|
142
142
|
})
|
|
143
143
|
|
|
144
|
+
/*
|
|
145
|
+
Copy and paste with the canvas focused (user 2026-10-21): left to the scene, Ctrl+C / Ctrl+V copied the canvas object
|
|
146
|
+
with the part's name -- two objects of one name, the copy moving the original and never a part of its own.
|
|
147
|
+
*/
|
|
148
|
+
describe('copy and paste on the canvas: a part of its own, named by figure-model', () => {
|
|
149
|
+
before(() => mount('box', 'three'))
|
|
150
|
+
|
|
151
|
+
const withControl = async code => {
|
|
152
|
+
await page().keyboard.down('Control')
|
|
153
|
+
await page().keyboard.press(code)
|
|
154
|
+
await page().keyboard.up('Control')
|
|
155
|
+
await sleep(400)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
test('Ctrl+C then Ctrl+V on the canvas: one step, a new row with a name of its own, one canvas object per name', async () => {
|
|
159
|
+
await click(await read(() => probe.partOnPage('b')), 'b on the canvas')
|
|
160
|
+
assert.equal((await state()).selected, 'b', 'picked on the canvas, the canvas focused')
|
|
161
|
+
const was = await ids()
|
|
162
|
+
const step = await revision()
|
|
163
|
+
await withControl('KeyC')
|
|
164
|
+
await withControl('KeyV')
|
|
165
|
+
const now = await ids()
|
|
166
|
+
const made = now.filter(id => !was.includes(id))
|
|
167
|
+
assert.equal(made.length, 1, `one new part: ${now}`)
|
|
168
|
+
assert.notEqual(made[0], 'b')
|
|
169
|
+
assert.equal(await revision(), step + 1, 'one step')
|
|
170
|
+
assert.equal((await state()).selected, made[0], 'the copy is the part selected')
|
|
171
|
+
const drawn = await read(() => parts.drawn())
|
|
172
|
+
assert.equal(new Set(drawn).size, drawn.length, `no two canvas objects of one name: ${drawn}`)
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
test('Ctrl+X and Ctrl+Shift+V are kept from the scene: nothing cut or pasted on the canvas alone', async () => {
|
|
176
|
+
await click(await read(() => probe.partOnPage('b')), 'b on the canvas')
|
|
177
|
+
const was = await ids()
|
|
178
|
+
const step = await revision()
|
|
179
|
+
await withControl('KeyX')
|
|
180
|
+
await page().keyboard.down('Control')
|
|
181
|
+
await page().keyboard.down('Shift')
|
|
182
|
+
await page().keyboard.press('KeyV')
|
|
183
|
+
await page().keyboard.up('Shift')
|
|
184
|
+
await page().keyboard.up('Control')
|
|
185
|
+
await sleep(400)
|
|
186
|
+
assert.deepEqual(await ids(), was)
|
|
187
|
+
assert.equal(await revision(), step)
|
|
188
|
+
const drawn = await read(() => parts.drawn())
|
|
189
|
+
assert.deepEqual([...drawn].sort(), [...was].sort(), 'the canvas draws the figure\'s parts, no more, no fewer')
|
|
190
|
+
})
|
|
191
|
+
})
|
|
192
|
+
|
|
144
193
|
describe('hidden on the workbench: off the canvas, not out of the figure, and let go when another figure opens', () => {
|
|
145
194
|
before(() => mount('box', 'three'))
|
|
146
195
|
|
|
@@ -239,25 +288,15 @@ describe('a part renamed in its name field: one step, still selected; a name ref
|
|
|
239
288
|
})
|
|
240
289
|
})
|
|
241
290
|
|
|
242
|
-
describe('a part
|
|
243
|
-
test('one step, nothing asked
|
|
291
|
+
describe('a part on the plane beside another removed: at once (2026-09-27)', () => {
|
|
292
|
+
test('one step, nothing asked', async () => {
|
|
244
293
|
await mount('box', 'gate')
|
|
245
294
|
await pick('stray')
|
|
246
295
|
const before = await revision()
|
|
247
|
-
const space = () => read(() => JSON.parse(document.querySelector('figure-editor').session.source()).occupancy ?? null)
|
|
248
|
-
const was = await space()
|
|
249
|
-
assert.ok(was, 'declared to begin with')
|
|
250
296
|
await click(await read(() => parts.op('remove-part')), 'remove')
|
|
251
297
|
assert.deepEqual(await ids(), ['housing'])
|
|
252
298
|
assert.equal(await revision(), before + 1, 'one step')
|
|
253
299
|
assert.equal(await read(() => editing.prompt()), null, 'nothing asked')
|
|
254
|
-
const now = await space()
|
|
255
|
-
assert.ok(now, 'the space is still declared')
|
|
256
|
-
assert.notDeepEqual(now, was, 'fitted to the part left')
|
|
257
|
-
const told = await read(() => editing.told())
|
|
258
|
-
assert.deepEqual(told.map(t => t.level), ['info'], JSON.stringify(told))
|
|
259
|
-
/* The page loads no translations: the words are the key. */
|
|
260
|
-
assert.ok(told[0].message.includes('figure.text.removal-space-refitted'), told[0].message)
|
|
261
300
|
})
|
|
262
301
|
})
|
|
263
302
|
|
|
@@ -285,7 +285,7 @@ describe('the history panel on the page', () => {
|
|
|
285
285
|
product.answer('figureVersions', () => [VERSION_1])
|
|
286
286
|
product.answer('inspectFigure', () => ({
|
|
287
287
|
blocked: true,
|
|
288
|
-
findings: ['
|
|
288
|
+
findings: ['off-mounting-plane', 'parts-off-placement-face', 'type-is-placeholder'].map(code => ({ code, message: code, why: null, how: null, at: null, blocking: true }))
|
|
289
289
|
}))
|
|
290
290
|
}, VERSION_1)
|
|
291
291
|
await open('h')
|
|
@@ -305,13 +305,12 @@ describe('the history panel on the page', () => {
|
|
|
305
305
|
assert.deepEqual(link, { href: '/figure-package?refs=acme%2Fagv%401', download: true })
|
|
306
306
|
})
|
|
307
307
|
|
|
308
|
-
test('
|
|
309
|
-
const
|
|
310
|
-
assert.deepEqual(
|
|
308
|
+
test('the findings stand in the history panel; an edit beside it is one step', async () => {
|
|
309
|
+
const shown = await until(p => { const found = product.deep(`${p} >>> li[finding]`); return found.length === 3 && found.map(li => li.querySelector('[msg]').textContent).sort() }, 'the findings', PANEL)
|
|
310
|
+
assert.deepEqual(shown, ['off-mounting-plane', 'parts-off-placement-face', 'type-is-placeholder'])
|
|
311
311
|
const before = await read(() => product.view())
|
|
312
|
-
await
|
|
312
|
+
await rename('H2')
|
|
313
313
|
await until(r => product.view().revision === r + 1, 'one step', before.revision)
|
|
314
|
-
await until(() => !!JSON.parse(product.view().source).occupancy, 'the room declared')
|
|
315
314
|
await openHistory()
|
|
316
315
|
})
|
|
317
316
|
|
|
@@ -557,7 +556,6 @@ describe('the page around the figure: the shell, the dock, the properties', () =
|
|
|
557
556
|
})
|
|
558
557
|
await until(r => product.view().revision === r + 1, 'one step', before.revision)
|
|
559
558
|
assert.equal(JSON.parse((await read(() => product.view())).source).placement, 'ceiling')
|
|
560
|
-
assert.equal(JSON.parse((await read(() => product.view())).source).occupancy, undefined, 'no room declared on the way')
|
|
561
559
|
})
|
|
562
560
|
|
|
563
561
|
test('unsaved: closing or reloading the tab is asked about', async () => {
|
|
@@ -806,8 +804,7 @@ describe('a mounting face named already', () => {
|
|
|
806
804
|
const before = await read(() => product.view())
|
|
807
805
|
await press('figure-properties >>> button[clear-mounting-face]', 'take the mounting face off')
|
|
808
806
|
await until(r => product.view().revision === r + 1, 'one step', before.revision)
|
|
809
|
-
assert.equal(JSON.parse((await read(() => product.view())).source).
|
|
810
|
-
assert.ok(JSON.parse((await read(() => product.view())).source).occupancy.bounds, 'the room stays')
|
|
807
|
+
assert.equal(JSON.parse((await read(() => product.view())).source).mountingFace, undefined, 'taken off')
|
|
811
808
|
await until(() => !product.deep('figure-modeller-page >>> figure-properties')[0].shadowRoot.querySelector('div[mounting-face]'), 'the line gone')
|
|
812
809
|
})
|
|
813
810
|
|
|
@@ -20,7 +20,7 @@ import { store } from '@operato/shell'
|
|
|
20
20
|
import { addressController } from '@operato/utils/address.js'
|
|
21
21
|
import './popup-stand-in.js'
|
|
22
22
|
import { html, render } from 'lit'
|
|
23
|
-
import { addFigureBox, applyFigureAuthoring, createFigureAsset, editFigurePart,
|
|
23
|
+
import { addFigureBox, applyFigureAuthoring, createFigureAsset, editFigurePart, serializeFigureAsset } from '@hatiolab/figure-model'
|
|
24
24
|
|
|
25
25
|
const PALETTE = { 'palette.dark': '#2b2b2b', 'palette.neutral': '#9a9a9a', 'palette.primary': '#477b9e', 'palette.accent': '#f79c40', 'palette.light': '#dddddd' }
|
|
26
26
|
|
|
@@ -35,7 +35,6 @@ export function mountedSource(placement = 'floor') {
|
|
|
35
35
|
let a = applyFigureAuthoring(createFigureAsset('Mounted'), { kind: 'set-placement', placement })
|
|
36
36
|
a.palette = { ...a.palette, ...PALETTE }
|
|
37
37
|
a = addFigureBox(a, { id: 'box', size: { x: 400, y: 400, z: 400 }, position: { x: 0, y: 200, z: 0 } })
|
|
38
|
-
a = applyFigureAuthoring(a, { kind: 'declare-occupancy', proposal: proposeFigureOccupancy(a) })
|
|
39
38
|
return serializeFigureAsset(applyFigureAuthoring(a, { kind: 'set-mounting-face', part: 'box', face: 'bottom' }))
|
|
40
39
|
}
|
|
41
40
|
|
|
@@ -16,7 +16,6 @@ import {
|
|
|
16
16
|
compileFigureAsset,
|
|
17
17
|
createFigureAsset,
|
|
18
18
|
editFigurePart,
|
|
19
|
-
proposeFigureOccupancy,
|
|
20
19
|
robotArmFigure,
|
|
21
20
|
serializeFigureAsset
|
|
22
21
|
} from '@hatiolab/figure-model'
|
|
@@ -67,18 +66,15 @@ const FIGURES = {
|
|
|
67
66
|
let a = addFigureBox(named('Stack'), { id: 'base', size: { x: 1000, y: 200, z: 600 }, position: { x: 0, y: 100, z: 0 } })
|
|
68
67
|
a = addFigureBox(a, { id: 'top', size: { x: 200, y: 200, z: 200 }, position: { x: 0, y: 300, z: 0 } })
|
|
69
68
|
a = applyFigureAuthoring(a, { kind: 'attach', part: 'top', to: { part: 'base', face: 'top' } })
|
|
70
|
-
a = applyFigureAuthoring(a, { kind: 'declare-occupancy', proposal: proposeFigureOccupancy(a) })
|
|
71
69
|
return applyFigureAuthoring(a, { kind: 'set-mounting-face', part: 'base', face: 'bottom' })
|
|
72
70
|
},
|
|
73
|
-
/* A conveyor that follows its width, a roller every 200 mm
|
|
71
|
+
/* A conveyor that follows its width, a roller every 200 mm: the one figure stretched. */
|
|
74
72
|
stretch: () => {
|
|
75
73
|
let a = addFigureBox(named('Stretch'), { id: 'frame', size: { x: 2000, y: 100, z: 600 }, position: { x: 0, y: 50, z: 0 } })
|
|
76
74
|
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 } })
|
|
77
75
|
a = applyFigureAuthoring(a, { kind: 'attach', part: 'roller', to: { part: 'frame', face: 'top' } })
|
|
78
76
|
a = applyFigureAuthoring(a, { kind: 'follow-size', part: 'frame', axes: ['x'] })
|
|
79
|
-
|
|
80
|
-
/* 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). */
|
|
81
|
-
return applyFigureAuthoring(a, { kind: 'declare-occupancy', proposal: proposeFigureOccupancy(a, { over: 'range' }), padding: { x: 100, y: 0, z: 0 } })
|
|
77
|
+
return applyFigureAuthoring(a, { kind: 'make-repeat', part: 'roller', axis: 'x', pitch: 200 })
|
|
82
78
|
},
|
|
83
79
|
/* The robot arm with a clip that turns its base: controls and a clip in the preview. */
|
|
84
80
|
'arm-clip': () => editFigurePart(robotArmFigure(), { kind: 'set-driver', state: 'j1-base', clip: 'cycle', duration: 4, keys: [{ seconds: 0, value: 0 }, { seconds: 4, value: 60 }] })
|