@things-factory/figure-ui 10.1.245 → 10.1.247
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/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/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/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
|
@@ -651,6 +651,49 @@ describe('the preview: registered as a board registers it, posed and played, the
|
|
|
651
651
|
assert.ok(given.every(v => v === now.value), `each given the speed shown: ${given}`)
|
|
652
652
|
})
|
|
653
653
|
|
|
654
|
+
test('an angle slides by a degree, and a value typed is given as typed (robot dog, 2026-11-03)', async () => {
|
|
655
|
+
const step = await read(() => screenCheck.previewControl('j2-shoulder').querySelector('input[type=range]').step)
|
|
656
|
+
assert.equal(step, '1', 'a degree, not a hundredth of the range')
|
|
657
|
+
const typed = await element('the shoulder typed', () => screenCheck.previewControl('j2-shoulder').querySelector('input[typed]'))
|
|
658
|
+
await typed.click({ clickCount: 3 })
|
|
659
|
+
await page().keyboard.type('37')
|
|
660
|
+
await page().keyboard.press('Enter')
|
|
661
|
+
await until('given', () => screenCheck.deep('figure-preview')[0].scene.root.components.every(c => c.state.parameters?.['j2-shoulder'] === 37))
|
|
662
|
+
})
|
|
663
|
+
|
|
664
|
+
test('a clip played lets go of the value tried on the control it drives; the default pose lets go of every one', async () => {
|
|
665
|
+
const given = () => read(() => screenCheck.deep('figure-preview')[0].scene.root.components.map(c => ({ ...(c.state.parameters ?? {}) })))
|
|
666
|
+
const base = await element('the base', () => screenCheck.previewControl('j1-base').querySelector('input[type=range]'))
|
|
667
|
+
await base.focus()
|
|
668
|
+
for (let i = 0; i < 5; i++) await page().keyboard.press('ArrowRight')
|
|
669
|
+
await until('tried', () => screenCheck.deep('figure-preview')[0].scene.root.components.every(c => c.state.parameters?.['j1-base'] !== undefined))
|
|
670
|
+
/* The cycle drives j1-base; it is played (its speed above 0 since the test before). */
|
|
671
|
+
const speed = await element('the clip\'s speed', () => screenCheck.previewControl('cycle').querySelector('input[type=range]'))
|
|
672
|
+
await speed.focus()
|
|
673
|
+
await page().keyboard.press('ArrowRight')
|
|
674
|
+
await until('let go', () => screenCheck.deep('figure-preview')[0].scene.root.components.every(c => c.state.parameters?.['j1-base'] === undefined))
|
|
675
|
+
assert.ok((await given()).every(p => p['j2-shoulder'] === 37), 'a control the clip does not drive keeps its value')
|
|
676
|
+
await press('back to the default pose', () => screenCheck.deep('figure-preview')[0].shadowRoot.querySelector('button[clear-tried]'))
|
|
677
|
+
await until('all let go', () => screenCheck.deep('figure-preview')[0].scene.root.components.every(c => Object.keys(c.state.parameters ?? {}).length === 0))
|
|
678
|
+
assert.equal(await read(() => !!screenCheck.deep('figure-preview')[0].shadowRoot.querySelector('button[clear-tried]')), false, 'nothing left to clear')
|
|
679
|
+
})
|
|
680
|
+
|
|
681
|
+
test('the preview looks from the front, the side or above, and its controls fold out of the way (robot dog, 2026-11-03)', async () => {
|
|
682
|
+
/* What the preview's camera is, read in the page. */
|
|
683
|
+
const square = () => !!screenCheck.deep('figure-preview')[0].scene.root.activeCamera3d?.isOrthographicCamera
|
|
684
|
+
assert.equal(await read(square), false, 'it starts in perspective')
|
|
685
|
+
await press('the front view', () => screenCheck.deep('figure-preview')[0].shadowRoot.querySelector('button[view=front]'))
|
|
686
|
+
await until('square on', square)
|
|
687
|
+
assert.equal(await read(() => screenCheck.deep('figure-preview')[0].shadowRoot.querySelector('button[view=front]').getAttribute('aria-pressed')), 'true')
|
|
688
|
+
await press('the perspective', () => screenCheck.deep('figure-preview')[0].shadowRoot.querySelector('button[view=perspective]'))
|
|
689
|
+
await until('perspective again', () => !screenCheck.deep('figure-preview')[0].scene.root.activeCamera3d?.isOrthographicCamera)
|
|
690
|
+
await press('fold the controls', () => screenCheck.deep('figure-preview')[0].shadowRoot.querySelector('button[fold-controls]'))
|
|
691
|
+
await until('folded', () => !screenCheck.previewControl('j1-base'))
|
|
692
|
+
assert.ok(await read(() => screenCheck.deep('figure-preview')[0].shadowRoot.querySelector('div[clips] button[fold-controls]')), 'the one button stays')
|
|
693
|
+
await press('unfold the controls', () => screenCheck.deep('figure-preview')[0].shadowRoot.querySelector('button[fold-controls]'))
|
|
694
|
+
await until('back', () => !!screenCheck.previewControl('j1-base'))
|
|
695
|
+
})
|
|
696
|
+
|
|
654
697
|
test('narrow (1024 wide and less, both sides open), the size help and the controls share the bottom row without covering each other', async () => {
|
|
655
698
|
const viewport = page().viewport()
|
|
656
699
|
await page().setViewport({ width: 1024, height: 768 })
|
|
@@ -885,7 +928,17 @@ describe('the side panel is found as it was left, per part', () => {
|
|
|
885
928
|
await pressRow('forearm')
|
|
886
929
|
await until('forearm chosen, at its top', () => screenCheck.editor().selected === 'forearm' && screenCheck.shownPanel().scrollTop === 0)
|
|
887
930
|
await pressRow('upper-arm')
|
|
888
|
-
|
|
931
|
+
/* Said with the numbers when it does not come back: where the panel stands, where it was left, how tall it is. */
|
|
932
|
+
const back = top => {
|
|
933
|
+
const panel = screenCheck.shownPanel()
|
|
934
|
+
return { selected: screenCheck.editor().selected, scrollTop: Math.round(panel.scrollTop), top, scrollHeight: panel.scrollHeight, clientHeight: panel.clientHeight }
|
|
935
|
+
}
|
|
936
|
+
let seen
|
|
937
|
+
for (const end = Date.now() + 8000; Date.now() < end; await new Promise(done => setTimeout(done, 50))) {
|
|
938
|
+
seen = await read(back, top)
|
|
939
|
+
if (seen.selected === 'upper-arm' && Math.abs(seen.scrollTop - top) <= 2) break
|
|
940
|
+
}
|
|
941
|
+
assert.ok(seen.selected === 'upper-arm' && Math.abs(seen.scrollTop - top) <= 2, `upper-arm as it was left: ${JSON.stringify(seen)}`)
|
|
889
942
|
assert.deepEqual(await folds(), opened, 'its folds as they were')
|
|
890
943
|
}
|
|
891
944
|
})
|
|
@@ -31,6 +31,8 @@ const FIGURES = {
|
|
|
31
31
|
},
|
|
32
32
|
pyramid: () => applyFigureAuthoring(polygon(figure('Pyramid'), 'wedge', TRIANGLE), { kind: 'set-section', part: 'wedge', apex: { x: 0, z: 0 } }),
|
|
33
33
|
/* A box standing on the floor: a length change keeps its bottom. */
|
|
34
|
+
/* An elbow pipe (ADR-0100 §16.2): up, a quarter bend toward +X, along. */
|
|
35
|
+
pipe: () => addFigurePrimitive(figure('Pipe'), { id: 'pipe', kind: 'pipe', dimensions: { radius: 50, wall: 5 }, path: [{ line: 400 }, { bend: { radius: 150, angle: 90, toward: 90 } }, { line: 300 }], position: { x: 0, y: 0, z: 0 } }),
|
|
34
36
|
'floor-box': () => applyFigureAuthoring(addFigureBox(figure('Floor'), { id: 'box', size: { x: 400, y: 200, z: 300 }, position: { x: 0, y: 100, z: 0 } }), { kind: 'set-placement', placement: 'floor' }),
|
|
35
37
|
/* A box whose sizes follow the figure's: none of them is its own to set. */
|
|
36
38
|
follows: () => applyFigureAuthoring(addFigureBox(figure('Follows'), { id: 'box', size: { x: 400, y: 400, z: 400 }, position: { x: 0, y: 200, z: 0 } }), { kind: 'follow-size', part: 'box' }),
|
|
@@ -89,7 +91,9 @@ const shapeWords = shape => {
|
|
|
89
91
|
section: drawn ? JSON.parse(JSON.stringify(drawn.section)) : null,
|
|
90
92
|
apex: shape.loft?.apex ?? null,
|
|
91
93
|
length: drawn ? Math.round(drawn.end - drawn.start) : null,
|
|
92
|
-
cavity: shape.cavity ? JSON.parse(JSON.stringify({ wall: shape.cavity.wall, start: shape.cavity.start, end: shape.cavity.end })) : null
|
|
94
|
+
cavity: shape.cavity ? JSON.parse(JSON.stringify({ wall: shape.cavity.wall, start: shape.cavity.start, end: shape.cavity.end })) : null,
|
|
95
|
+
top: shape.loft?.to ? JSON.parse(JSON.stringify(shape.loft.to)) : null,
|
|
96
|
+
pipe: shape.sweep ? JSON.parse(JSON.stringify({ section: shape.sweep.section, to: shape.sweep.to ?? null, wall: shape.sweep.wall ?? null, path: shape.sweep.path })) : null
|
|
93
97
|
}
|
|
94
98
|
}
|
|
95
99
|
const box = element => {
|
|
@@ -116,6 +120,7 @@ Object.assign(window.probe, {
|
|
|
116
120
|
const shown = e => !!e && getComputedStyle(e).display !== 'none'
|
|
117
121
|
const length = root.querySelector('div[sizes] label[mm]')
|
|
118
122
|
const apex = root.querySelector('label[apex]')
|
|
123
|
+
const top = root.querySelector('label[top]')
|
|
119
124
|
return {
|
|
120
125
|
caption: words(caption),
|
|
121
126
|
lastValid: !!caption?.hasAttribute('last-valid'),
|
|
@@ -126,6 +131,7 @@ Object.assign(window.probe, {
|
|
|
126
131
|
note: words(root.querySelector('small[role=status]')),
|
|
127
132
|
length: length ? { label: words(length.querySelector('span')), disabled: length.querySelector('input').disabled, note: words(length.querySelector('small')) } : null,
|
|
128
133
|
apex: apex ? { checked: apex.querySelector('input').checked, disabled: apex.querySelector('input').disabled, words: words(apex.querySelector('small')) } : null,
|
|
134
|
+
top: top ? { checked: top.querySelector('input').checked, disabled: top.querySelector('input').disabled, words: words(top.querySelector('small')) } : null,
|
|
129
135
|
narrow: host.hasAttribute('narrow'),
|
|
130
136
|
show: host.getAttribute('show'),
|
|
131
137
|
tabs: shown(root.querySelector('nav[tabs]')),
|
|
@@ -418,6 +418,87 @@ describe('a drawn section\'s end drawn to one point', () => {
|
|
|
418
418
|
})
|
|
419
419
|
})
|
|
420
420
|
|
|
421
|
+
describe('a box\'s top another size: a tapered box (ADR-0100 §16.1, robot dog 2026-11-03)', () => {
|
|
422
|
+
test('turned on, its top\'s width typed and applied is one step; turned off, a box again', async () => {
|
|
423
|
+
await mount('floor-box')
|
|
424
|
+
await pick('box')
|
|
425
|
+
await openEditor()
|
|
426
|
+
assert.deepEqual((await editorNow()).top, { checked: false, disabled: false, words: '' })
|
|
427
|
+
await press('label[top] input', { within: 'figure-section-editor' })
|
|
428
|
+
await until(() => probe.sectionEditor().top.checked, 'the top on')
|
|
429
|
+
/* The length, the two widths across, then the top's two widths and its corner. */
|
|
430
|
+
await type('div[sizes] label[mm] input', { within: 'figure-section-editor', index: 3 }, '200', 'Tab')
|
|
431
|
+
const start = await state()
|
|
432
|
+
await press('footer button', { within: 'figure-section-editor', text: 'figure.button.section-editor-apply' })
|
|
433
|
+
await until(r => probe.state().revision === r + 1, 'one step', start.revision)
|
|
434
|
+
const now = await held('box')
|
|
435
|
+
assert.equal(now.top.size.x, 200)
|
|
436
|
+
assert.equal(now.section.size.x, 400, 'the bottom as it was')
|
|
437
|
+
await until(() => probe.drawnShape('box')?.top?.size?.x === 200, 'drawn tapered')
|
|
438
|
+
await openEditor()
|
|
439
|
+
await until(() => probe.sectionEditor()?.top?.checked, 'opened again with its top on')
|
|
440
|
+
await press('label[top] input', { within: 'figure-section-editor' })
|
|
441
|
+
await press('footer button', { within: 'figure-section-editor', text: 'figure.button.section-editor-apply' })
|
|
442
|
+
await until(r => probe.state().revision === r + 2, 'one step back', start.revision)
|
|
443
|
+
assert.equal((await held('box')).top, null)
|
|
444
|
+
})
|
|
445
|
+
})
|
|
446
|
+
|
|
447
|
+
describe('a pipe\'s panel: its wall, its pieces, each change one step (ADR-0100 §16.2)', () => {
|
|
448
|
+
before(async () => {
|
|
449
|
+
await mount('pipe')
|
|
450
|
+
await pick('pipe')
|
|
451
|
+
await until(() => !!findDeep('figure-pipe-panel'), 'the pipe panel')
|
|
452
|
+
})
|
|
453
|
+
const pipeNow = async () => (await held('pipe')).pipe
|
|
454
|
+
const step = async (what, act) => {
|
|
455
|
+
const start = await state()
|
|
456
|
+
await act()
|
|
457
|
+
await until(r => probe.state().revision === r + 1, what, start.revision)
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
test('a wall of 0 is a solid bar', async () => {
|
|
461
|
+
await step('the wall', () => type('input[aria-label="figure.label.pipe-wall"]', { within: 'figure-pipe-panel' }, '0'))
|
|
462
|
+
assert.equal((await pipeNow()).wall, null)
|
|
463
|
+
})
|
|
464
|
+
|
|
465
|
+
test('a bend added is the path\'s last piece; its direction chosen, then taken away again', async () => {
|
|
466
|
+
await step('a bend added', () => press('button[add-bend]', { within: 'figure-pipe-panel' }))
|
|
467
|
+
const added = await pipeNow()
|
|
468
|
+
assert.equal(added.path.length, 4)
|
|
469
|
+
assert.deepEqual(added.path[3], { kind: 'bend', radius: 100, angle: 90, toward: 0 })
|
|
470
|
+
/* The second piece's bend turns toward +X; its quarters are said as the figure's axes it turns to there. */
|
|
471
|
+
const words = await read(() => [...findDeep('figure-pipe-panel').shadowRoot.querySelectorAll('ol[pieces] li')[1].querySelectorAll('select option')].map(o => o.textContent.trim()))
|
|
472
|
+
assert.deepEqual(words, ['+Z', '+X', '−Z', '−X'])
|
|
473
|
+
await step('its direction', () =>
|
|
474
|
+
read(() => {
|
|
475
|
+
const select = findDeep('figure-pipe-panel').shadowRoot.querySelectorAll('ol[pieces] li')[1].querySelector('select')
|
|
476
|
+
select.value = '270'
|
|
477
|
+
select.dispatchEvent(new Event('change'))
|
|
478
|
+
})
|
|
479
|
+
)
|
|
480
|
+
assert.equal((await pipeNow()).path[1].toward, 270)
|
|
481
|
+
await step('the last piece taken away', () => read(() => [...findDeep('figure-pipe-panel').shadowRoot.querySelectorAll('ol[pieces] li button[act]')].at(-1).click()))
|
|
482
|
+
assert.equal((await pipeNow()).path.length, 3)
|
|
483
|
+
})
|
|
484
|
+
|
|
485
|
+
/* ADR-0100 §16.2.9: thick below and thin above, one pipe. */
|
|
486
|
+
test('an end diameter typed narrows it toward its end; 「one size all along」 makes it one size again', async () => {
|
|
487
|
+
await step('the end diameter', () => type('input[aria-label="figure.label.pipe-end-diameter"]', { within: 'figure-pipe-panel' }, '40'))
|
|
488
|
+
assert.deepEqual((await pipeNow()).to, { kind: 'ellipse', radius: { x: 20, z: 20 } })
|
|
489
|
+
await step('one size again', () => press('button[end-same]', { within: 'figure-pipe-panel' }))
|
|
490
|
+
assert.equal((await pipeNow()).to, null)
|
|
491
|
+
})
|
|
492
|
+
|
|
493
|
+
test('a bend tighter than the pipe is refused, said with the radius it takes', async () => {
|
|
494
|
+
const start = await state()
|
|
495
|
+
await type('input[aria-label="figure.label.pipe-bend-radius"]', { within: 'figure-pipe-panel' }, '30')
|
|
496
|
+
await until(() => probe.state().error !== '', 'the refusal said')
|
|
497
|
+
assert.equal((await state()).revision, start.revision, 'nothing written')
|
|
498
|
+
assert.equal((await state()).error, 'figure.text.pipe-bend-tight')
|
|
499
|
+
})
|
|
500
|
+
})
|
|
501
|
+
|
|
421
502
|
describe('the inside in the section editor: drawn on the widths being typed, sent with them in one step', () => {
|
|
422
503
|
before(async () => {
|
|
423
504
|
await mount('tray')
|
|
@@ -179,4 +179,17 @@ describe('the preview', () => {
|
|
|
179
179
|
assert.equal(looks[0].radius, '5px')
|
|
180
180
|
assert.notEqual(looks[0].background, 'rgb(239, 239, 239)')
|
|
181
181
|
})
|
|
182
|
+
/* Robot dog 2026-11-03: with the data tab's sample on, a field it gives changed nothing and said nothing. */
|
|
183
|
+
test('a value the sample gives is shown as the sample\'s, not offered as a field; the sample off, the field is back', async () => {
|
|
184
|
+
const rows = () => read(DEEP => {
|
|
185
|
+
const root = eval(DEEP)('figure-preview')[0].shadowRoot
|
|
186
|
+
return { fields: root.querySelectorAll('div[clips] input, div[clips] select').length, sampled: [...root.querySelectorAll('div[clips] label[sampled] output')].map(o => o.textContent.trim()) }
|
|
187
|
+
}, DEEP)
|
|
188
|
+
await read(DEEP => (eval(DEEP)('figure-preview')[0].sample = { A: 4 }), DEEP)
|
|
189
|
+
await until(DEEP => eval(DEEP)('figure-preview')[0].shadowRoot.querySelector('div[clips] label[sampled]'), [DEEP], 'the sampled row')
|
|
190
|
+
assert.deepEqual(await rows(), { fields: 1, sampled: ['4'] }, 'A from the sample, CARRIERSTATUS still a field')
|
|
191
|
+
await read(DEEP => (eval(DEEP)('figure-preview')[0].sample = null), DEEP)
|
|
192
|
+
await until(DEEP => !eval(DEEP)('figure-preview')[0].shadowRoot.querySelector('div[clips] label[sampled]'), [DEEP], 'the field back')
|
|
193
|
+
assert.deepEqual(await rows(), { fields: 2, sampled: [] })
|
|
194
|
+
})
|
|
182
195
|
})
|
package/test/clips.test.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { fileURLToPath } from 'node:url'
|
|
|
11
11
|
import { addFigureBox, applyFigureAuthoring, createFigureAsset, editFigurePart } from '@hatiolab/figure-model'
|
|
12
12
|
import type { FigureAsset } from '@hatiolab/figure-model'
|
|
13
13
|
|
|
14
|
-
import { clipsOf, poseAt, trackEdit, undrivenOf, withKey, withoutKey } from '../client/modeller/clips.ts'
|
|
14
|
+
import { clipsOf, poseAt, trackEdit, undrivenOf, withKey, withKeyChanged, withoutKey } from '../client/modeller/clips.ts'
|
|
15
15
|
import { moving } from './joints.ts'
|
|
16
16
|
|
|
17
17
|
const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
|
|
@@ -62,3 +62,32 @@ test('움직임 탭에 서고, 막대는 입력 한 묶음 · 나머지는 부
|
|
|
62
62
|
for (const key of keys) assert.ok(table[key], `${lang}: ${key}`)
|
|
63
63
|
}
|
|
64
64
|
})
|
|
65
|
+
|
|
66
|
+
test('키는 그 자리에서 시각과 값을 고친다 — 첫 키는 0초, 클립 밖·다른 키와 같은 시각은 거절 (제안 18, 2026-11-03)', () => {
|
|
67
|
+
const keys = [{ seconds: 0, value: 0 }, { seconds: 1, value: 30 }, { seconds: 2, value: 60 }]
|
|
68
|
+
assert.deepEqual(withKeyChanged(keys, 1, { value: 45 }, 2), [{ seconds: 0, value: 0 }, { seconds: 1, value: 45 }, { seconds: 2, value: 60 }])
|
|
69
|
+
assert.deepEqual(withKeyChanged(keys, 1, { seconds: 1.37 }, 2)!.map(k => k.seconds), [0, 1.37, 2], '1% 눈금에 매이지 않는다')
|
|
70
|
+
assert.deepEqual(withKeyChanged(keys, 2, { seconds: 0.5 }, 2)!.map(k => k.value), [0, 60, 30], '시각 순서로 다시 선다')
|
|
71
|
+
assert.equal(withKeyChanged(keys, 0, { seconds: 0.2 }, 2), null, '첫 키는 0초')
|
|
72
|
+
assert.equal(withKeyChanged(keys, 1, { seconds: 2 }, 2), null, '다른 키의 시각')
|
|
73
|
+
assert.equal(withKeyChanged(keys, 1, { seconds: 3 }, 2), null, '클립 밖')
|
|
74
|
+
assert.equal(withKeyChanged(keys, 1, { value: NaN }, 2), null)
|
|
75
|
+
assert.deepEqual(withKeyChanged(keys, 0, { value: 10 }, 2)![0], { seconds: 0, value: 10 }, '첫 키의 값은 고친다')
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
/* Robot dog 2026-11-03: a curve given to another control on its own beat, a clip copied to be timed apart. */
|
|
79
|
+
test('a track copied onto another control a while later; a clip copy takes a free name', async () => {
|
|
80
|
+
const { copiedTrackEdit, copyTargetsOf, freeClipNameOf, shiftable } = await import('../client/modeller/clips.ts')
|
|
81
|
+
let asset = editFigurePart(machine(), { kind: 'set-driver', state: 'spin', clip: 'turn', duration: 2, keys: [{ seconds: 0, value: 0 }, { seconds: 1, value: 90 }, { seconds: 2, value: 0 }] })
|
|
82
|
+
const clip = clipsOf(asset)[0]!
|
|
83
|
+
const track = clip.tracks[0]!
|
|
84
|
+
assert.deepEqual(copyTargetsOf(asset, 'turn'), ['spin', 'shift'], 'its own control, then those it may take')
|
|
85
|
+
assert.ok(shiftable(track))
|
|
86
|
+
const onto = copiedTrackEdit(asset, clip, track, 'shift', 0.5)
|
|
87
|
+
assert.equal(onto.state, 'shift')
|
|
88
|
+
assert.deepEqual(onto.keys[0], { seconds: 0, value: 45 }, 'half a second later it starts half way up')
|
|
89
|
+
asset = editFigurePart(asset, onto)
|
|
90
|
+
assert.deepEqual(clipsOf(asset)[0]!.tracks.map(t => t.state).sort(), ['shift', 'spin'])
|
|
91
|
+
assert.equal(freeClipNameOf(asset, 'turn'), 'turn-copy')
|
|
92
|
+
assert.equal(freeClipNameOf(editFigurePart(asset, { kind: 'set-clip', clip: 'turn', name: 'turn-copy' }), 'turn-copy'), 'turn-copy-copy')
|
|
93
|
+
})
|
package/test/data-motion.test.ts
CHANGED
|
@@ -308,7 +308,9 @@ test('데이터 탭 · 클립 패널 · 미리보기의 배선과 문구', () =>
|
|
|
308
308
|
assert.match(clips, /kind: 'remove-driver', state: track\.state, clip: clip\.name/)
|
|
309
309
|
/* A key's value is shown to three decimals: a slider's 14.399999999999999 is not what a person typed. */
|
|
310
310
|
assert.match(clips, /value: current === undefined \? '' : shown\(current\)/)
|
|
311
|
-
|
|
311
|
+
/* Typed in its chip since 2026-11-03: the field holds the value to a thousandth, with no grouping. */
|
|
312
|
+
assert.match(clips, /const typed = \(value: number\) => String\(Math\.round\(value \* 1000\) \/ 1000\)/)
|
|
313
|
+
assert.match(clips, /\.value=\$\{typed\(key\.value\)\}/)
|
|
312
314
|
const keys = [read('../client/modeller/figure-data-panel.ts'), clips, read('../client/modeller/refusals.ts')].flatMap(src =>
|
|
313
315
|
[...src.matchAll(/'(figure\.[a-z.-]*[a-z])'/g)].map(m => m[1]!)
|
|
314
316
|
)
|
|
@@ -340,3 +342,14 @@ test('데이터 탭 · 클립 패널 · 미리보기의 배선과 문구', () =>
|
|
|
340
342
|
for (const key of made) assert.ok(table[key], `${lang}: ${key}`)
|
|
341
343
|
}
|
|
342
344
|
})
|
|
345
|
+
|
|
346
|
+
/* Robot dog 2026-11-03: what the sample gives a row is what the figure shows; the preview says so. */
|
|
347
|
+
test('the value data gives a row is read down its accessor; left out or null, there is none', async () => {
|
|
348
|
+
const { dataValueAt } = await import('../client/modeller/data-interface.ts')
|
|
349
|
+
const data = { status: 'ALARM', arm: { joints: [{ angle: 30 }] }, off: null }
|
|
350
|
+
assert.equal(dataValueAt(data, 'status'), 'ALARM')
|
|
351
|
+
assert.equal(dataValueAt(data, 'arm.joints[0].angle'), 30)
|
|
352
|
+
assert.equal(dataValueAt(data, 'off'), undefined)
|
|
353
|
+
assert.equal(dataValueAt(data, 'missing.deep'), undefined)
|
|
354
|
+
assert.equal(dataValueAt(null, 'status'), undefined)
|
|
355
|
+
})
|
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
import { test } from 'node:test'
|
|
9
9
|
import assert from 'node:assert/strict'
|
|
10
10
|
|
|
11
|
-
import { addFigureBox, applyFigureAuthoring, compileFigureAsset, createFigureAsset } from '@hatiolab/figure-model'
|
|
11
|
+
import { addFigureBox, addFigurePrimitive, applyFigureAuthoring, compileFigureAsset, createFigureAsset } from '@hatiolab/figure-model'
|
|
12
12
|
import type { FigureAsset, FigureAuthoringAction } from '@hatiolab/figure-model'
|
|
13
13
|
|
|
14
|
-
import { alignmentAt, faceViewOf, offsetOf, overhangOf, screenOf } from '../client/modeller/fasten-geometry.ts'
|
|
14
|
+
import { alignmentAt, faceViewOf, facesAsDrawnOf, offsetOf, overhangOf, screenOf } from '../client/modeller/fasten-geometry.ts'
|
|
15
15
|
import { revisionOf } from './revision-of.ts'
|
|
16
16
|
|
|
17
17
|
const scene = (lidRotation?: { x: number; y: number; z: number }): FigureAsset => {
|
|
@@ -86,3 +86,14 @@ test('the drawing\'s two names do not lie over each other (user 2026-10-16: 「r
|
|
|
86
86
|
/* A face that fills the drawing: its name inside its top edge. */
|
|
87
87
|
assert.deepEqual(fastenLabelsOf({ x: 4, y: 4, w: 300, h: 170 }, { x: 100, y: 60, w: 100, h: 40 }, 'lid', 180).face, { x: 8, y: 18 })
|
|
88
88
|
})
|
|
89
|
+
|
|
90
|
+
test('붙일 면 그림은 그려진 자세대로 — 눕힌 원기둥의 위·아래 면은 옆에 서고, 화면의 위는 면이 아니다 (로봇개, 2026-11-03)', () => {
|
|
91
|
+
const upright = revisionOf(scene())
|
|
92
|
+
assert.deepEqual(facesAsDrawnOf(upright, 'bin'), { left: 'left', right: 'right', bottom: 'bottom', top: 'top', back: 'back', front: 'front' }, '돌지 않았으면 제 이름 그대로')
|
|
93
|
+
const laid = revisionOf(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 } } as never))
|
|
94
|
+
const drawn = facesAsDrawnOf(laid, 'drum')
|
|
95
|
+
/* Turned 90° about Z: its own top points along -X, its own bottom along +X. */
|
|
96
|
+
assert.deepEqual([drawn.left, drawn.right], ['top', 'bottom'])
|
|
97
|
+
assert.ok(!['top', 'bottom'].includes(drawn.top) && !['top', 'bottom'].includes(drawn.bottom), `화면의 위·아래는 원기둥의 면이 아니다: ${drawn.top}, ${drawn.bottom}`)
|
|
98
|
+
assert.deepEqual([drawn.front, drawn.back], ['front', 'back'])
|
|
99
|
+
})
|
|
@@ -13,6 +13,7 @@ import { jointAxisUnit, jointAxisWord } from '../client/modeller/joint-axis.ts'
|
|
|
13
13
|
import { alignOf, endPlaceKeyOf, fasteningOf, fixedJointOf, motionFormOf, motionFormRefusalOf, motionJoint, motionOf, rowAxesOf, rowBlockOf } from '../client/modeller/links.ts'
|
|
14
14
|
import { jointDragOf } from '../client/modeller/joint-drag.ts'
|
|
15
15
|
import { jointFanOf } from '../client/modeller/joint-fan.ts'
|
|
16
|
+
import { defaultsMovedOf } from '../client/modeller/refusals.ts'
|
|
16
17
|
import { revisionOf } from './revision-of.ts'
|
|
17
18
|
|
|
18
19
|
type Dynamic = any
|
|
@@ -147,9 +148,18 @@ test('a range need not hold 0; it needs its most above its least', () => {
|
|
|
147
148
|
const fixed = jointOf(still, 'link')
|
|
148
149
|
const lifted = set(still, motionJoint(fixed, { ...motionFormOf(fixed), kind: 'turn', min: 10, max: 90 }, 'link'))
|
|
149
150
|
assert.equal(lifted.stateDefaults[jointOf(lifted, 'link').motion[0].state.id], 10)
|
|
150
|
-
/*
|
|
151
|
+
/*
|
|
152
|
+
An existing control keeps its name; a range that leaves its stored value out brings the value to the nearest end, as
|
|
153
|
+
set-state-input does -- one rule for a default out of its range (2026-11-03, replacing the 10-04 refusal). The screen
|
|
154
|
+
says it moved (\`defaultsMovedOf\`).
|
|
155
|
+
*/
|
|
151
156
|
const j = jointOf(plain(), 'link')
|
|
152
|
-
|
|
157
|
+
const id = motionFormOf(j).id
|
|
158
|
+
const narrowed = set(plain(), motionJoint(j, { ...motionFormOf(j), min: 10, max: 90 }, 'link'))
|
|
159
|
+
assert.equal(motionFormOf(jointOf(narrowed, 'link')).id, id, 'the same control')
|
|
160
|
+
assert.equal(narrowed.stateDefaults[id], 10)
|
|
161
|
+
const say = (key: string, p: Record<string, string>) => `${key} ${JSON.stringify(p)}`
|
|
162
|
+
assert.equal(defaultsMovedOf(plain(), narrowed, say), `figure.text.default-moved-to-end ${JSON.stringify({ control: id, from: '0', to: '10' })}`)
|
|
153
163
|
})
|
|
154
164
|
|
|
155
165
|
test('old shapes: every form path writes the joint back as it was, saved and reopened', () => {
|
package/test/links.test.ts
CHANGED
|
@@ -757,3 +757,27 @@ test('상태에 따라 크기 — 지름으로 적은 두 끝이 반지름 명
|
|
|
757
757
|
assert.deepEqual(stateSizeFixedOf(asset, 'wound', 'x'), { kind: 'unlink-dimension', part: 'wound', axis: 'x', mm: 450 }, '쉬는 값(100 %)의 반지름으로 고정')
|
|
758
758
|
assert.equal(stateSizeFixedOf(asset, 'wound', 'y'), undefined)
|
|
759
759
|
})
|
|
760
|
+
|
|
761
|
+
test('축이 지나는 점 — 폼에서 정하고, 부품은 두고 축만 옮기며, 길이를 따르는 거리는 그대로 다시 쓴다 (ADR-0159, 로봇개 2026-11-03)', async () => {
|
|
762
|
+
const { motionFormOf, motionJoint } = await import('../client/modeller/links.ts')
|
|
763
|
+
const { compileFigureAsset } = await import('@hatiolab/figure-model')
|
|
764
|
+
let asset = createFigureAsset('Hip')
|
|
765
|
+
asset = addFigureBox(asset, { id: 'body', size: { x: 400, y: 100, z: 200 }, position: { x: 0, y: 300, z: 0 } })
|
|
766
|
+
asset = addFigureBox(asset, { id: 'leg', size: { x: 40, y: 200, z: 40 }, position: { x: 0, y: 0, z: 0 } })
|
|
767
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'body', to: { plane: 'mounting-plane' } })
|
|
768
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'leg', to: { part: 'body', face: 'bottom' } })
|
|
769
|
+
const joint = () => asset.joints!.find(j => j.body1.part === 'leg')!
|
|
770
|
+
const form = { kind: 'turn' as const, axis: 'z' as const, id: 'hip', min: -90, max: 90, follow: '', multiplier: 1, offset: 0, through: { x: 100 } }
|
|
771
|
+
asset = applyFigureAuthoring(asset, { kind: 'set-joint', joint: motionJoint(joint(), form, 'leg') })
|
|
772
|
+
assert.deepEqual(joint().motion![0]!.through, { x: 100 })
|
|
773
|
+
assert.deepEqual(motionFormOf(joint()).through, { x: 100 }, '폼이 다시 읽는다')
|
|
774
|
+
const plain = applyFigureAuthoring(asset, { kind: 'set-joint', joint: motionJoint(joint(), { ...form, through: {} }, 'leg') })
|
|
775
|
+
const at = (a: FigureAsset, hip: number) => compileFigureAsset(a).evaluate({ hip }).geometry.find(g => g.id === 'leg')!.pose.t.map(v => Math.round(v))
|
|
776
|
+
assert.deepEqual(at(asset, 0), at(plain, 0), '0 에서는 축이 어디를 지나든 제자리 — 부품은 옮기지 않는다')
|
|
777
|
+
/* Turned a quarter about a point 100 mm along x, the leg swings around that point, not about the joint. */
|
|
778
|
+
assert.notDeepEqual(at(asset, 90), at(plain, 90))
|
|
779
|
+
/* A distance that follows a length is said and written back unchanged; one cleared to 0 goes. */
|
|
780
|
+
const follows = { y: { from: { of: 'part' as const, part: 'body', axis: 'y' as const }, times: 0.5 } }
|
|
781
|
+
const kept = motionJoint({ ...joint(), motion: [{ ...joint().motion![0]!, through: { x: 100, ...follows } }] }, { ...motionFormOf({ ...joint(), motion: [{ ...joint().motion![0]!, through: { x: 100, ...follows } }] }), through: { x: 0 } }, 'leg')
|
|
782
|
+
assert.deepEqual(kept.motion![0]!.through, follows)
|
|
783
|
+
})
|
package/test/material.test.ts
CHANGED
|
@@ -9,7 +9,7 @@ import assert from 'node:assert/strict'
|
|
|
9
9
|
|
|
10
10
|
import { addFigureBox, addFigurePrimitive, compileFigureAsset, createFigureAsset, editFigurePart, figureCostOf } from '@hatiolab/figure-model'
|
|
11
11
|
|
|
12
|
-
import { glowOf, materialEditOf, offeredPaletteOf } from '../client/modeller/material-edit.ts'
|
|
12
|
+
import { glowOf, materialEditOf, offeredPaletteOf, ownColourOf } from '../client/modeller/material-edit.ts'
|
|
13
13
|
|
|
14
14
|
const probe = () => {
|
|
15
15
|
let asset = createFigureAsset('Probe')
|
|
@@ -68,3 +68,17 @@ test('자산에 없는 색 역할은 그 색을 함께 보낸다 — 새 피규
|
|
|
68
68
|
const own = materialEditOf('body', { token: 'palette.accent' }, { token: 'palette.primary' }, asset.palette, offered)
|
|
69
69
|
assert.equal('palette' in own, false, '자산에 있는 색이면 보낼 것이 없다')
|
|
70
70
|
})
|
|
71
|
+
|
|
72
|
+
test('직접 정한 색은 그 값으로 이름 붙어 자산 팔레트에 함께 들어가고, 테마 역할이 아니다 (로봇개 노랑, 2026-11-03)', () => {
|
|
73
|
+
const asset = probe()
|
|
74
|
+
const { token, colour } = ownColourOf('#FFD400')
|
|
75
|
+
assert.deepEqual([token, colour], ['colour.ffd400', '#ffd400'])
|
|
76
|
+
const edit = materialEditOf('body', { token: 'palette.primary' }, { token }, asset.palette, { ...offeredPaletteOf({}, asset.palette), [token]: colour }) as { palette?: Record<string, string> }
|
|
77
|
+
assert.deepEqual(edit.palette, { 'colour.ffd400': '#ffd400' })
|
|
78
|
+
const after = editFigurePart(asset, edit as never)
|
|
79
|
+
assert.equal(drawn(after, 'body').material.token, 'colour.ffd400')
|
|
80
|
+
assert.equal(after.palette['colour.ffd400'], '#ffd400')
|
|
81
|
+
/* The same colour again is the same entry: nothing more to add. */
|
|
82
|
+
const again = materialEditOf('drum', { token: 'palette.primary' }, { token }, after.palette, { ...offeredPaletteOf({}, after.palette), [token]: colour })
|
|
83
|
+
assert.equal('palette' in again, false)
|
|
84
|
+
})
|
package/test/maturity.test.ts
CHANGED
|
@@ -150,7 +150,7 @@ test('여섯 축 — 정해진 순서, 잴 수 없는 둘은 비율 없이 까
|
|
|
150
150
|
)
|
|
151
151
|
const byKey = Object.fromEntries(axes.map(axis => [axis.key, axis]))
|
|
152
152
|
assert.equal(byKey.detail!.ratio, undefined)
|
|
153
|
-
assert.equal(byKey.detail!.blocked, 'figure.text.maturity-blocked-no-detail-level', '
|
|
153
|
+
assert.equal(byKey.detail!.blocked, 'figure.text.maturity-blocked-no-detail-level', '등급을 정하지 않은 피규어')
|
|
154
154
|
assert.equal(byKey.reactive!.ratio, undefined)
|
|
155
155
|
assert.equal(byKey.reactive!.blocked, 'figure.text.maturity-blocked-no-binding')
|
|
156
156
|
|
|
@@ -161,6 +161,24 @@ test('여섯 축 — 정해진 순서, 잴 수 없는 둘은 비율 없이 까
|
|
|
161
161
|
assert.equal(byKey.readable!.reading, '2')
|
|
162
162
|
})
|
|
163
163
|
|
|
164
|
+
test('등급 충실도 — 디테일 등급을 정하면 그 등급이 허용하는 부품 수 중 쓴 비율로 잰다 (ADR-0087 등급 복귀, 로봇개 2026-11-03)', () => {
|
|
165
|
+
let asset = withPalette('palette.accent')
|
|
166
|
+
asset = addFigureBox(asset, cube('a', 50))
|
|
167
|
+
asset = addFigureBox(asset, cube('b', 150, 'palette.accent'))
|
|
168
|
+
asset = { ...asset, detailLevel: 'M' }
|
|
169
|
+
const detail = figureReportOf(asset)!.axes.find(axis => axis.key === 'detail')!
|
|
170
|
+
assert.equal(detail.blocked, undefined, 'XL·M 을 골라도 「등급이 없다」고 하지 않는다')
|
|
171
|
+
assert.equal(detail.ratio, 2 / 8)
|
|
172
|
+
assert.equal(detail.reading, '2/8')
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
test('무제한 등급은 부품 수를 재지 않는다고 말한다 — 등급이 없다고 하지 않는다 (사용자 2026-11-03)', () => {
|
|
176
|
+
let asset = withPalette('palette.accent')
|
|
177
|
+
asset = addFigureBox(asset, cube('a', 50))
|
|
178
|
+
const detail = figureReportOf({ ...asset, detailLevel: 'UNLIMITED' })!.axes.find(axis => axis.key === 'detail')!
|
|
179
|
+
assert.deepEqual([detail.ratio, detail.blocked], [undefined, 'figure.text.maturity-blocked-unlimited'])
|
|
180
|
+
})
|
|
181
|
+
|
|
164
182
|
test('재질이 한도를 넘으면 일관성이 한 칸 내려간다 — 셋 중 하나', () => {
|
|
165
183
|
let asset = withPalette('t1', 't2', 't3')
|
|
166
184
|
asset = addFigureBox(asset, cube('a', 0))
|
|
@@ -13,8 +13,9 @@ import { fileURLToPath } from 'node:url'
|
|
|
13
13
|
/**
|
|
14
14
|
* Measured in the app's icon font on :3300, 2026-09-24: each draws one cell wide. `wall` drew 52px -- its letters.
|
|
15
15
|
* 2026-09-30: `details` (the pyramid) 24px at 24px; `pyramid` is not in the font (168px).
|
|
16
|
+
* 2026-11-03: `conversion_path` (the pipe) 24px in the app's font file, material-symbols 0.28.2 outlined, at 24px.
|
|
16
17
|
*/
|
|
17
|
-
const RENDERS = new Set(['deployed_code', 'database', 'crop_square', 'circle', 'change_history', 'hexagon', 'details', 'check_box_outline_blank', 'crop_portrait', 'door_front'])
|
|
18
|
+
const RENDERS = new Set(['deployed_code', 'database', 'crop_square', 'circle', 'change_history', 'hexagon', 'details', 'check_box_outline_blank', 'crop_portrait', 'door_front', 'conversion_path'])
|
|
18
19
|
|
|
19
20
|
test('every shape in the palette has an icon the font draws', () => {
|
|
20
21
|
const parts = readFileSync(fileURLToPath(new URL('../client/modeller/figure-parts.ts', import.meta.url)), 'utf8')
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* **관 패널** (ADR-0100 §16.2, 로봇개 11-03 · 사용자 「파이프같은 경우가 많이 활용될 것 같으니까」).
|
|
3
|
+
*
|
|
4
|
+
* 굽는 방향은 커널의 「단면 첫 축에서 잰 각도」로 저장되지만, 화면은 그 자리에서 실제로 꺾이는 축(+X 쪽)으로 말한다.
|
|
5
|
+
* 굽힘마다 그 굽힘이 시작하는 자리의 단면 축을 커널이 놓은 스테이션에서 읽는다.
|
|
6
|
+
*/
|
|
7
|
+
import { test } from 'node:test'
|
|
8
|
+
import assert from 'node:assert/strict'
|
|
9
|
+
|
|
10
|
+
import { addFigurePrimitive, applyFigureAuthoring, compileFigureAsset, createFigureAsset } from '@hatiolab/figure-model'
|
|
11
|
+
import type { FigureAsset, FigureEvaluatedShape } from '@hatiolab/figure-model'
|
|
12
|
+
|
|
13
|
+
import { bendWordOf, piecesOf } from '../client/modeller/pipe-path.ts'
|
|
14
|
+
|
|
15
|
+
const PATH = [{ line: 200 }, { bend: { radius: 100, angle: 90, toward: 90 } }, { line: 200 }, { bend: { radius: 100, angle: 90, toward: 0 } }]
|
|
16
|
+
const pipe = (path = PATH): FigureAsset =>
|
|
17
|
+
addFigurePrimitive(createFigureAsset('P'), { id: 'pipe', kind: 'pipe', dimensions: { radius: 25, wall: 3 }, path, position: { x: 0, y: 0, z: 0 } })
|
|
18
|
+
const sweepOf = (a: FigureAsset) => (compileFigureAsset(a).evaluate().geometry.find(g => (g as { id: string }).id === 'pipe') as unknown as { shape: FigureEvaluatedShape }).shape.sweep!
|
|
19
|
+
|
|
20
|
+
test('a bend says the axis it turns to there: the first, up Y, toward +X; the second, heading +X, as its section lies then', () => {
|
|
21
|
+
const sweep = sweepOf(pipe())
|
|
22
|
+
assert.equal(bendWordOf(sweep, 0, 90), '', 'a run has none')
|
|
23
|
+
assert.deepEqual([0, 90, 180, 270].map(t => bendWordOf(sweep, 1, t)), ['+Z', '+X', '−Z', '−X'])
|
|
24
|
+
/* Heading +X after the first bend, its section's axes are turned with it: toward 0 is still +Z, 90 is now −Y. */
|
|
25
|
+
assert.deepEqual([0, 90, 180, 270].map(t => bendWordOf(sweep, 3, t)), ['+Z', '−Y', '−Z', '+Y'])
|
|
26
|
+
assert.equal(bendWordOf(sweep, 1, 45), '', 'between the axes: no word')
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test('the path as set-pipe takes it, read back from the shape: the same pieces', () => {
|
|
30
|
+
assert.deepEqual(piecesOf(sweepOf(pipe())), PATH)
|
|
31
|
+
const longer = applyFigureAuthoring(pipe(), { kind: 'set-pipe', part: 'pipe', path: [...PATH, { line: 50 }] })
|
|
32
|
+
assert.deepEqual(piecesOf(sweepOf(longer)), [...PATH, { line: 50 }])
|
|
33
|
+
})
|
package/test/refusals.test.ts
CHANGED
|
@@ -14,6 +14,7 @@ import * as fm from '@hatiolab/figure-model'
|
|
|
14
14
|
import type { FigureAsset } from '@hatiolab/figure-model'
|
|
15
15
|
import {
|
|
16
16
|
clipLossOf,
|
|
17
|
+
defaultsMovedOf,
|
|
17
18
|
editRefusalOf,
|
|
18
19
|
inputRangeRefusalOf,
|
|
19
20
|
partNameRefusalOf,
|
|
@@ -216,6 +217,22 @@ test('기즈모로 옮길 수 없는 부품 — 관절이 자리를 정하는지
|
|
|
216
217
|
assert.equal(placedRefusalOf(refusalOf(() => act(conveyor(), { kind: 'set-repeat', part: 'frame', pitch: 10 })), '프레임', keyed), null, '다른 EDIT_TARGET 은 이 말이 아니다')
|
|
217
218
|
})
|
|
218
219
|
|
|
220
|
+
test('코드로만 보이던 거절 — 관절·붙임이 자리를 정함은 어느 명령에서든, 대칭 복사, 없는 값 (로봇개 견본 2026-11-03)', () => {
|
|
221
|
+
/* The same refusal as the gizmo's, reached from another command: named by the part the kernel gave, not "(EDIT_TARGET)". */
|
|
222
|
+
assert.match(editRefusalOf(refusalOf(() => act(conveyor(), { kind: 'nudge-part', part: 'roller', by: { x: 10 } })), keyed), /gizmo-joint-places .*"part":"roller"/)
|
|
223
|
+
const mirror = (asset: FigureAsset, target: string) => refusalOf(() => fm.editFigurePart(asset, { kind: 'mirror-part', target, axis: 'x' } as never))
|
|
224
|
+
assert.match(editRefusalOf(mirror(box(), 'box'), keyed), /mirror-would-overlap .*"part":"box"/)
|
|
225
|
+
let off = fm.addFigureBox(box(), { id: 'side', size: { x: 100, y: 100, z: 100 }, position: { x: 500, y: 50, z: 0 } })
|
|
226
|
+
off = fm.addFigureBox(off, { id: 'lamp', size: { x: 20, y: 20, z: 20 }, position: { x: 0, y: 0, z: 0 } })
|
|
227
|
+
off = act(off, { kind: 'attach', part: 'lamp', to: { part: 'side', face: 'top' } })
|
|
228
|
+
assert.match(editRefusalOf(mirror(off, 'lamp'), keyed), /mirror-not-symmetric .*"part":"lamp"/)
|
|
229
|
+
/* A value the figure does not have, as a fastened part's own position is not (the mirror that wrote it, before). */
|
|
230
|
+
const w = fm.createFigureEditorWorkspace(), context = w.open(fm.serializeFigureAsset(box())).context!
|
|
231
|
+
const unknown = refusalOf(() => w.apply({ context, origin: 'ui', expectedRevision: 0, changes: [{ kind: 'set-design', input: 'lamp.position.x', unit: 'mm', value: 1 }] }))
|
|
232
|
+
const said = editRefusalOf(unknown, say)
|
|
233
|
+
assert.ok(said.includes('lamp.position.x') && !said.includes('UNKNOWN_INPUT'), said)
|
|
234
|
+
})
|
|
235
|
+
|
|
219
236
|
test('옮겨 적은 거절 — 기록은 커널의 거절과 같은 모양이라 같은 말로 읽힌다 (ADR-0127)', () => {
|
|
220
237
|
const a = act(fm.addFigureBox(fm.createFigureAsset('Post'), { id: 'post', size: { x: 100, y: 1000, z: 100 }, position: { x: 0, y: 0, z: 0 } }), {
|
|
221
238
|
kind: 'attach',
|
|
@@ -326,3 +343,15 @@ test('화면은 커널의 거절 문장에 정규식을 대지 않는다 (ADR-01
|
|
|
326
343
|
const refusals = readFileSync(join(root, 'modeller/refusals.ts'), 'utf8')
|
|
327
344
|
assert.doesNotMatch(refusals, /\.message\b/, 'refusals.ts 는 문장을 읽지 않는다')
|
|
328
345
|
})
|
|
346
|
+
|
|
347
|
+
test('기본 자세가 새 범위 밖이라 옮겨지면 한 줄로 말한다 — 조작값, 어디서 어디로; 범위 안에서 정한 값은 말하지 않는다 (제안 6, 2026-11-03)', () => {
|
|
348
|
+
const knee = (min: number, max: number, start: number) => {
|
|
349
|
+
const a = fm.addFigureBox(fm.createFigureAsset('Knee'), { id: 'shin', size: { x: 40, y: 200, z: 40 }, position: { x: 0, y: 0, z: 0 } })
|
|
350
|
+
a.document.model.inputs.push({ id: 'fl-knee', unit: 'deg', role: 'state', min, max } as never)
|
|
351
|
+
a.stateDefaults['fl-knee'] = start
|
|
352
|
+
return a
|
|
353
|
+
}
|
|
354
|
+
assert.equal(defaultsMovedOf(knee(-140, 0, -60), knee(0, 140, 0), say), '기본 자세 fl-knee -60 → 0(새 범위 끝)으로 옮겼습니다.')
|
|
355
|
+
assert.equal(defaultsMovedOf(knee(0, 140, 20), knee(0, 140, 60), say), '', '범위 안에서 작성자가 바꾼 값')
|
|
356
|
+
assert.equal(defaultsMovedOf(knee(0, 140, 20), knee(0, 140, 20), say), '')
|
|
357
|
+
})
|
package/test/revision.test.ts
CHANGED
|
@@ -89,7 +89,7 @@ function selectionQuestions(shown: FigureRevision, part: string) {
|
|
|
89
89
|
jointFanOf(shown, part)
|
|
90
90
|
partSummaryOf(asset, part, key => key, 'ko')
|
|
91
91
|
for (const axis of ['x', 'y', 'z'] as const) measureNow(shown, part, axis)
|
|
92
|
-
for (const axis of ['x', 'z'] as const) canMirror(shown, part, axis
|
|
92
|
+
for (const axis of ['x', 'z'] as const) canMirror(shown, part, axis)
|
|
93
93
|
figurePlaneAlignChanges(asset, part)
|
|
94
94
|
figureFaceOverhangOf(asset, part, shown.inputs)
|
|
95
95
|
figurePartFaceOf(asset, part, 'top', shown.inputs)
|
|
@@ -163,7 +163,7 @@ test('사각형·원도 같은 창에서 — 폭·지름·길이를 한 명령
|
|
|
163
163
|
const box = applyFigureAuthoring(addFigureBox(createFigureAsset('B'), { id: 'b', size: { x: 400, y: 200, z: 300 }, position: { x: 0, y: 100, z: 0 } }), { kind: 'set-placement', placement: 'floor' })
|
|
164
164
|
const shape = (compileFigureAsset(box).evaluate().geometry.find(g => (g as { id: string }).id === 'b') as unknown as { shape: FigureEvaluatedShape }).shape
|
|
165
165
|
const w = workingOf(shape)
|
|
166
|
-
assert.deepEqual(w, { points: [], across: { x: 400, z: 300 }, length: 200, apex: null, cavity: null })
|
|
166
|
+
assert.deepEqual(w, { points: [], across: { x: 400, z: 300 }, length: 200, apex: null, top: null, cavity: null })
|
|
167
167
|
assert.deepEqual(sectionOf(shape, { ...w, across: { x: 500, z: 300 } }), { kind: 'rectangle', size: { x: 500, z: 300 } })
|
|
168
168
|
assert.deepEqual(figureSectionMeasuresOf(box, 'b')!.measures, { y: 'editable', x: 'editable', z: 'editable' })
|
|
169
169
|
/* The editor sends only what changed: a measure left alone may follow another value and must not be written. */
|
|
@@ -182,6 +182,22 @@ test('사각형·원도 같은 창에서 — 폭·지름·길이를 한 명령
|
|
|
182
182
|
assert.equal(workingIssuesOf(r, { ...workingOf(r), length: 0 }).words[0]!.key, 'figure.text.section-issue-length')
|
|
183
183
|
})
|
|
184
184
|
|
|
185
|
+
/* ADR-0100 §16.1 (robot dog 2026-11-03): a box's top another size -- a tapered box -- is one command from the same window. */
|
|
186
|
+
test('윗면 크기 바꾸기: 켜면 윗면의 크기와 모서리를 한 명령으로, 끄면 다시 상자로 — 윗면이 맞지 않으면 보내기 전에 말한다', async () => {
|
|
187
|
+
const { addFigureBox } = await import('@hatiolab/figure-model')
|
|
188
|
+
const { workingOf, workingIssuesOf } = await import('../client/modeller/section-drawing.ts')
|
|
189
|
+
const box = addFigureBox(createFigureAsset('B'), { id: 'b', size: { x: 400, y: 200, z: 300 }, position: { x: 0, y: 100, z: 0 } })
|
|
190
|
+
const shapeOf = (a: FigureAsset) => (compileFigureAsset(a).evaluate().geometry.find(g => (g as { id: string }).id === 'b') as unknown as { shape: FigureEvaluatedShape }).shape
|
|
191
|
+
const w = workingOf(shapeOf(box))
|
|
192
|
+
const top = { across: { x: 200, z: 150 }, corner: 20 }
|
|
193
|
+
const action = sectionActionOf('b', 'rectangle', w, { ...w, top })
|
|
194
|
+
assert.deepEqual(action, { kind: 'set-section', part: 'b', top })
|
|
195
|
+
const tapered = applyFigureAuthoring(box, action)
|
|
196
|
+
assert.deepEqual(workingOf(shapeOf(tapered)).top, top, 'opened again, the top as it was set')
|
|
197
|
+
assert.deepEqual(sectionActionOf('b', 'rectangle', workingOf(shapeOf(tapered)), { ...workingOf(shapeOf(tapered)), top: null }), { kind: 'set-section', part: 'b', top: null })
|
|
198
|
+
assert.equal(workingIssuesOf(shapeOf(box), { ...w, top: { across: { x: 30, z: 150 }, corner: 20 } }).words[0]!.key, 'figure.text.section-issue-top')
|
|
199
|
+
})
|
|
200
|
+
|
|
185
201
|
test('속 공간이 있으면 폭·길이가 그 벽과 막힌 끝을 남기는지도 커널로 묻는다', async () => {
|
|
186
202
|
const { addFigureBox } = await import('@hatiolab/figure-model')
|
|
187
203
|
const { workingOf, workingIssuesOf } = await import('../client/modeller/section-drawing.ts')
|