@things-factory/figure-ui 10.1.244 → 10.1.246
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/graphql/index.ts +15 -0
- package/client/modeller/align.ts +51 -13
- package/client/modeller/clips.ts +51 -1
- package/client/modeller/data-interface.ts +16 -0
- package/client/modeller/editor-styles.ts +6 -0
- package/client/modeller/editor.ts +112 -25
- package/client/modeller/fasten-form.ts +8 -1
- package/client/modeller/fasten-geometry.ts +24 -2
- package/client/modeller/figure-clips.ts +137 -4
- package/client/modeller/figure-fasten-dialog.ts +1 -0
- package/client/modeller/figure-fasten-drawing.ts +11 -5
- package/client/modeller/figure-links.ts +25 -0
- package/client/modeller/figure-material-panel.ts +54 -4
- package/client/modeller/figure-parts.ts +7 -2
- package/client/modeller/figure-pipe-panel.ts +215 -0
- package/client/modeller/figure-preview.ts +125 -4
- package/client/modeller/figure-section-editor.ts +36 -3
- package/client/modeller/links.ts +24 -2
- package/client/modeller/material-edit.ts +9 -0
- package/client/modeller/maturity.ts +15 -4
- package/client/modeller/palette.ts +9 -0
- package/client/modeller/pipe-path.ts +21 -0
- package/client/modeller/refusals.ts +66 -1
- package/client/modeller/section-controls.ts +2 -1
- package/client/modeller/section-drawing.ts +20 -1
- package/client/modeller/starting-shape.ts +8 -5
- package/client/pages/figure-list-page.ts +15 -5
- package/dist-client/graphql/index.d.ts +2 -0
- package/dist-client/graphql/index.js +15 -0
- package/dist-client/graphql/index.js.map +1 -1
- package/dist-client/modeller/align.d.ts +28 -7
- package/dist-client/modeller/align.js +37 -12
- package/dist-client/modeller/align.js.map +1 -1
- package/dist-client/modeller/clips.d.ts +24 -0
- package/dist-client/modeller/clips.js +53 -1
- package/dist-client/modeller/clips.js.map +1 -1
- package/dist-client/modeller/data-interface.d.ts +6 -0
- package/dist-client/modeller/data-interface.js +17 -0
- package/dist-client/modeller/data-interface.js.map +1 -1
- package/dist-client/modeller/editor-styles.js +6 -0
- package/dist-client/modeller/editor-styles.js.map +1 -1
- package/dist-client/modeller/editor.d.ts +10 -1
- package/dist-client/modeller/editor.js +117 -26
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/fasten-form.d.ts +2 -0
- package/dist-client/modeller/fasten-form.js +5 -1
- package/dist-client/modeller/fasten-form.js.map +1 -1
- package/dist-client/modeller/fasten-geometry.d.ts +8 -1
- package/dist-client/modeller/fasten-geometry.js +22 -1
- package/dist-client/modeller/fasten-geometry.js.map +1 -1
- package/dist-client/modeller/figure-clips.d.ts +9 -0
- package/dist-client/modeller/figure-clips.js +137 -4
- package/dist-client/modeller/figure-clips.js.map +1 -1
- package/dist-client/modeller/figure-fasten-dialog.js +1 -0
- package/dist-client/modeller/figure-fasten-dialog.js.map +1 -1
- package/dist-client/modeller/figure-fasten-drawing.d.ts +3 -1
- package/dist-client/modeller/figure-fasten-drawing.js +13 -5
- package/dist-client/modeller/figure-fasten-drawing.js.map +1 -1
- package/dist-client/modeller/figure-links.d.ts +5 -0
- package/dist-client/modeller/figure-links.js +22 -0
- package/dist-client/modeller/figure-links.js.map +1 -1
- package/dist-client/modeller/figure-material-panel.d.ts +2 -0
- package/dist-client/modeller/figure-material-panel.js +53 -4
- package/dist-client/modeller/figure-material-panel.js.map +1 -1
- package/dist-client/modeller/figure-parts.d.ts +1 -1
- package/dist-client/modeller/figure-parts.js +5 -1
- package/dist-client/modeller/figure-parts.js.map +1 -1
- package/dist-client/modeller/figure-pipe-panel.d.ts +29 -0
- package/dist-client/modeller/figure-pipe-panel.js +221 -0
- package/dist-client/modeller/figure-pipe-panel.js.map +1 -0
- package/dist-client/modeller/figure-preview.d.ts +8 -0
- package/dist-client/modeller/figure-preview.js +128 -4
- package/dist-client/modeller/figure-preview.js.map +1 -1
- package/dist-client/modeller/figure-section-editor.d.ts +5 -0
- package/dist-client/modeller/figure-section-editor.js +33 -3
- package/dist-client/modeller/figure-section-editor.js.map +1 -1
- package/dist-client/modeller/links.d.ts +6 -0
- package/dist-client/modeller/links.js +17 -2
- package/dist-client/modeller/links.js.map +1 -1
- package/dist-client/modeller/material-edit.d.ts +8 -0
- package/dist-client/modeller/material-edit.js +8 -0
- package/dist-client/modeller/material-edit.js.map +1 -1
- package/dist-client/modeller/maturity.js +16 -5
- package/dist-client/modeller/maturity.js.map +1 -1
- package/dist-client/modeller/palette.js +9 -0
- package/dist-client/modeller/palette.js.map +1 -1
- package/dist-client/modeller/pipe-path.d.ts +19 -0
- package/dist-client/modeller/pipe-path.js +14 -0
- package/dist-client/modeller/pipe-path.js.map +1 -0
- package/dist-client/modeller/refusals.d.ts +12 -1
- package/dist-client/modeller/refusals.js +67 -0
- package/dist-client/modeller/refusals.js.map +1 -1
- package/dist-client/modeller/section-controls.js +2 -1
- package/dist-client/modeller/section-controls.js.map +1 -1
- package/dist-client/modeller/section-drawing.d.ts +5 -0
- package/dist-client/modeller/section-drawing.js +19 -1
- package/dist-client/modeller/section-drawing.js.map +1 -1
- package/dist-client/modeller/starting-shape.d.ts +9 -2
- package/dist-client/modeller/starting-shape.js +8 -3
- package/dist-client/modeller/starting-shape.js.map +1 -1
- package/dist-client/pages/figure-list-page.d.ts +1 -0
- package/dist-client/pages/figure-list-page.js +12 -3
- package/dist-client/pages/figure-list-page.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +8 -8
- package/test/align.test.ts +28 -6
- package/test/browser/editing-page.js +21 -1
- package/test/browser/editing.check.mjs +124 -0
- package/test/browser/links.check.mjs +49 -8
- package/test/browser/modeller.check.mjs +27 -1
- package/test/browser/part-ids.check.mjs +60 -3
- package/test/browser/parts.check.mjs +2 -2
- package/test/browser/places-mesh.check.mjs +14 -8
- package/test/browser/screen.check.mjs +54 -1
- package/test/browser/section-page.js +7 -1
- package/test/browser/section.check.mjs +81 -0
- package/test/browser/side-panel-sweep.check.mjs +13 -0
- package/test/clips.test.ts +30 -1
- package/test/data-motion.test.ts +14 -1
- package/test/fasten-geometry.test.ts +13 -2
- package/test/joint-frames.test.ts +12 -2
- package/test/links.test.ts +24 -0
- package/test/material.test.ts +15 -1
- package/test/maturity.test.ts +19 -1
- package/test/palette-icons.test.ts +2 -1
- package/test/pipe-panel.test.ts +33 -0
- package/test/refusals.test.ts +29 -0
- package/test/revision.test.ts +1 -1
- package/test/section-editor.test.ts +17 -1
- package/translations/en.json +86 -3
- package/translations/es.json +86 -3
- package/translations/ja.json +86 -3
- package/translations/ko.json +86 -3
- package/translations/ms.json +86 -3
- package/translations/zh.json +86 -3
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.246",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public",
|
|
6
6
|
"@things-factory:registry": "https://registry.npmjs.org"
|
|
@@ -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.12.
|
|
31
|
-
"@hatiolab/things-scene": "^10.1.
|
|
30
|
+
"@hatiolab/figure-model": "^0.12.2",
|
|
31
|
+
"@hatiolab/things-scene": "^10.1.109",
|
|
32
32
|
"@material/web": "^2.0.0",
|
|
33
33
|
"@operato/data-grist": "^10.29.43",
|
|
34
34
|
"@operato/graphql": "^10.29.43",
|
|
@@ -42,11 +42,11 @@
|
|
|
42
42
|
"@operato/slot-layout-editor": "^10.29.58",
|
|
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.246",
|
|
46
|
+
"@things-factory/figure-service": "^10.1.246",
|
|
47
|
+
"@things-factory/figure-viewer": "^10.1.246",
|
|
48
|
+
"@things-factory/setting-base": "^10.1.246",
|
|
49
49
|
"three": "^0.185.1"
|
|
50
50
|
},
|
|
51
|
-
"gitHead": "
|
|
51
|
+
"gitHead": "46c0d3c93edf765cc1f13040b1f9d03fd1d9e60b"
|
|
52
52
|
}
|
package/test/align.test.ts
CHANGED
|
@@ -9,9 +9,9 @@ import assert from 'node:assert/strict'
|
|
|
9
9
|
import { readFileSync } from 'node:fs'
|
|
10
10
|
import { fileURLToPath } from 'node:url'
|
|
11
11
|
|
|
12
|
-
import { addFigureBox, createFigureAsset, editFigurePart } from '@hatiolab/figure-model'
|
|
12
|
+
import { addFigureBox, applyFigureAuthoring, createFigureAsset, editFigurePart } from '@hatiolab/figure-model'
|
|
13
13
|
|
|
14
|
-
import { alignChanges, boxOf, canMirror, gridStepOf,
|
|
14
|
+
import { alignChanges, boxOf, canMirror, gridStepOf, mirrorEditOf, mirrorPlanOf, niceStep } from '../client/modeller/align.ts'
|
|
15
15
|
import { gizmoReachOf } from '../client/modeller/gizmo.ts'
|
|
16
16
|
import { revisionOf } from './revision-of.ts'
|
|
17
17
|
|
|
@@ -61,12 +61,18 @@ test('눈금은 1 · 2 · 5 간격으로 자리와 크기를 맞추고, 크기
|
|
|
61
61
|
assert.ok(['x', 'y', 'z'].every(a => snapped.designInputs[`body.size.${a}`]! >= step))
|
|
62
62
|
})
|
|
63
63
|
|
|
64
|
-
test('대칭은
|
|
64
|
+
test('대칭은 한 단계로 가운데 면 반대편에 복사하고, 붙인 부품에도 낸다 · 가운데에 있으면 내지 않는다', () => {
|
|
65
65
|
const asset = probe()
|
|
66
|
-
const
|
|
67
|
-
const mirrored = applied(copied, mirrorChanges(revisionOf(copied), 'body', 'body-copy', 'x'))
|
|
66
|
+
const mirrored = editFigurePart(asset, { kind: 'mirror-part', target: 'body', axis: 'x' })
|
|
68
67
|
assert.equal(Math.round(box(mirrored, 'body-copy')!.centre.x), -Math.round(box(mirrored, 'body')!.centre.x))
|
|
69
|
-
assert.equal(canMirror(revisionOf(probe({ x: 0, y: 50, z: 0 })), 'body', 'x'
|
|
68
|
+
assert.equal(canMirror(revisionOf(probe({ x: 0, y: 50, z: 0 })), 'body', 'x'), false)
|
|
69
|
+
/* A part fastened to a face: the gizmo does not move it, and its mirrored copy is still offered. */
|
|
70
|
+
let fastened = addFigureBox(probe({ x: 0, y: 50, z: 0 }), { id: 'arm', size: { x: 40, y: 40, z: 40 }, position: { x: 0, y: 0, z: 0 } })
|
|
71
|
+
fastened = applyFigureAuthoring(fastened, { kind: 'attach', part: 'arm', to: { part: 'body', face: 'right' } })
|
|
72
|
+
assert.equal(gizmoReachOf(fastened as never, 'arm').translate, false)
|
|
73
|
+
assert.equal(canMirror(revisionOf(fastened), 'arm', 'x'), true)
|
|
74
|
+
const copied = editFigurePart(fastened, { kind: 'mirror-part', target: 'arm', axis: 'x' })
|
|
75
|
+
assert.equal(Math.round(box(copied, 'arm-copy')!.centre.x), -Math.round(box(copied, 'arm')!.centre.x))
|
|
70
76
|
})
|
|
71
77
|
|
|
72
78
|
test('붙인 부품처럼 옮길 수 없는 것은 맞추기도 없다', () => {
|
|
@@ -109,3 +115,19 @@ test('no size drag is refused for a turn; what a size typed keeps is said, in ev
|
|
|
109
115
|
const ko = JSON.parse(read('../translations/ko.json'))
|
|
110
116
|
assert.deepEqual([ko['figure.text.size-keeps-centre'], ko['figure.text.size-keeps-fastening']], ['가운데 유지', '붙인 위치 유지'])
|
|
111
117
|
})
|
|
118
|
+
|
|
119
|
+
test('다리처럼 아래 붙은 것이 있으면 대칭 전에 묻는다 — 함께 가져갈 것, 이름 앞머리 짐작(X 는 좌우, Z 는 앞뒤), 움직임 (로봇개 2026-11-03)', () => {
|
|
120
|
+
let a = probe({ x: 0, y: 300, z: 0 })
|
|
121
|
+
for (const id of ['fl-hip', 'fl-shin']) a = addFigureBox(a, { id, size: { x: 40, y: 100, z: 40 }, position: { x: 0, y: 0, z: 0 } })
|
|
122
|
+
a = applyFigureAuthoring(a, { kind: 'attach', part: 'body', to: { plane: 'mounting-plane' } })
|
|
123
|
+
a = applyFigureAuthoring(a, { kind: 'attach', part: 'fl-hip', to: { part: 'body', face: 'bottom' }, align: { x: { mm: 60 } } })
|
|
124
|
+
a = applyFigureAuthoring(a, { kind: 'attach', part: 'fl-shin', to: { part: 'fl-hip', face: 'bottom' } })
|
|
125
|
+
const across = mirrorPlanOf(a, 'fl-hip', 'x')
|
|
126
|
+
assert.deepEqual([across.hanging, across.moves, across.asks], [['fl-shin'], false, true])
|
|
127
|
+
assert.deepEqual([across.form.from, across.form.to, across.form.chain], ['fl-', 'fr-', true], 'X 는 피규어의 왼쪽·오른쪽')
|
|
128
|
+
assert.deepEqual([mirrorPlanOf(a, 'fl-hip', 'z').form.to], ['bl-'], 'Z 는 앞·뒤')
|
|
129
|
+
assert.deepEqual(mirrorEditOf('fl-hip', across.form), { kind: 'mirror-part', target: 'fl-hip', axis: 'x', chain: true, prefix: { from: 'fl-', to: 'fr-' } })
|
|
130
|
+
assert.equal(mirrorPlanOf(a, 'fl-shin', 'x').asks, false, '아래 붙은 것도 움직임도 없으면 묻지 않는다')
|
|
131
|
+
const made = editFigurePart(a, mirrorEditOf('fl-hip', across.form))
|
|
132
|
+
assert.deepEqual(made.appearance.map(x => x.target).filter(t => t.startsWith('fr-')).sort(), ['fr-hip', 'fr-shin'])
|
|
133
|
+
})
|
|
@@ -8,7 +8,7 @@ import './more-open.js'
|
|
|
8
8
|
import './modeller-page.js'
|
|
9
9
|
import { i18next } from '@operato/i18n'
|
|
10
10
|
import { Raycaster, Vector2 } from 'three'
|
|
11
|
-
import { addFigureBox, addFigurePrimitive, applyFigureAuthoring, createFigureAsset, editFigurePart, serializeFigureAsset } from '@hatiolab/figure-model'
|
|
11
|
+
import { addFigureBox, addFigurePrimitive, applyFigureAuthoring, compileFigureAsset, createFigureAsset, editFigurePart, figureOccupiedBoxesOf, figureWritersOf, parseFigureAsset, serializeFigureAsset } from '@hatiolab/figure-model'
|
|
12
12
|
|
|
13
13
|
const PALETTE = {
|
|
14
14
|
'palette.dark': '#2b2b2b',
|
|
@@ -30,7 +30,20 @@ function box(placement, position, rotation) {
|
|
|
30
30
|
return serializeFigureAsset(applyFigureAuthoring(a, { kind: 'set-placement', placement }))
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/* A leg hung under the box, turned 40 degrees in the saved pose: its lowest corner, not the box, is what reaches the floor. */
|
|
34
|
+
function legged(a) {
|
|
35
|
+
a = addFigureBox(a, { id: 'leg', size: { x: 60, y: 500, z: 60 }, position: { x: 0, y: 0, z: 0 } })
|
|
36
|
+
a = applyFigureAuthoring(a, { kind: 'attach', part: 'leg', to: { part: 'box', face: 'bottom' } })
|
|
37
|
+
const j = a.joints.find(k => k.body1.part === 'leg')
|
|
38
|
+
a = applyFigureAuthoring(a, { kind: 'set-joint', joint: { ...j, motion: [{ type: 'revolute', axis: 'Z', lowerLimit: -60, upperLimit: 60 }] } })
|
|
39
|
+
return serializeFigureAsset({ ...a, stateDefaults: { ...a.stateDefaults, [j.id]: 40 } })
|
|
40
|
+
}
|
|
41
|
+
|
|
33
42
|
window.figures = {
|
|
43
|
+
/* A body standing by its position, a leg under it (robot dog 2026-11-03). */
|
|
44
|
+
legged: () => legged(parseFigureAsset(box('floor', { x: 0, y: 1500, z: 0 }))),
|
|
45
|
+
/* The same body fastened to the floor plane 1200 mm up. */
|
|
46
|
+
'legged-on-plane': () => legged(applyFigureAuthoring(parseFigureAsset(box('floor', { x: 0, y: 0, z: 0 })), { kind: 'attach', part: 'box', to: { plane: 'mounting-plane' }, gap: 1200 })),
|
|
34
47
|
/* 500 mm above the floor. */
|
|
35
48
|
raised: () => box('floor', { x: 0, y: 700, z: 0 }),
|
|
36
49
|
/* Turned 45 degrees about Z, off the floor: it meets the floor by an edge. */
|
|
@@ -44,6 +57,8 @@ window.figures = {
|
|
|
44
57
|
a = addFigurePrimitive(a, { id: 'drum', kind: 'cylinder', dimensions: { radius: 200, length: 500 }, position: { x: 400, y: 250, z: 0 } })
|
|
45
58
|
return serializeFigureAsset(a)
|
|
46
59
|
},
|
|
60
|
+
/* The doors, the right one's clip in a motion group: a clip of a group can be copied. */
|
|
61
|
+
'doors-grouped': () => serializeFigureAsset(editFigurePart(parseFigureAsset(window.figures.doors()), { kind: 'set-motion-group', group: { id: 'motion', label: 'swing', clips: ['right-swing'], defaultClip: 'right-swing', transitionSeconds: 0.2 } })),
|
|
47
62
|
/* Two doors on a frame, each swinging on a clip of its own. */
|
|
48
63
|
doors: () => {
|
|
49
64
|
let a = figure('Doors')
|
|
@@ -168,6 +183,11 @@ window.editing = {
|
|
|
168
183
|
told() {
|
|
169
184
|
return told.splice(0).map(n => ({ level: n.level, message: n.message }))
|
|
170
185
|
},
|
|
186
|
+
/** The lowest point of every part, in the figure's saved pose, as the session holds it. */
|
|
187
|
+
lowest() {
|
|
188
|
+
const a = parseFigureAsset(editor().session.source())
|
|
189
|
+
return Math.round(Math.min(...figureOccupiedBoxesOf(compileFigureAsset(a).evaluate().geometry, figureWritersOf(a)).map(b => b.centre.y - b.extent.y / 2)))
|
|
190
|
+
},
|
|
171
191
|
/** A part's material as the session holds it. */
|
|
172
192
|
material(id) {
|
|
173
193
|
return editor().session.read().geometry.find(g => g.id === id)?.material ?? null
|
|
@@ -264,6 +264,23 @@ describe('put on the plane by the figure\'s placement: once, not fastened (desig
|
|
|
264
264
|
})
|
|
265
265
|
})
|
|
266
266
|
|
|
267
|
+
describe('put down as it stands: a body by what hangs from it (robot dog 2026-11-03)', () => {
|
|
268
|
+
for (const [figure, how] of [['legged', 'its position'], ['legged-on-plane', 'its gap to the plane']])
|
|
269
|
+
test(`${figure}: the leg's lowest point reaches the floor in one step, by ${how}`, async () => {
|
|
270
|
+
await mount('box', figure)
|
|
271
|
+
await pick('box')
|
|
272
|
+
const tool = await read(() => editing.at('button[plane-align]'))
|
|
273
|
+
assert.ok(tool, 'the tool is offered')
|
|
274
|
+
assert.equal(tool.title, 'figure.text.align-to-floor-hanging')
|
|
275
|
+
assert.ok((await read(() => editing.lowest())) > 100, 'it starts off the floor')
|
|
276
|
+
const start = await revision()
|
|
277
|
+
await click(tool)
|
|
278
|
+
assert.equal(await revision(), start + 1, 'one step')
|
|
279
|
+
assert.equal(await read(() => editing.lowest()), 0)
|
|
280
|
+
assert.ok((await bottomOf('box')) > 300, 'the body stands on its leg, not on the floor')
|
|
281
|
+
})
|
|
282
|
+
})
|
|
283
|
+
|
|
267
284
|
describe('the material: colours by purpose, one step each, a glow drawn while dragged, segments for round bodies only', () => {
|
|
268
285
|
before(() => mount('box', 'shapes'))
|
|
269
286
|
|
|
@@ -286,6 +303,22 @@ describe('the material: colours by purpose, one step each, a glow drawn while dr
|
|
|
286
303
|
assert.equal((await read(() => editing.material('bin'))).token, 'palette.accent')
|
|
287
304
|
})
|
|
288
305
|
|
|
306
|
+
test('a colour of its own, picked by its value, is one step: the part takes it and the palette carries it (robot dog, 2026-11-03)', async () => {
|
|
307
|
+
const start = await revision()
|
|
308
|
+
const found = await read(() => {
|
|
309
|
+
const panel = [...document.querySelectorAll('*')].flatMap(function all(e) { return e.shadowRoot ? [e, ...[...e.shadowRoot.querySelectorAll('*')].flatMap(all)] : [e] }).find(e => e.localName === 'figure-material-panel' && e.shadowRoot?.querySelector('label[own-colour] input'))
|
|
310
|
+
const input = panel?.shadowRoot.querySelector('label[own-colour] input')
|
|
311
|
+
if (!input) return false
|
|
312
|
+
input.value = '#ffd400'
|
|
313
|
+
input.dispatchEvent(new Event('change', { bubbles: true }))
|
|
314
|
+
return true
|
|
315
|
+
})
|
|
316
|
+
assert.ok(found, 'the own colour field beside the swatches')
|
|
317
|
+
await until(async () => (await revision()) === start + 1, 'one step')
|
|
318
|
+
assert.equal((await read(() => editing.material('bin'))).token, 'colour.ffd400')
|
|
319
|
+
assert.equal(await read(() => probe.source().palette['colour.ffd400']), '#ffd400')
|
|
320
|
+
})
|
|
321
|
+
|
|
289
322
|
test('the glow is drawn while its slider is held, nothing written; the release writes it once', async () => {
|
|
290
323
|
const start = await revision()
|
|
291
324
|
const thumb = await read(() => editing.range('figure-material-panel'))
|
|
@@ -477,6 +510,42 @@ describe('a clip scrubbed and keyed (2026-09-24)', () => {
|
|
|
477
510
|
assert.deepEqual(Object.fromEntries((await read(() => editing.clips())).map(c => [c.clip, c.keys])), { 'left-swing': 3, 'right-swing': 2 })
|
|
478
511
|
})
|
|
479
512
|
|
|
513
|
+
/* A field of the clip panel by its label: the moment, or a key's time or value (proposal 18, 2026-11-03). */
|
|
514
|
+
const typeInClips = async (label, text) => {
|
|
515
|
+
const field = await page().evaluateHandle(label => {
|
|
516
|
+
const deep = (root = document) => [...root.querySelectorAll('input')].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
|
|
517
|
+
return deep().find(i => i.type === 'number' && i.getAttribute('aria-label') === label && i.getRootNode().host?.localName === 'figure-clips') ?? null
|
|
518
|
+
}, label)
|
|
519
|
+
assert.ok(await field.evaluate(i => !!i), `${label} in the clips`)
|
|
520
|
+
await field.evaluate(i => i.scrollIntoView({ block: 'center' }))
|
|
521
|
+
await field.click({ clickCount: 3 })
|
|
522
|
+
await page().keyboard.type(text)
|
|
523
|
+
await page().keyboard.press('Tab')
|
|
524
|
+
return field
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
test('a moment typed is posed as the bar would pose it, not on the bar\'s hundredths', async () => {
|
|
528
|
+
const start = await revision()
|
|
529
|
+
await typeInClips('figure.label.clip-time', '1.37')
|
|
530
|
+
await until(async () => (await revision()) === start + 1, 'one step')
|
|
531
|
+
/* The first clip on the panel is the right door's: 80 degrees over 2 seconds, so 1.37 s is 54.8. */
|
|
532
|
+
const value = await read(() => probe.value('right-open'))
|
|
533
|
+
assert.ok(Math.abs(value - 40 * 1.37) < 1e-6, `the pose at 1.37 s: ${value}`)
|
|
534
|
+
})
|
|
535
|
+
|
|
536
|
+
test('a key\'s value typed in its chip is one step of the track; the first key\'s time stays at 0', async () => {
|
|
537
|
+
const start = await revision()
|
|
538
|
+
await typeInClips('figure.label.key-value 2', '55')
|
|
539
|
+
await until(async () => (await revision()) === start + 1, 'one step')
|
|
540
|
+
/* The first track's second key: whichever clip the panel lists first. */
|
|
541
|
+
const keys = await read(() => JSON.parse(document.querySelector('figure-editor').session.source()).drivers.flatMap(d => d.keys))
|
|
542
|
+
assert.ok(keys.some(k => k.value === 55), `a key at 55: ${JSON.stringify(keys)}`)
|
|
543
|
+
assert.equal(await read(() => {
|
|
544
|
+
const deep = (root = document) => [...root.querySelectorAll('input')].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
|
|
545
|
+
return deep().find(i => i.getAttribute('aria-label') === 'figure.label.key-seconds 1')?.readOnly
|
|
546
|
+
}), true)
|
|
547
|
+
})
|
|
548
|
+
|
|
480
549
|
test('a track\'s name stands on its own line, the repeat choice and a small remove button under it', async () => {
|
|
481
550
|
const head = await read(() => editing.trackHead())
|
|
482
551
|
assert.ok(head, 'a track')
|
|
@@ -487,6 +556,61 @@ describe('a clip scrubbed and keyed (2026-09-24)', () => {
|
|
|
487
556
|
})
|
|
488
557
|
})
|
|
489
558
|
|
|
559
|
+
describe('a track moved round its loop, a clip copied (robot dog 2026-11-03)', () => {
|
|
560
|
+
before(async () => {
|
|
561
|
+
await mount('box', 'doors-grouped')
|
|
562
|
+
await click(await read(() => probe.tab('figure.label.tab-short-motion')))
|
|
563
|
+
})
|
|
564
|
+
const drivers = () => read(() => JSON.parse(document.querySelector('figure-editor').session.source()).drivers)
|
|
565
|
+
const inClips = selector => read(selector => {
|
|
566
|
+
const deep = (root = document) => [...root.querySelectorAll(selector)].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
|
|
567
|
+
return deep().filter(e => e.getRootNode().host?.localName === 'figure-clips').length
|
|
568
|
+
}, selector)
|
|
569
|
+
const pressIn = async (selector, index = 0) => {
|
|
570
|
+
const button = await page().evaluateHandle(({ selector, index }) => {
|
|
571
|
+
const deep = (root = document) => [...root.querySelectorAll(selector)].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
|
|
572
|
+
return deep().filter(e => e.getRootNode().host?.localName === 'figure-clips')[index] ?? null
|
|
573
|
+
}, { selector, index })
|
|
574
|
+
assert.ok(await button.evaluate(b => !!b), selector)
|
|
575
|
+
await button.evaluate(b => b.scrollIntoView({ block: 'center' }))
|
|
576
|
+
await button.click()
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
test('a clip in a group is copied in one step; one in none is not offered', async () => {
|
|
580
|
+
/* The left door's clip is in no group, the right one's is in one. */
|
|
581
|
+
assert.equal(await read(() => {
|
|
582
|
+
const deep = (root = document) => [...root.querySelectorAll('button[copy-clip]')].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
|
|
583
|
+
return deep().map(b => b.disabled).join()
|
|
584
|
+
}), 'true,false')
|
|
585
|
+
const start = await revision()
|
|
586
|
+
await pressIn('button[copy-clip]', 1)
|
|
587
|
+
await until(async () => (await revision()) === start + 1, 'one step')
|
|
588
|
+
const copy = (await drivers()).find(d => d.clip === 'right-swing-copy')
|
|
589
|
+
assert.deepEqual(copy?.keys, (await drivers()).find(d => d.clip === 'right-swing').keys)
|
|
590
|
+
assert.ok(await read(() => JSON.parse(document.querySelector('figure-editor').session.source()).motionGroups[0].clips.includes('right-swing-copy')), 'in the group')
|
|
591
|
+
})
|
|
592
|
+
|
|
593
|
+
test('a track moved one second round its two-second loop starts where it stood at one second', async () => {
|
|
594
|
+
const start = await revision()
|
|
595
|
+
await pressIn('button[copy-track]')
|
|
596
|
+
assert.equal(await inClips('div[copy-form]'), 1, 'the form under the track')
|
|
597
|
+
const later = await page().evaluateHandle(() => {
|
|
598
|
+
const deep = (root = document) => [...root.querySelectorAll('input')].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
|
|
599
|
+
return deep().find(i => i.getAttribute('aria-label') === 'figure.label.copy-track-later')
|
|
600
|
+
})
|
|
601
|
+
await later.click({ clickCount: 3 })
|
|
602
|
+
await page().keyboard.type('1')
|
|
603
|
+
await page().keyboard.press('Tab')
|
|
604
|
+
await pressIn('div[copy-form] button[act]')
|
|
605
|
+
await until(async () => (await revision()) === start + 1, 'one step')
|
|
606
|
+
const moved = (await drivers()).find(d => d.clip === 'left-swing' && d.state === 'left-open')
|
|
607
|
+
/* 0 to 80 over 2 s: a second later it starts at 40, reaches 80 at 1 s, leaps back to 0 there. */
|
|
608
|
+
assert.equal(moved.keys[0].value, 40)
|
|
609
|
+
assert.ok(moved.keys.some(k => Math.abs(k.at - 0.5) < 1e-6 && k.value === 0), JSON.stringify(moved.keys))
|
|
610
|
+
assert.equal(await inClips('div[copy-form]'), 0, 'the form closes')
|
|
611
|
+
})
|
|
612
|
+
})
|
|
613
|
+
|
|
490
614
|
describe('an edit that would stop a clip is asked first (designer 2026-09-27)', () => {
|
|
491
615
|
before(() => mount('box', 'doors'))
|
|
492
616
|
|
|
@@ -174,12 +174,17 @@ async function type(field, text) {
|
|
|
174
174
|
if (now === last) break
|
|
175
175
|
last = now
|
|
176
176
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
177
|
+
/* Under the whole suite's load the window can still be settling after that: a press that missed is made again where it stands now. */
|
|
178
|
+
let focused = false
|
|
179
|
+
for (let tries = 0; tries < 5 && !focused; tries++) {
|
|
180
|
+
if (tries) await sleep(200)
|
|
181
|
+
await field.click({ clickCount: 3 })
|
|
182
|
+
focused = await field.evaluate(i => {
|
|
183
|
+
let at = document.activeElement
|
|
184
|
+
while (at?.shadowRoot?.activeElement) at = at.shadowRoot.activeElement
|
|
185
|
+
return at === i
|
|
186
|
+
})
|
|
187
|
+
}
|
|
183
188
|
assert.ok(focused, 'the press lands in the field')
|
|
184
189
|
await page().keyboard.type(text)
|
|
185
190
|
await page().keyboard.press('Tab')
|
|
@@ -340,8 +345,15 @@ describe('a row\'s pitch', () => {
|
|
|
340
345
|
await pick('roller')
|
|
341
346
|
await tab('response')
|
|
342
347
|
const start = await state()
|
|
343
|
-
|
|
344
|
-
await
|
|
348
|
+
const field = await handle('input[aria-label="figure.label.repeat-gap"]')
|
|
349
|
+
await type(field, '300')
|
|
350
|
+
/* Unsteady under the whole suite's load once in a while (2026-11-01, 11-03): said with what the field and the editor hold when it does. */
|
|
351
|
+
try {
|
|
352
|
+
await oneStep(start)
|
|
353
|
+
} catch (error) {
|
|
354
|
+
const seen = { field: await field.evaluate(i => ({ value: i.value, connected: i.isConnected })), state: await state() }
|
|
355
|
+
throw new Error(`${error.message}: ${JSON.stringify(seen)}`)
|
|
356
|
+
}
|
|
345
357
|
const now = await source()
|
|
346
358
|
assert.equal(now.designInputs.pitch, 300, 'the input is 300')
|
|
347
359
|
assert.deepEqual(now.joints.find(j => j.body1.part === 'roller').repeat.pitch, { from: { of: 'input', input: 'pitch' } }, 'the row still reads it')
|
|
@@ -434,6 +446,35 @@ describe('the fastening window on the robot arm', () => {
|
|
|
434
446
|
})
|
|
435
447
|
})
|
|
436
448
|
|
|
449
|
+
describe('the drawing of a turned target shows its faces where they are drawn (robot dog, 2026-11-03)', () => {
|
|
450
|
+
before(async () => {
|
|
451
|
+
let a = addFigurePrimitive(createFigureAsset('Laid'), { id: 'drum', kind: 'cylinder', dimensions: { radius: 100, length: 400 }, position: { x: 0, y: 500, z: 0 }, rotation: { x: 0, y: 0, z: 90 } })
|
|
452
|
+
a = addFigureBox(a, { id: 'cap', size: { x: 40, y: 40, z: 40 }, position: { x: 500, y: 500, z: 0 } })
|
|
453
|
+
await mount('laid-drum', a)
|
|
454
|
+
})
|
|
455
|
+
|
|
456
|
+
test('a cylinder laid on its side: its end faces stand left and right, and the top drawn is no face of it', async () => {
|
|
457
|
+
await pick('cap')
|
|
458
|
+
await press('button[fasten-start]')
|
|
459
|
+
await until(() => probe.fastenTargets(), [], 'the fastening window')
|
|
460
|
+
await aimAt('drum')
|
|
461
|
+
const placed = () =>
|
|
462
|
+
read(() => {
|
|
463
|
+
const deep = (root = document) => [...root.querySelectorAll('[place]')].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
|
|
464
|
+
return Object.fromEntries(deep().map(e => [e.getAttribute('place'), { face: e.getAttribute('face'), pickable: (e.getAttribute('style') ?? '').includes('cursor:pointer') }]))
|
|
465
|
+
})
|
|
466
|
+
await until(() => {
|
|
467
|
+
const deep = (root = document) => [...root.querySelectorAll('[place]')].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
|
|
468
|
+
return deep().length === 6
|
|
469
|
+
}, [], 'the six places drawn')
|
|
470
|
+
const drawn = await placed()
|
|
471
|
+
assert.deepEqual([drawn.left.face, drawn.right.face], ['top', 'bottom'], 'its ends, where they are seen')
|
|
472
|
+
assert.equal(drawn.right.pickable, true)
|
|
473
|
+
assert.equal(drawn.top.pickable, false, 'the round side drawn on top is no face to fasten to')
|
|
474
|
+
await page().keyboard.press('Escape')
|
|
475
|
+
})
|
|
476
|
+
})
|
|
477
|
+
|
|
437
478
|
describe('a new fastening for a door leaf: chosen in the window, seen before it is done', () => {
|
|
438
479
|
before(async () => {
|
|
439
480
|
await mount('door')
|
|
@@ -612,7 +612,11 @@ describe('a motion is changed where it is (2026-09-25)', () => {
|
|
|
612
612
|
await click(await button('figure.button.change-motion'))
|
|
613
613
|
const max = await page().evaluateHandle(() => {
|
|
614
614
|
const deep = (root = document) => [...root.querySelectorAll('input[type=number]')].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
|
|
615
|
-
|
|
615
|
+
/*
|
|
616
|
+
The motion form's range: the pair under its range label in degrees. The size-by-state form below it has a range
|
|
617
|
+
of its own (in percent, 2026-10-27), which the last pair on the panel had become.
|
|
618
|
+
*/
|
|
619
|
+
const inputs = deep().filter(i => i.closest('div[pair]') && i.getRootNode().host?.tagName === 'FIGURE-LINKS' && i.closest('div[pair]').previousElementSibling?.textContent.includes('figure.label.control-range (deg)'))
|
|
616
620
|
return inputs[inputs.length - 1] ?? null
|
|
617
621
|
})
|
|
618
622
|
assert.ok(await max.evaluate(i => !!i), 'the form is open with the range')
|
|
@@ -685,6 +689,28 @@ describe('a motion is changed where it is (2026-09-25)', () => {
|
|
|
685
689
|
assert.deepEqual(now.motion, [was.motion[0]], 'the first axis alone, as it was')
|
|
686
690
|
assert.equal(await rows(), 1)
|
|
687
691
|
})
|
|
692
|
+
|
|
693
|
+
test('the point a turn passes is typed in the motion form: one step, the axis\'s through, the part not moved at rest (robot dog, 2026-11-03)', async () => {
|
|
694
|
+
const rest = await read(() => probe.body('upper-arm'))
|
|
695
|
+
await click(await button('figure.button.change-motion'), 'change the motion')
|
|
696
|
+
const x = await page().evaluateHandle(() => {
|
|
697
|
+
const deep = (root = document) => [...root.querySelectorAll('input')].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
|
|
698
|
+
return deep().find(i => i.getAttribute('aria-label') === 'figure.label.motion-through X') ?? null
|
|
699
|
+
})
|
|
700
|
+
assert.ok(await x.evaluate(i => !!i), 'the through cells under a turn')
|
|
701
|
+
await x.evaluate(i => i.scrollIntoView({ block: 'center' }))
|
|
702
|
+
await x.click({ clickCount: 3 })
|
|
703
|
+
await page().keyboard.type('50')
|
|
704
|
+
await page().keyboard.press('Tab')
|
|
705
|
+
const start = await state()
|
|
706
|
+
await click(await button('figure.button.apply-motion-change'), 'apply')
|
|
707
|
+
const after = await state()
|
|
708
|
+
assert.equal(after.error, '', 'not refused')
|
|
709
|
+
assert.equal(after.revision, start.revision + 1, 'one step')
|
|
710
|
+
assert.deepEqual((await jointOf('upper-arm')).motion[0].through, { x: 50 })
|
|
711
|
+
const moved = await read(() => probe.body('upper-arm'))
|
|
712
|
+
assert.deepEqual(moved.t.map(Math.round), rest.t.map(Math.round), 'at its pose now, the part where it was')
|
|
713
|
+
})
|
|
688
714
|
})
|
|
689
715
|
|
|
690
716
|
describe('the modeller made easier to find and to keep (2026-09-26 baseline)', () => {
|
|
@@ -14,6 +14,7 @@ import { addFigureBox, applyFigureAuthoring, createFigureAsset, serializeFigureA
|
|
|
14
14
|
import { buildPage } from './build.mjs'
|
|
15
15
|
import { openPage } from './serve.mjs'
|
|
16
16
|
|
|
17
|
+
const probeJoint = (source, part) => source.joints.find(j => j.body1.part === part).body0
|
|
17
18
|
const sleep = ms => new Promise(done => setTimeout(done, ms))
|
|
18
19
|
|
|
19
20
|
let opened
|
|
@@ -55,7 +56,14 @@ function rowed() {
|
|
|
55
56
|
a = applyFigureAuthoring(a, { kind: 'attach', part: 'box-2', to: { part: 'box-1', face: 'top' } })
|
|
56
57
|
a = applyFigureAuthoring(a, { kind: 'make-repeat', part: 'box-2', axis: 'x', pitch: 300 })
|
|
57
58
|
/* A part off to one side, free: its mirrored copy lands on the other side. */
|
|
58
|
-
|
|
59
|
+
a = addFigureBox(a, { id: 'side', size: { x: 100, y: 100, z: 100 }, position: { x: 900, y: 50, z: 0 } })
|
|
60
|
+
/* A part fastened to the floor box's right face: no gizmo moves it, and it is mirrored to the left face (robot dog, 2026-11-03). */
|
|
61
|
+
a = addFigureBox(a, { id: 'ear', size: { x: 60, y: 60, z: 60 }, position: { x: 0, y: 0, z: 0 } })
|
|
62
|
+
a = applyFigureAuthoring(a, { kind: 'attach', part: 'ear', to: { part: 'box-1', face: 'right' }, align: { z: { mm: 100 } } })
|
|
63
|
+
/* A leg on the floor box's front, a shin under its hip: mirrored whole, named fr- (robot dog, 2026-11-03). */
|
|
64
|
+
for (const id of ['fl-hip', 'fl-shin']) a = addFigureBox(a, { id, size: { x: 40, y: 40, z: 40 }, position: { x: 0, y: 0, z: 0 } })
|
|
65
|
+
a = applyFigureAuthoring(a, { kind: 'attach', part: 'fl-hip', to: { part: 'box-1', face: 'front' }, align: { x: { mm: 300 } } })
|
|
66
|
+
return applyFigureAuthoring(a, { kind: 'attach', part: 'fl-shin', to: { part: 'fl-hip', face: 'bottom' } })
|
|
59
67
|
}
|
|
60
68
|
|
|
61
69
|
describe('a new part on a figure with a row takes an id no part holds', () => {
|
|
@@ -72,8 +80,8 @@ describe('a new part on a figure with a row takes an id no part holds', () => {
|
|
|
72
80
|
const after = await state()
|
|
73
81
|
assert.equal(after.error, '')
|
|
74
82
|
const now = await parts()
|
|
75
|
-
assert.equal(now.length,
|
|
76
|
-
const added = now.find(id => !['box-1', 'box-2', 'side'].includes(id))
|
|
83
|
+
assert.equal(now.length, 7)
|
|
84
|
+
const added = now.find(id => !['box-1', 'box-2', 'side', 'ear', 'fl-hip', 'fl-shin'].includes(id))
|
|
77
85
|
assert.ok(added, now.join())
|
|
78
86
|
assert.equal(after.selected, added)
|
|
79
87
|
})
|
|
@@ -101,4 +109,53 @@ describe('a new part on a figure with a row takes an id no part holds', () => {
|
|
|
101
109
|
assert.equal((await state()).selected, copy)
|
|
102
110
|
assert.equal(Math.round((await read(id => probe.body(id), copy)).t[0]), -900, 'on the other side')
|
|
103
111
|
})
|
|
112
|
+
|
|
113
|
+
test('a fastened part mirrored: one step, fastened to the opposite face, no refusal', async () => {
|
|
114
|
+
const at = await read(() => probe.row('ear'))
|
|
115
|
+
await page().mouse.click(at.x, at.y)
|
|
116
|
+
await until(() => probe.state().selected === 'ear', [], 'ear selected')
|
|
117
|
+
const start = await state()
|
|
118
|
+
const was = await read(() => probe.body('ear'))
|
|
119
|
+
await click('button', { text: 'figure.button.mirror-x' }, 'mirror across X')
|
|
120
|
+
await until(r => probe.state().revision === r + 1, [start.revision], 'one step')
|
|
121
|
+
const after = await state()
|
|
122
|
+
assert.equal(after.error, '')
|
|
123
|
+
assert.equal(after.selected, 'ear-copy')
|
|
124
|
+
const copy = await read(() => probe.body('ear-copy'))
|
|
125
|
+
assert.equal(Math.round(copy.t[0]), -Math.round(was.t[0]), 'on the other side')
|
|
126
|
+
assert.equal(Math.round(copy.t[2]), Math.round(was.t[2]), 'as far along the face')
|
|
127
|
+
assert.deepEqual(probeJoint(await read(() => probe.source()), 'ear-copy'), { part: 'box-1', face: 'left' })
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
test('the unlimited level is offered with no part limit; chosen, one step, saved, and the list counts no limit (user 2026-11-03)', async () => {
|
|
131
|
+
const offered = await read(() => [...findDeep('select#detail-level').options].map(o => [o.value, o.textContent.replace(/\s+/g, ' ').trim()]))
|
|
132
|
+
assert.deepEqual(offered.find(([v]) => v === 'UNLIMITED'), ['UNLIMITED', 'figure.label.detail-UNLIMITED · figure.text.no-part-limit'])
|
|
133
|
+
const start = await state()
|
|
134
|
+
await read(() => {
|
|
135
|
+
const select = findDeep('select#detail-level')
|
|
136
|
+
select.value = 'UNLIMITED'
|
|
137
|
+
select.dispatchEvent(new Event('change', { bubbles: true }))
|
|
138
|
+
})
|
|
139
|
+
await until(r => probe.state().revision === r + 1, [start.revision], 'one step')
|
|
140
|
+
assert.equal((await read(() => probe.source())).detailLevel, 'UNLIMITED')
|
|
141
|
+
assert.equal(await read(() => findDeep('span[room]')?.textContent.trim()), String((await parts()).length), 'the parts counted, no limit beside them')
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
test('a leg mirrored from its hip: asked first, the shin with it, named fr-, one step', async () => {
|
|
145
|
+
const at = await read(() => probe.row('fl-hip'))
|
|
146
|
+
await page().mouse.click(at.x, at.y)
|
|
147
|
+
await until(() => probe.state().selected === 'fl-hip', [], 'fl-hip selected')
|
|
148
|
+
const start = await state()
|
|
149
|
+
await click('button', { text: 'figure.button.mirror-x' }, 'mirror across X')
|
|
150
|
+
await until(() => !!findDeep('div[mirror-form]'), [], 'the mirror asked about')
|
|
151
|
+
assert.equal((await state()).revision, start.revision, 'nothing made yet')
|
|
152
|
+
assert.deepEqual(await read(() => [...findDeep('div[mirror-form]').querySelectorAll('input')].filter(i => i.type !== 'checkbox').map(i => i.value)), ['fl-', 'fr-'], 'the names guessed')
|
|
153
|
+
await click('div[mirror-form] button', { text: 'figure.button.mirror-x' }, 'the form\'s mirror')
|
|
154
|
+
await until(r => probe.state().revision === r + 1, [start.revision], 'one step')
|
|
155
|
+
const after = await state()
|
|
156
|
+
assert.equal(after.error, '')
|
|
157
|
+
assert.equal(after.selected, 'fr-hip')
|
|
158
|
+
assert.deepEqual(probeJoint(await read(() => probe.source()), 'fr-shin'), { part: 'fr-hip', face: 'bottom' }, 'the shin under the copy')
|
|
159
|
+
assert.equal(Math.round((await read(() => probe.body('fr-hip'))).t[0]), -Math.round((await read(() => probe.body('fl-hip'))).t[0]))
|
|
160
|
+
})
|
|
104
161
|
})
|
|
@@ -320,11 +320,11 @@ describe('the list narrowed: the selected row keeps its name and its buttons in
|
|
|
320
320
|
})
|
|
321
321
|
|
|
322
322
|
describe('the palette: the starting shapes, locked at the detail level\'s limit', () => {
|
|
323
|
-
test('
|
|
323
|
+
test('ten starting shapes, in order -- the pipe (ADR-0100 §16.2) among them -- then a mesh brought from a file; a rounded box or a drum is made from the box', async () => {
|
|
324
324
|
await mount('box', 'three')
|
|
325
325
|
assert.deepEqual(
|
|
326
326
|
(await read(() => parts.palette())).map(b => b.label),
|
|
327
|
-
['cube', 'cylinder', 'sphere', 'frustum', 'polygon', 'pyramid', 'plate', 'wall', 'portal'].map(k => `figure.label.primitive-${k}`).concat('figure.button.import-mesh')
|
|
327
|
+
['cube', 'cylinder', 'sphere', 'frustum', 'polygon', 'pyramid', 'pipe', 'plate', 'wall', 'portal'].map(k => `figure.label.primitive-${k}`).concat('figure.button.import-mesh')
|
|
328
328
|
)
|
|
329
329
|
})
|
|
330
330
|
|
|
@@ -241,24 +241,30 @@ describe('places made by a rule: the shared slot layout editor on a part\'s face
|
|
|
241
241
|
assert.equal(await has('button[data-action="use-default"]'), false, 'no default: the figure\'s layout is the only one')
|
|
242
242
|
})
|
|
243
243
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
244
|
+
/*
|
|
245
|
+
The editor of 2026-10-26: with no layout yet the face is one slot at once, sized by the face and the room over it, put
|
|
246
|
+
in on Apply; a slot that names no type takes any item (ADR-0051 decisions 5 and 7, 2026-10-27). The preset's typed
|
|
247
|
+
size and the held-back slot that names no type this check once pressed are gone.
|
|
248
|
+
*/
|
|
249
|
+
test('with no layout yet, the fork\'s top is one slot at once, sized by the face; nothing written until Apply', async () => {
|
|
250
|
+
const start = await state()
|
|
251
|
+
await until(() => !!findDeep('button[data-action="apply"]') && !findDeep('button[data-action="apply"]').disabled, [], 'Apply offered for the one slot')
|
|
252
|
+
assert.equal((await state()).revision, start.revision, 'nothing written yet')
|
|
253
|
+
assert.equal(await has('*', { within: 'ox-slot-layout-editor', has: 'figure.text.slot-accepts-required' }), false, 'a slot that names no type takes any item')
|
|
250
254
|
})
|
|
251
255
|
|
|
252
256
|
test('the slot names a type; Apply is one step, the layout the figure holds, its slots drawn on the fork', async () => {
|
|
253
257
|
const start = await state()
|
|
254
|
-
await
|
|
258
|
+
await click('[data-field="accepts-mode"] button[data-value="LIST"]', {}, 'listed types only')
|
|
255
259
|
await type('input[data-field="accepts"]', {}, 'PALLET')
|
|
256
260
|
await click('button[data-action="apply"]')
|
|
257
261
|
await until(r => probe.state().revision === r + 1, [start.revision], 'the layout written')
|
|
258
262
|
const held = (await source()).document.capabilities.slotLayout
|
|
259
263
|
assert.equal(held.role, 'seat')
|
|
260
264
|
assert.deepEqual(held.anchors.map(a => [a.id, a.frame]), [['fork', 'fork.face.top.frame']], 'on the fork\'s top')
|
|
261
|
-
|
|
265
|
+
const slot = held.layout.tiers[0].layout.slot
|
|
266
|
+
assert.deepEqual([slot.allowed.width, slot.allowed.depth, slot.accepts], [1200, 900, ['PALLET']], 'the face\'s size, the type named')
|
|
267
|
+
assert.ok(slot.allowed.height > 0, 'a height from the room over the face')
|
|
262
268
|
assert.equal(held.layout.preset.key, 'single')
|
|
263
269
|
await until(() => look.drawn().includes('__slot:t1_s1'), [], 'the slot drawn')
|
|
264
270
|
})
|