@things-factory/figure-ui 10.1.208 → 10.1.211
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/data-interface.ts +5 -5
- package/client/modeller/editor.ts +97 -88
- package/client/modeller/fasten-geometry.ts +2 -1
- package/client/modeller/figure-links.ts +87 -157
- package/client/modeller/figure-size-editor.ts +233 -0
- package/client/modeller/joint-drag.ts +25 -21
- package/client/modeller/joint-fan.ts +4 -3
- package/client/modeller/links.ts +87 -36
- package/client/modeller/live.ts +33 -0
- package/client/modeller/motion-ends.ts +10 -6
- package/client/modeller/size-response.ts +95 -0
- package/dist-client/modeller/data-interface.d.ts +1 -1
- package/dist-client/modeller/data-interface.js +5 -5
- package/dist-client/modeller/data-interface.js.map +1 -1
- package/dist-client/modeller/editor.d.ts +14 -1
- package/dist-client/modeller/editor.js +103 -89
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/fasten-geometry.js +1 -1
- package/dist-client/modeller/fasten-geometry.js.map +1 -1
- package/dist-client/modeller/figure-links.d.ts +3 -4
- package/dist-client/modeller/figure-links.js +91 -145
- package/dist-client/modeller/figure-links.js.map +1 -1
- package/dist-client/modeller/figure-size-editor.d.ts +29 -0
- package/dist-client/modeller/figure-size-editor.js +262 -0
- package/dist-client/modeller/figure-size-editor.js.map +1 -0
- package/dist-client/modeller/joint-drag.d.ts +1 -1
- package/dist-client/modeller/joint-drag.js +27 -22
- package/dist-client/modeller/joint-drag.js.map +1 -1
- package/dist-client/modeller/joint-fan.js +4 -3
- package/dist-client/modeller/joint-fan.js.map +1 -1
- package/dist-client/modeller/links.d.ts +21 -9
- package/dist-client/modeller/links.js +85 -35
- package/dist-client/modeller/links.js.map +1 -1
- package/dist-client/modeller/live.d.ts +14 -0
- package/dist-client/modeller/live.js +31 -0
- package/dist-client/modeller/live.js.map +1 -1
- package/dist-client/modeller/motion-ends.d.ts +4 -1
- package/dist-client/modeller/motion-ends.js +10 -6
- package/dist-client/modeller/motion-ends.js.map +1 -1
- package/dist-client/modeller/size-response.d.ts +29 -0
- package/dist-client/modeller/size-response.js +99 -0
- package/dist-client/modeller/size-response.js.map +1 -0
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/docs/evidence/2026-10-24/current-size-panel.png +0 -0
- package/docs/evidence/2026-10-24/integrated-size.png +0 -0
- package/docs/evidence/2026-10-24/multipart-response.png +0 -0
- package/docs/evidence/2026-10-24/repeat-layout.png +0 -0
- package/docs/evidence/2026-10-24/repeat-shared-style.png +0 -0
- package/docs/evidence/2026-10-24/size-in-layout.png +0 -0
- package/docs/evidence/2026-10-24/size-shared-style.png +0 -0
- package/docs/evidence/2026-10-24/width-preview.png +0 -0
- package/docs/multipart-response-review-2026-10-24.md +77 -0
- package/docs/response-authoring-2026-10-24.md +46 -0
- package/docs/size-and-repeat-ux-2026-10-24.md +67 -0
- package/package.json +7 -7
- package/test/attachment-size-contract.test.ts +47 -0
- package/test/browser/acceptance-page.js +1 -1
- package/test/browser/detail-page.js +1 -1
- package/test/browser/detail-tabs-page.js +1 -1
- package/test/browser/editing-page.js +1 -1
- package/test/browser/editing.check.mjs +66 -14
- package/test/browser/links.check.mjs +41 -22
- package/test/browser/modeller-page.js +1 -1
- package/test/browser/modeller-revision.check.mjs +21 -7
- package/test/browser/modeller.check.mjs +106 -39
- package/test/browser/multipart-response-page.js +8 -0
- package/test/browser/parts.check.mjs +2 -1
- package/test/browser/product-page.js +1 -1
- package/test/browser/props.check.mjs +17 -14
- package/test/browser/response-page.js +12 -0
- package/test/browser/row-preview.check.mjs +3 -3
- package/test/browser/screen.check.mjs +57 -13
- package/test/browser/serve-response.mjs +24 -0
- package/test/clip-ai-proposal.test.ts +1 -1
- package/test/clip-consent.test.ts +2 -2
- package/test/command-preview.test.ts +7 -7
- package/test/joint-drag.test.ts +6 -6
- package/test/joint-frames.test.ts +33 -32
- package/test/joints.ts +1 -6
- package/test/links.test.ts +60 -20
- package/test/multipart-response-fixture.js +17 -0
- package/test/multipart-response.test.ts +37 -0
- package/test/refusals.test.ts +2 -2
- package/test/resize-with-placement.test.ts +23 -1
- package/test/size-response.test.ts +49 -0
- package/test/size-source-pairing.test.ts +66 -0
- package/translations/en.json +47 -10
- package/translations/es.json +47 -10
- package/translations/ja.json +47 -10
- package/translations/ko.json +52 -15
- package/translations/ms.json +47 -10
- package/translations/zh.json +47 -10
|
@@ -82,6 +82,8 @@ const tabOn = () => read(() => screenCheck.editor().shadowRoot.querySelector('bu
|
|
|
82
82
|
const pressBar = action => press(action, a => screenCheck.editor().shadowRoot.querySelector('ox-canvas-bar').shadowRoot.querySelector(`button[data-action=${a}]`), action)
|
|
83
83
|
const pressTab = name => press(name, name => screenCheck.editor().shadowRoot.querySelector(`button[tab][aria-label="${name}"]`), name)
|
|
84
84
|
const TAB = name => `figure.label.tab-short-${name}`
|
|
85
|
+
/* 「반응형」 while a part is chosen: named by what it holds for the part, its size and arrangement (2026-10-24). */
|
|
86
|
+
const ARRANGEMENT = 'figure.label.tab-arrangement'
|
|
85
87
|
/*
|
|
86
88
|
The view settings or the releases, opened from the bar over the work (they were tabs once); left open if open. The
|
|
87
89
|
releases open from 「발행」, the bar's one way to them (user 2026-10-18).
|
|
@@ -226,7 +228,9 @@ describe('the frame: parts · canvas · one side of seven tabs, and the bar over
|
|
|
226
228
|
assert.equal(await saveOn(), false, 'saved: the save button is dimmed')
|
|
227
229
|
|
|
228
230
|
await pressRow('box')
|
|
229
|
-
|
|
231
|
+
/* A part's size is the 「배치」 tab's (2026-10-24). */
|
|
232
|
+
await pressTab(ARRANGEMENT)
|
|
233
|
+
const height = await element('the height field', () => screenCheck.deep('input').find(i => i.getAttribute('aria-label') === 'box Y figure.label.size-measure'))
|
|
230
234
|
await height.click({ clickCount: 3 })
|
|
231
235
|
await page().keyboard.type('600')
|
|
232
236
|
await page().keyboard.press('Enter')
|
|
@@ -240,6 +244,7 @@ describe('the frame: parts · canvas · one side of seven tabs, and the bar over
|
|
|
240
244
|
await until('redone', () => probe.value('box.size.y') === 600)
|
|
241
245
|
await press('save', () => screenCheck.editor().shadowRoot.querySelector('ox-canvas-bar button[save]'))
|
|
242
246
|
await until('saved: dimmed again', () => screenCheck.editor().shadowRoot.querySelector('ox-canvas-bar button[save]').disabled)
|
|
247
|
+
await pressTab(TAB('properties'))
|
|
243
248
|
|
|
244
249
|
const width = () => read(() => Math.round(screenCheck.deep('figure-canvas')[0].getBoundingClientRect().width))
|
|
245
250
|
const was = await width()
|
|
@@ -316,7 +321,7 @@ describe('the editor is drawn in the shell\'s tokens (2026-09-27)', () => {
|
|
|
316
321
|
describe('a part chosen: each tab in its place, the figure-wide tabs give way, the list\'s root goes back', () => {
|
|
317
322
|
before(() => mount('arm'))
|
|
318
323
|
|
|
319
|
-
test('properties hold the part\'s
|
|
324
|
+
test('properties hold the part\'s place; 「반응형」 its size and how it answers one; capability its places; motion its motion and controls -- each under the part\'s head', async () => {
|
|
320
325
|
await pressRow('upper-arm')
|
|
321
326
|
await until('upper-arm chosen', () => screenCheck.editor().selected === 'upper-arm')
|
|
322
327
|
const holds = () =>
|
|
@@ -334,10 +339,11 @@ describe('a part chosen: each tab in its place, the figure-wide tabs give way, t
|
|
|
334
339
|
await pressTab(TAB('properties'))
|
|
335
340
|
let now = await holds()
|
|
336
341
|
assert.ok(now.subject, 'the part\'s head')
|
|
337
|
-
|
|
342
|
+
/* A box's size is the 「배치」 tab's, in its size editor (2026-10-24): the properties keep its place. */
|
|
343
|
+
assert.ok(now.links.includes('figure.label.group-place') && !now.links.includes('figure.label.group-size'), `the shape's links: ${now.links}`)
|
|
338
344
|
assert.ok(now.material, 'its material')
|
|
339
|
-
/* How it answers a size has a tab of its own (user 2026-10-17, 「반응형」). */
|
|
340
|
-
await pressTab(
|
|
345
|
+
/* How it answers a size has a tab of its own (user 2026-10-17, 「반응형」), named 「배치」 while a part is chosen. */
|
|
346
|
+
await pressTab(ARRANGEMENT)
|
|
341
347
|
now = await holds()
|
|
342
348
|
assert.ok(now.subject, 'the part\'s head')
|
|
343
349
|
assert.ok(now.links.includes('figure.label.group-size'), `the response's links: ${now.links}`)
|
|
@@ -726,6 +732,8 @@ describe('the camera follows a size typed far from the view (first 15 minutes, 2
|
|
|
726
732
|
test('a width typed ten times larger fits the view again; a little wider leaves the camera where it is', async () => {
|
|
727
733
|
await mount('box')
|
|
728
734
|
await pressRow('box')
|
|
735
|
+
/* A part's size is the 「배치」 tab's (2026-10-24). */
|
|
736
|
+
await pressTab(ARRANGEMENT)
|
|
729
737
|
await read(() => {
|
|
730
738
|
const canvas = screenCheck.deep('figure-canvas')[0]
|
|
731
739
|
const fit = canvas.fit.bind(canvas)
|
|
@@ -733,7 +741,7 @@ describe('the camera follows a size typed far from the view (first 15 minutes, 2
|
|
|
733
741
|
canvas.fit = () => (window.fits++, fit())
|
|
734
742
|
})
|
|
735
743
|
const typeWidth = async mm => {
|
|
736
|
-
const width = await element('the width field', () => screenCheck.deep('input').find(i => i.getAttribute('aria-label') === 'box.size
|
|
744
|
+
const width = await element('the width field', () => screenCheck.deep('input').find(i => i.getAttribute('aria-label') === 'box X figure.label.size-measure'))
|
|
737
745
|
await width.click({ clickCount: 3 })
|
|
738
746
|
await page().keyboard.type(String(mm))
|
|
739
747
|
await page().keyboard.press('Enter')
|
|
@@ -745,6 +753,7 @@ describe('the camera follows a size typed far from the view (first 15 minutes, 2
|
|
|
745
753
|
assert.equal(await read(() => window.fits), 0, 'a little wider: the camera stays')
|
|
746
754
|
await typeWidth(4000)
|
|
747
755
|
await until('the view fitted to it', () => window.fits >= 1)
|
|
756
|
+
await pressTab(TAB('properties'))
|
|
748
757
|
await boardsUp()
|
|
749
758
|
})
|
|
750
759
|
})
|
|
@@ -796,16 +805,25 @@ describe('a part\'s size row: a rule\'s numbers only where a rule sets every sid
|
|
|
796
805
|
assert.equal(await sizeRow(), null)
|
|
797
806
|
})
|
|
798
807
|
|
|
808
|
+
/*
|
|
809
|
+
A box's size is the 「배치」 tab's size editor (2026-10-24), one row a side: made to follow on every side, each row
|
|
810
|
+
shows the size the rule gives it, and the properties draw no size row.
|
|
811
|
+
*/
|
|
799
812
|
test('a box made to follow the figure\'s size on every side shows the size the rule gives it', async () => {
|
|
800
813
|
await mount('box')
|
|
801
814
|
await pressRow('box')
|
|
802
815
|
await until('box chosen', () => screenCheck.editor().selected === 'box')
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
816
|
+
await pressTab(ARRANGEMENT)
|
|
817
|
+
for (const axis of ['X', 'Y', 'Z']) {
|
|
818
|
+
const before = await revision()
|
|
819
|
+
await press(`${axis} made to follow`, axis => screenCheck.deep('input').find(i => i.getAttribute('aria-label') === `box ${axis} figure.label.size-follows`), axis)
|
|
820
|
+
await until(`${axis} following`, before => screenCheck.editor().session.read().revision > before, before)
|
|
821
|
+
}
|
|
806
822
|
await until('following', () => ['x', 'y', 'z'].every(axis => !screenCheck.editor().session.read().fields.some(f => f.id === `box.size.${axis}`)))
|
|
823
|
+
const measures = await read(() => ['X', 'Y', 'Z'].map(axis => screenCheck.deep('input').find(i => i.getAttribute('aria-label') === `box ${axis} figure.label.size-measure`)?.value))
|
|
824
|
+
assert.deepEqual(measures, ['400', '400', '400'], 'each side the size the rule gives it')
|
|
807
825
|
await pressTab(TAB('properties'))
|
|
808
|
-
assert.
|
|
826
|
+
assert.equal(await sizeRow(), null, 'no size row on the properties')
|
|
809
827
|
})
|
|
810
828
|
})
|
|
811
829
|
|
|
@@ -829,7 +847,20 @@ describe('the side panel is found as it was left, per part', () => {
|
|
|
829
847
|
}
|
|
830
848
|
before(() => mount('arm'))
|
|
831
849
|
|
|
850
|
+
/*
|
|
851
|
+
The part's properties fit a tall window since its size moved to 「배치」 (2026-10-24): a shorter one gives the panel
|
|
852
|
+
something to scroll.
|
|
853
|
+
*/
|
|
832
854
|
test('scrolled and a fold opened on one part; another part opens at its top; back, the first is as it was left', async () => {
|
|
855
|
+
const viewport = page().viewport()
|
|
856
|
+
await page().setViewport({ ...viewport, height: 600 })
|
|
857
|
+
try {
|
|
858
|
+
await scrolledAndBack()
|
|
859
|
+
} finally {
|
|
860
|
+
await page().setViewport(viewport)
|
|
861
|
+
}
|
|
862
|
+
})
|
|
863
|
+
async function scrolledAndBack() {
|
|
833
864
|
await pressRow('upper-arm')
|
|
834
865
|
await until('upper-arm chosen', () => screenCheck.editor().selected === 'upper-arm')
|
|
835
866
|
/* 「더 보기」 starts open on this page: folded first, so the fold opened below is one this part leaves open. */
|
|
@@ -854,7 +885,7 @@ describe('the side panel is found as it was left, per part', () => {
|
|
|
854
885
|
await pressRow('upper-arm')
|
|
855
886
|
await until('upper-arm as it was left', top => screenCheck.editor().selected === 'upper-arm' && Math.abs(screenCheck.shownPanel().scrollTop - top) <= 2, top)
|
|
856
887
|
assert.deepEqual(await folds(), opened, 'its folds as they were')
|
|
857
|
-
}
|
|
888
|
+
}
|
|
858
889
|
})
|
|
859
890
|
|
|
860
891
|
describe('the fastening window says whether it changes the fastening the part has or fastens it anew', () => {
|
|
@@ -1075,12 +1106,15 @@ describe('the modeller page: the editor with the page\'s figure panel, verdict a
|
|
|
1075
1106
|
|
|
1076
1107
|
test('an edit is judged as it stands on screen: the source the server is sent is the one edited', async () => {
|
|
1077
1108
|
await pressRow('left')
|
|
1078
|
-
|
|
1109
|
+
/* A part's size is the 「배치」 tab's (2026-10-24). */
|
|
1110
|
+
await pressTab(ARRANGEMENT)
|
|
1111
|
+
const height = await element('the height field', () => screenCheck.deep('input').find(i => i.getAttribute('aria-label') === 'left Y figure.label.size-measure'))
|
|
1079
1112
|
await height.click({ clickCount: 3 })
|
|
1080
1113
|
await page().keyboard.type('600')
|
|
1081
1114
|
await page().keyboard.press('Enter')
|
|
1082
1115
|
await until('the edit made', () => screenCheck.editor().session.read().fields.find(f => f.id === 'left.size.y')?.value === 600)
|
|
1083
1116
|
await until('the edited source judged', () => product.calls().filter(c => c.field === 'inspectFigureDraft').at(-1)?.variables.source === screenCheck.editor().session.revisionNow().text)
|
|
1117
|
+
await pressTab(TAB('properties'))
|
|
1084
1118
|
})
|
|
1085
1119
|
|
|
1086
1120
|
test('a save sends the card\'s picture with the figure', async () => {
|
|
@@ -1114,7 +1148,17 @@ describe('the modeller page: the editor with the page\'s figure panel, verdict a
|
|
|
1114
1148
|
}
|
|
1115
1149
|
})
|
|
1116
1150
|
|
|
1151
|
+
/* The part's properties fit a tall window since its size moved to 「배치」 (2026-10-24): a shorter one scrolls. */
|
|
1117
1152
|
test('another figure opened starts its panels clean: a part of the same name opens at its top', async () => {
|
|
1153
|
+
const viewport = page().viewport()
|
|
1154
|
+
await page().setViewport({ ...viewport, height: 600 })
|
|
1155
|
+
try {
|
|
1156
|
+
await anotherFigureClean()
|
|
1157
|
+
} finally {
|
|
1158
|
+
await page().setViewport(viewport)
|
|
1159
|
+
}
|
|
1160
|
+
})
|
|
1161
|
+
async function anotherFigureClean() {
|
|
1118
1162
|
await open('fig-p', 'arm-clip')
|
|
1119
1163
|
await pressRow('upper-arm')
|
|
1120
1164
|
await until('upper-arm chosen', () => screenCheck.editor().selected === 'upper-arm')
|
|
@@ -1136,7 +1180,7 @@ describe('the modeller page: the editor with the page\'s figure panel, verdict a
|
|
|
1136
1180
|
await until('upper-arm chosen', () => screenCheck.editor().selected === 'upper-arm')
|
|
1137
1181
|
for (let i = 0; i < 3; i++) await page().waitForFunction(() => new Promise(done => requestAnimationFrame(() => requestAnimationFrame(() => done(true)))))
|
|
1138
1182
|
assert.equal(await read(() => screenCheck.shownPanel().scrollTop), 0, 'not where the other figure\'s part was left')
|
|
1139
|
-
}
|
|
1183
|
+
}
|
|
1140
1184
|
|
|
1141
1185
|
test('a new figure\'s AI draft puts a request in the assistant\'s input, and sends nothing', async () => {
|
|
1142
1186
|
await read(() => assistant.forget())
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/* Manual browser verification. Uses buildPage's FIGURE_MODEL / THINGS_SCENE overrides when provided. */
|
|
2
|
+
import { buildPage } from './build.mjs'
|
|
3
|
+
import { createServer } from 'node:http'
|
|
4
|
+
import { readFileSync } from 'node:fs'
|
|
5
|
+
import { dirname, join } from 'node:path'
|
|
6
|
+
import { createRequire } from 'node:module'
|
|
7
|
+
const require = createRequire(import.meta.url)
|
|
8
|
+
const bundle = await buildPage(process.argv[2] ?? 'response-page.js')
|
|
9
|
+
const routes = new Map([
|
|
10
|
+
['/page.js', [bundle, 'application/javascript']],
|
|
11
|
+
['/ko.json', [new URL('../../translations/ko.json', import.meta.url), 'application/json']],
|
|
12
|
+
['/icons.woff2', [require.resolve('material-symbols/material-symbols-outlined.woff2'), 'font/woff2']],
|
|
13
|
+
['/workers/markdown-webworker.js', [join(dirname(bundle), 'workers/markdown-webworker.js'), 'application/javascript']]
|
|
14
|
+
])
|
|
15
|
+
const html = `<!doctype html><html lang="ko"><meta charset="utf-8"><title>반응형 치수 — 실제 편집기 로컬 검증</title><style>
|
|
16
|
+
@font-face{font-family:"Material Symbols Outlined";src:url(/icons.woff2)}
|
|
17
|
+
html,body{margin:0;height:100%;font-family:sans-serif}body{display:flex}figure-editor{flex:1;min-width:0;min-height:0}
|
|
18
|
+
</style><body><script type="module" src="/page.js"></script></body></html>`
|
|
19
|
+
createServer((req, res) => {
|
|
20
|
+
const file = routes.get(req.url)
|
|
21
|
+
if (!file && req.url !== '/') { res.writeHead(404); res.end(); return }
|
|
22
|
+
res.setHeader('Content-Type', file ? file[1] : 'text/html; charset=utf-8')
|
|
23
|
+
res.end(file ? readFileSync(file[0]) : html)
|
|
24
|
+
}).listen(Number(process.env.PORT ?? 4391), '127.0.0.1', function () { console.log(`Ready http://127.0.0.1:${this.address().port}/`) })
|
|
@@ -23,7 +23,7 @@ function door(): string {
|
|
|
23
23
|
a = addFigureBox(a, { id: 'sign', size: { x: 200, y: 100, z: 10 }, position: { x: 0, y: 2200, z: 200 } })
|
|
24
24
|
a = applyFigureAuthoring(a, { kind: 'attach', part: 'leaf', to: { part: 'frame', face: 'top' } })
|
|
25
25
|
const j = a.joints!.find(k => k.body1.part === 'leaf')!
|
|
26
|
-
a = applyFigureAuthoring(a, { kind: 'set-joint', joint: { ...j, type: 'revolute', axis: 'Y', state: { id: 'open' } } })
|
|
26
|
+
a = applyFigureAuthoring(a, { kind: 'set-joint', joint: { ...j, motion: [{ type: 'revolute', axis: 'Y', state: { id: 'open' } }] } })
|
|
27
27
|
a = editFigurePart(a, { kind: 'set-driver', state: 'open', clip: 'swing', duration: 2, keys: [{ seconds: 0, value: 0 }, { seconds: 2, value: 80 }] })
|
|
28
28
|
return serializeFigureAsset(a)
|
|
29
29
|
}
|
|
@@ -22,7 +22,7 @@ function doors(): string {
|
|
|
22
22
|
a = addFigureBox(a, { id, size: { x: 900, y: 1800, z: 40 }, position: { x, y: 1000, z: 0 } })
|
|
23
23
|
a = applyFigureAuthoring(a, { kind: 'attach', part: id, to: { part: 'frame', face: 'top' } })
|
|
24
24
|
const j = a.joints!.find(k => k.body1.part === id)!
|
|
25
|
-
a = applyFigureAuthoring(a, { kind: 'set-joint', joint: { ...j, type: 'revolute', axis: 'Y', state: { id: `${id}-open` } } })
|
|
25
|
+
a = applyFigureAuthoring(a, { kind: 'set-joint', joint: { ...j, motion: [{ type: 'revolute', axis: 'Y', state: { id: `${id}-open` } }] } })
|
|
26
26
|
a = editFigurePart(a, { kind: 'set-driver', state: `${id}-open`, clip: `${id}-swing`, duration: 2, keys: [{ seconds: 0, value: 0 }, { seconds: 2, value: 80 }] })
|
|
27
27
|
}
|
|
28
28
|
return serializeFigureAsset(a)
|
|
@@ -31,7 +31,7 @@ const store = { load: async (id: string) => ({ id, source: doors() }), save: asy
|
|
|
31
31
|
const clipsOf = (s: ReturnType<typeof createFigureProductSession>) => (parseFigureAsset(s.source()) as FigureAsset).drivers?.map(d => d.clip) ?? []
|
|
32
32
|
const fixed = (s: ReturnType<typeof createFigureProductSession>, part: string) => {
|
|
33
33
|
const j = (parseFigureAsset(s.source()) as FigureAsset).joints!.find(k => k.body1.part === part)!
|
|
34
|
-
return { kind: 'set-joint', joint: { id: j.id,
|
|
34
|
+
return { kind: 'set-joint', joint: { id: j.id, body0: j.body0, body1: j.body1, origin: j.origin } } as never
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
test('refused and named first; agreed, exactly those go in one step; undo brings them back; consent does not outlive it', async () => {
|
|
@@ -161,8 +161,8 @@ test('a motion is named by its parts, and drawn at both ends of its range -- onl
|
|
|
161
161
|
a = applyFigureAuthoring(a, { kind: 'attach', part: 'gate', to: { part: 'frame', face: 'top' } })
|
|
162
162
|
const j = (part: string) => a.joints!.find(k => k.body1.part === part)!
|
|
163
163
|
assert.equal(motionEndsOf(revisionOf(a), 'leaf'), null, 'not moving: nothing drawn')
|
|
164
|
-
a = applyFigureAuthoring(a, { kind: 'set-joint', joint: { ...j('leaf'), type: 'revolute', axis: 'Y', lowerLimit: 0, upperLimit: 90, state: { id: 'open' } } })
|
|
165
|
-
a = applyFigureAuthoring(a, { kind: 'set-joint', joint: { ...j('gate'), type: 'prismatic', axis: 'X', lowerLimit: 0, upperLimit: 300, state: { id: 'slide' } } })
|
|
164
|
+
a = applyFigureAuthoring(a, { kind: 'set-joint', joint: { ...j('leaf'), motion: [{ type: 'revolute', axis: 'Y', lowerLimit: 0, upperLimit: 90, state: { id: 'open' } }] } })
|
|
165
|
+
a = applyFigureAuthoring(a, { kind: 'set-joint', joint: { ...j('gate'), motion: [{ type: 'prismatic', axis: 'X', lowerLimit: 0, upperLimit: 300, state: { id: 'slide' } }] } })
|
|
166
166
|
/* At the version the screen holds, a design value typed since it was opened. */
|
|
167
167
|
const held = revisionOf(a, { 'frame.size.x': 2400 })
|
|
168
168
|
const turn = motionEndsOf(held, 'leaf')!
|
|
@@ -204,19 +204,19 @@ test('a moving part is made into a row, and a row given or relieved of a motion
|
|
|
204
204
|
assert.deepEqual(rowAxesOf(now(), 'roller'), ['x', 'z'])
|
|
205
205
|
s.command({ kind: 'make-repeat', part: 'roller', axis: 'x', pitch: 200 })
|
|
206
206
|
assert.equal(rollers(), 10)
|
|
207
|
-
assert.equal(jointOf().type, 'revolute')
|
|
207
|
+
assert.equal((jointOf().motion?.[0]?.type ?? 'fixed'), 'revolute')
|
|
208
208
|
/* The motion changed by the form: the row stays. */
|
|
209
209
|
s.command({ kind: 'set-joint', joint: motionJoint(jointOf(), { ...(turn as object), kind: 'slide', axis: 'z', min: 0, max: 100 } as never, 'roller') })
|
|
210
|
-
assert.deepEqual([jointOf().type, rollers()], ['prismatic', 10])
|
|
210
|
+
assert.deepEqual([(jointOf().motion?.[0]?.type ?? 'fixed'), rollers()], ['prismatic', 10])
|
|
211
211
|
/* The motion taken away by its button: still a row. */
|
|
212
212
|
s.command({ kind: 'set-joint', joint: fixedJointOf(jointOf()) })
|
|
213
|
-
assert.deepEqual([jointOf().type, rollers()], ['fixed', 10])
|
|
213
|
+
assert.deepEqual([(jointOf().motion?.[0]?.type ?? 'fixed'), rollers()], ['fixed', 10])
|
|
214
214
|
/* Each undone once. */
|
|
215
215
|
s.undo()
|
|
216
|
-
assert.deepEqual([jointOf().type, rollers()], ['prismatic', 10])
|
|
216
|
+
assert.deepEqual([(jointOf().motion?.[0]?.type ?? 'fixed'), rollers()], ['prismatic', 10])
|
|
217
217
|
s.undo()
|
|
218
218
|
s.undo()
|
|
219
|
-
assert.deepEqual([jointOf().type, rollers()], ['revolute', 1])
|
|
219
|
+
assert.deepEqual([(jointOf().motion?.[0]?.type ?? 'fixed'), rollers()], ['revolute', 1])
|
|
220
220
|
})
|
|
221
221
|
|
|
222
222
|
/* Designer 2026-09-27: wider than its face is not hidden -- a fact in the relation's details, off to one side said. */
|
package/test/joint-drag.test.ts
CHANGED
|
@@ -17,7 +17,7 @@ function column(joint: Partial<FigureJoint>): FigureAsset {
|
|
|
17
17
|
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'post', to: { plane: 'mounting-plane' } })
|
|
18
18
|
return applyFigureAuthoring(asset, {
|
|
19
19
|
kind: 'set-joint',
|
|
20
|
-
joint: { id: 'bend',
|
|
20
|
+
joint: { id: 'bend', motion: undefined, body0: { part: 'post', face: 'top' }, body1: { part: 'link' }, ...joint } as FigureJoint
|
|
21
21
|
})
|
|
22
22
|
}
|
|
23
23
|
|
|
@@ -28,7 +28,7 @@ const drawn = (asset: FigureAsset, id: string, state: Record<string, number> = {
|
|
|
28
28
|
const turnedZ = (deg: number) => [0, 0, deg] as [number, number, number]
|
|
29
29
|
|
|
30
30
|
test('돌리면 관절값 — 관절 축 둘레로 돈 만큼, 범위 안으로', () => {
|
|
31
|
-
const asset = column({ type: 'revolute', axis: 'Z', lowerLimit: -90, upperLimit: 90 })
|
|
31
|
+
const asset = column({ motion: [{ type: 'revolute', axis: 'Z', lowerLimit: -90, upperLimit: 90 }] })
|
|
32
32
|
const part = drawn(asset, 'link')
|
|
33
33
|
assert.deepEqual(jointDragOf(asset, 'link', part as never, { rotation: turnedZ(30) }, {}), { kind: 'value', input: 'bend', unit: 'deg', value: 30 })
|
|
34
34
|
assert.deepEqual(jointDragOf(asset, 'link', part as never, { rotation: turnedZ(120) }, {}), { kind: 'value', input: 'bend', unit: 'deg', value: 90 }, '범위 끝에서 멈춘다')
|
|
@@ -38,7 +38,7 @@ test('돌리면 관절값 — 관절 축 둘레로 돈 만큼, 범위 안으로'
|
|
|
38
38
|
})
|
|
39
39
|
|
|
40
40
|
test('끝없이 도는 관절은 −180..180 으로 감는다', () => {
|
|
41
|
-
const asset = column({ type: 'revolute', axis: 'Y' })
|
|
41
|
+
const asset = column({ motion: [{ type: 'revolute', axis: 'Y' }] })
|
|
42
42
|
const at170 = drawn(asset, 'link', { bend: 170 })
|
|
43
43
|
const drag = jointDragOf(asset, 'link', at170 as never, { rotation: [0, 190, 0] }, { bend: 170 })
|
|
44
44
|
assert.equal(drag?.kind, 'value')
|
|
@@ -46,7 +46,7 @@ test('끝없이 도는 관절은 −180..180 으로 감는다', () => {
|
|
|
46
46
|
})
|
|
47
47
|
|
|
48
48
|
test('미끄러지는 관절은 축을 따라 옮긴 거리가 값', () => {
|
|
49
|
-
const asset = column({ type: 'prismatic', axis: 'X', lowerLimit: -100, upperLimit: 100 })
|
|
49
|
+
const asset = column({ motion: [{ type: 'prismatic', axis: 'X', lowerLimit: -100, upperLimit: 100 }] })
|
|
50
50
|
const part = drawn(asset, 'link')
|
|
51
51
|
const to = [part.pose.t[0]! + 40, part.pose.t[1]!, part.pose.t[2]!] as [number, number, number]
|
|
52
52
|
assert.deepEqual(jointDragOf(asset, 'link', part as never, { position: to }, {}), { kind: 'value', input: 'bend', unit: 'mm', value: 40 })
|
|
@@ -76,7 +76,7 @@ test('관계를 끊는 끌기는 거절하고 까닭을 댄다', () => {
|
|
|
76
76
|
const edge = column({ origin: { align: { x: 'min' } } })
|
|
77
77
|
const part = drawn(edge, 'link')
|
|
78
78
|
assert.deepEqual(jointDragOf(edge, 'link', part as never, { position: [part.pose.t[0]! + 30, part.pose.t[1]!, part.pose.t[2]!] }, {}), { kind: 'refused', reason: 'origin-follows', joint: 'bend' }, '모서리에 맞춘 자리는 그 모서리를 따른다')
|
|
79
|
-
const bend = column({ type: 'revolute', axis: 'Z', lowerLimit: -90, upperLimit: 90 })
|
|
79
|
+
const bend = column({ motion: [{ type: 'revolute', axis: 'Z', lowerLimit: -90, upperLimit: 90 }] })
|
|
80
80
|
assert.deepEqual(jointDragOf(bend, 'link', drawn(bend, 'link') as never, { rotation: [25, 0, 0] }, {}), { kind: 'refused', reason: 'off-axis', joint: 'bend', axis: 'Z' }, '다른 축으로 돌린 것은 적지 않는다')
|
|
81
81
|
const still = column({})
|
|
82
82
|
assert.deepEqual(jointDragOf(still, 'link', drawn(still, 'link') as never, { rotation: turnedZ(20) }, {}), { kind: 'refused', reason: 'fixed-turn', joint: 'bend' })
|
|
@@ -90,7 +90,7 @@ test('관절이 없는 부품은 이 길로 오지 않는다 — 제 숫자를
|
|
|
90
90
|
|
|
91
91
|
test('크기 끌기는 관절이 아니라 부품의 치수 — 따라가는 치수는 두고 제 치수만 쓴다 (사용자 09-25: 「R 모드에서 크기가 변하지 않는다」)', async () => {
|
|
92
92
|
const { gizmoChanges, gizmoReachOf } = await import('../client/modeller/gizmo.ts')
|
|
93
|
-
let asset = column({ type: 'revolute', axis: 'Z', lowerLimit: -90, upperLimit: 90 })
|
|
93
|
+
let asset = column({ motion: [{ type: 'revolute', axis: 'Z', lowerLimit: -90, upperLimit: 90 }] })
|
|
94
94
|
/* A square section: the link's depth follows its width. */
|
|
95
95
|
asset = applyFigureAuthoring(asset, { kind: 'link-dimension', part: 'link', axis: 'z', source: { of: 'part', part: 'link', axis: 'x' } })
|
|
96
96
|
const part = drawn(asset, 'link')
|
|
@@ -32,25 +32,25 @@ function framed(): FigureAsset {
|
|
|
32
32
|
a = box(a, 'link', 100, 400, 100, 300)
|
|
33
33
|
a = box(a, 'tool', 60, 120, 40)
|
|
34
34
|
a = box(a, 'cap', 20, 20, 20)
|
|
35
|
-
a = set(a, { id: 'base',
|
|
36
|
-
a = set(a, { id: 'link',
|
|
37
|
-
a = set(a, { id: 'tool',
|
|
38
|
-
return set(a, { id: 'cap',
|
|
35
|
+
a = set(a, { id: 'base', body0: { plane: 'mounting-plane' }, body1: { part: 'base' } })
|
|
36
|
+
a = set(a, { id: 'link', body0: { part: 'base', face: 'top' }, body1: { part: 'link' }, origin: { pivot: 'child-face', offset: { x: 20 }, parentRotate: { x: 10 } }, motion: [{ type: 'revolute', axis: { x: 1, y: 1, z: 0 }, lowerLimit: -90, upperLimit: 90 }] })
|
|
37
|
+
a = set(a, { id: 'tool', body0: { part: 'link', at: 'origin' }, body1: { part: 'tool', at: 'origin' }, origin: { offset: { x: 120, y: SUMMED, z: -30 }, parentRotate: { x: 15, y: -30, z: 60 } }, motion: [{ type: 'revolute', axis: 'Z', lowerLimit: -45, upperLimit: 45 }] })
|
|
38
|
+
return set(a, { id: 'cap', body0: { part: 'tool', at: 'origin' }, body1: { part: 'cap' }, origin: { offset: { y: 40 } } })
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
/** The old shapes only: a token axis, face ends, a gap and an edge. */
|
|
42
42
|
function plain(): FigureAsset {
|
|
43
43
|
let a = box(createFigureAsset('Plain'), 'base', 300, 100, 300, 50)
|
|
44
44
|
a = box(a, 'link', 100, 400, 100, 300)
|
|
45
|
-
a = set(a, { id: 'base',
|
|
46
|
-
return set(a, { id: 'link',
|
|
45
|
+
a = set(a, { id: 'base', body0: { plane: 'mounting-plane' }, body1: { part: 'base' } })
|
|
46
|
+
return set(a, { id: 'link', body0: { part: 'base', face: 'top' }, body1: { part: 'link' }, origin: { gap: 5, align: { x: 'min' } }, motion: [{ type: 'revolute', axis: 'Y', lowerLimit: -90, upperLimit: 90 }] })
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
const reopen = (asset: FigureAsset) => parseFigureAsset(serializeFigureAsset(asset) as unknown as string)
|
|
50
50
|
const jointOf = (asset: FigureAsset, id: string) => asset.joints!.find(j => j.id === id)! as Dynamic
|
|
51
51
|
/** What the frames say of a joint: what no form path may change. */
|
|
52
52
|
const frameOf = (j: Dynamic, keepType = true) => ({
|
|
53
|
-
...(keepType ? { type: j.type, axis: j.axis } : {}),
|
|
53
|
+
...(keepType ? { type: j.motion?.[0]?.type ?? 'fixed', axis: j.motion?.[0]?.axis } : {}),
|
|
54
54
|
body0: j.body0,
|
|
55
55
|
body1: j.body1,
|
|
56
56
|
pivot: j.origin?.pivot,
|
|
@@ -80,7 +80,7 @@ function roundTrips(asset: FigureAsset, id: string) {
|
|
|
80
80
|
const j = jointOf(asset, id)
|
|
81
81
|
const part = j.body1.part
|
|
82
82
|
const out: Record<string, FigureJoint> = {}
|
|
83
|
-
if (j.
|
|
83
|
+
if (j.motion?.length) out.motion = jointOf(reopen(set(asset, motionJoint(j, motionFormOf(j), part))), id)
|
|
84
84
|
const fasten = fasteningSave(asset, part)
|
|
85
85
|
if (fasten) out.fastening = jointOf(reopen(applyFigureAuthoring(asset, fasten)), id)
|
|
86
86
|
out.fixed = jointOf(reopen(set(asset, fixedJointOf(j))), id)
|
|
@@ -113,24 +113,25 @@ test('the axis reader: a token is its axis, a vector its unit vector, unrounded;
|
|
|
113
113
|
test('a follower is not a turn without end: it has no limits because its leader gives its range', () => {
|
|
114
114
|
let a = box(createFigureAsset('Pair'), 'a', 100, 100, 100, 50)
|
|
115
115
|
a = box(a, 'b', 50, 50, 50, 125)
|
|
116
|
-
a = set(a, { id: 'a',
|
|
117
|
-
a = set(a, { id: 'b',
|
|
116
|
+
a = set(a, { id: 'a', body0: { plane: 'mounting-plane' }, body1: { part: 'a' }, motion: [{ type: 'revolute', axis: 'Y', lowerLimit: -90, upperLimit: 90 }] })
|
|
117
|
+
a = set(a, { id: 'b', body0: { part: 'a', face: 'top' }, body1: { part: 'b' }, motion: [{ type: 'revolute', axis: 'Y', mimic: { motion: 'a', multiplier: -1 } }] })
|
|
118
118
|
assert.equal(motionOf(a, 'b')?.kind, 'turn')
|
|
119
119
|
assert.equal(motionFormOf(jointOf(a, 'b')).kind, 'turn')
|
|
120
120
|
assert.equal(motionOf(a, 'a')?.kind, 'turn')
|
|
121
|
-
const free = set(a, { id: 'a',
|
|
121
|
+
const free = set(a, { id: 'a', body0: { plane: 'mounting-plane' }, body1: { part: 'a' }, motion: [{ type: 'revolute', axis: 'Y' }] })
|
|
122
122
|
assert.equal(motionOf(free, 'a')?.kind, 'continuous', 'a joint of its own without limits still turns without end')
|
|
123
123
|
})
|
|
124
124
|
|
|
125
125
|
test('a new motion replaces the old one whole: made a follower, its own limits and control go; made still, its axis goes (ADR-0140 D6)', () => {
|
|
126
126
|
let a = box(createFigureAsset('Pair'), 'a', 100, 100, 100, 50)
|
|
127
127
|
a = box(a, 'b', 50, 50, 50, 125)
|
|
128
|
-
a = set(a, { id: 'a',
|
|
129
|
-
a = set(a, { id: 'b',
|
|
128
|
+
a = set(a, { id: 'a', body0: { plane: 'mounting-plane' }, body1: { part: 'a' }, motion: [{ type: 'revolute', axis: 'Y', lowerLimit: -90, upperLimit: 90 }] })
|
|
129
|
+
a = set(a, { id: 'b', body0: { part: 'a', face: 'top' }, body1: { part: 'b' }, origin: { gap: 5 }, motion: [{ type: 'revolute', axis: 'Y', lowerLimit: -30, upperLimit: 30, state: { id: 'b-turn' } }] })
|
|
130
130
|
const follower = motionJoint(jointOf(a, 'b'), { ...motionFormOf(jointOf(a, 'b')), follow: 'a', multiplier: -1 } as Dynamic, 'b') as Dynamic
|
|
131
|
-
assert.deepEqual(Object.keys(follower), ['id', 'body0', 'body1', 'origin', '
|
|
131
|
+
assert.deepEqual(Object.keys(follower), ['id', 'body0', 'body1', 'origin', 'motion'])
|
|
132
|
+
assert.deepEqual(Object.keys(follower.motion[0]), ['type', 'axis', 'mimic'], 'its own limits and control gone')
|
|
132
133
|
assert.doesNotThrow(() => set(a, follower))
|
|
133
|
-
assert.deepEqual(Object.keys(fixedJointOf(jointOf(a, 'b'))), ['id', '
|
|
134
|
+
assert.deepEqual(Object.keys(fixedJointOf(jointOf(a, 'b'))), ['id', 'body0', 'body1', 'origin'])
|
|
134
135
|
})
|
|
135
136
|
|
|
136
137
|
test('a range need not hold 0; it needs its most above its least', () => {
|
|
@@ -145,7 +146,7 @@ test('a range need not hold 0; it needs its most above its least', () => {
|
|
|
145
146
|
const still = set(plain(), fixedJointOf(jointOf(plain(), 'link')))
|
|
146
147
|
const fixed = jointOf(still, 'link')
|
|
147
148
|
const lifted = set(still, motionJoint(fixed, { ...motionFormOf(fixed), kind: 'turn', min: 10, max: 90 }, 'link'))
|
|
148
|
-
assert.equal(lifted.stateDefaults[jointOf(lifted, 'link').state.id], 10)
|
|
149
|
+
assert.equal(lifted.stateDefaults[jointOf(lifted, 'link').motion[0].state.id], 10)
|
|
149
150
|
/* An existing control keeps its name and its stored value: a range that leaves that value out is the kernel's refusal (R1). */
|
|
150
151
|
const j = jointOf(plain(), 'link')
|
|
151
152
|
assert.throws(() => set(plain(), motionJoint(j, { ...motionFormOf(j), min: 10, max: 90 }, 'link')), /INPUT_RANGE/)
|
|
@@ -170,8 +171,8 @@ test('a vector axis: the form keeps it, the summary says it, saved and reopened
|
|
|
170
171
|
assert.deepEqual(motionOf(asset, 'link')?.vector, { x: 1, y: 1, z: 0 })
|
|
171
172
|
/* Changing the range leaves the axis alone. */
|
|
172
173
|
const saved = reopen(set(asset, motionJoint(jointOf(asset, 'link'), { ...form, min: -30, max: 60 }, 'link')))
|
|
173
|
-
assert.deepEqual(jointOf(saved, 'link').axis, { x: 1, y: 1, z: 0 })
|
|
174
|
-
assert.equal(jointOf(saved, 'link').upperLimit, 60)
|
|
174
|
+
assert.deepEqual(jointOf(saved, 'link').motion[0].axis, { x: 1, y: 1, z: 0 })
|
|
175
|
+
assert.equal(jointOf(saved, 'link').motion[0].upperLimit, 60)
|
|
175
176
|
})
|
|
176
177
|
|
|
177
178
|
test('frames: every form path keeps the vector axis, pivot, offset (summed too), parentRotate and origin ends, saved and reopened', () => {
|
|
@@ -209,8 +210,8 @@ test('placed by an origin: said as the origin, no row offered, and the fastening
|
|
|
209
210
|
test('a drag about a vector axis sets the value; off it, it is refused and the vector said', () => {
|
|
210
211
|
let a = box(createFigureAsset('Tilt'), 'post', 100, 200, 100, 100)
|
|
211
212
|
a = box(a, 'head', 60, 60, 60, 230)
|
|
212
|
-
a = set(a, { id: 'post',
|
|
213
|
-
a = set(a, { id: 'head',
|
|
213
|
+
a = set(a, { id: 'post', body0: { plane: 'mounting-plane' }, body1: { part: 'post' } })
|
|
214
|
+
a = set(a, { id: 'head', body0: { part: 'post', face: 'top' }, body1: { part: 'head' }, motion: [{ type: 'revolute', axis: { x: 0, y: 3, z: 4 }, lowerLimit: -90, upperLimit: 90 }] })
|
|
214
215
|
const at = drawn(a, 'head')
|
|
215
216
|
/* 30° about (0, 0.6, 0.8), as XYZ degrees. */
|
|
216
217
|
const q = [0, 0.6, 0.8].map(v => v * Math.sin(Math.PI / 12))
|
|
@@ -223,7 +224,7 @@ test('a drag about a vector axis sets the value; off it, it is refused and the v
|
|
|
223
224
|
assert.deepEqual(jointDragOf(a, 'head', at as never, { rotation: deg }, {}), { kind: 'value', input: 'head', unit: 'deg', value: 30 })
|
|
224
225
|
assert.deepEqual(jointDragOf(a, 'head', at as never, { rotation: [40, 0, 0] }, {}), { kind: 'refused', reason: 'off-axis', joint: 'head', axis: '(0, 3, 4)' })
|
|
225
226
|
/* A slide along it: the distance along the vector. */
|
|
226
|
-
const slide = set(a, { ...jointOf(a, 'head'), type: 'prismatic', lowerLimit: -100, upperLimit: 100 })
|
|
227
|
+
const slide = set(a, { ...jointOf(a, 'head'), motion: [{ ...jointOf(a, 'head').motion[0], type: 'prismatic', lowerLimit: -100, upperLimit: 100 }] })
|
|
227
228
|
const s = drawn(slide, 'head')
|
|
228
229
|
assert.deepEqual(jointDragOf(slide, 'head', s as never, { position: [s.pose.t[0]!, s.pose.t[1]! + 30, s.pose.t[2]! + 40] }, {}), { kind: 'value', input: 'head', unit: 'mm', value: 50 })
|
|
229
230
|
})
|
|
@@ -231,8 +232,8 @@ test('a drag about a vector axis sets the value; off it, it is refused and the v
|
|
|
231
232
|
test('a rotated joint frame: a turn about its turned axis is its value, and a move is measured before the rotation', () => {
|
|
232
233
|
let a = box(createFigureAsset('Rotated'), 'post', 100, 200, 100, 100)
|
|
233
234
|
a = box(a, 'link', 60, 300, 60, 350)
|
|
234
|
-
a = set(a, { id: 'post',
|
|
235
|
-
a = set(a, { id: 'bend',
|
|
235
|
+
a = set(a, { id: 'post', body0: { plane: 'mounting-plane' }, body1: { part: 'post' } })
|
|
236
|
+
a = set(a, { id: 'bend', body0: { part: 'post', face: 'top' }, body1: { part: 'link' }, origin: { parentRotate: { x: 90 } }, motion: [{ type: 'revolute', axis: 'Z', lowerLimit: -90, upperLimit: 90 }] })
|
|
236
237
|
/* Turned 90° about X, the frame's Z is the figure's -Y: 20° about -Y is 20 on the joint. */
|
|
237
238
|
const at = drawn(a, 'link', { bend: 10 })
|
|
238
239
|
const before = at.pose.r
|
|
@@ -249,7 +250,7 @@ test('a rotated joint frame: a turn about its turned axis is its value, and a mo
|
|
|
249
250
|
const drag = jointDragOf(a, 'link', at as never, { rotation: [rx, ry, rz].map(v => (v * 180) / Math.PI) as [number, number, number] }, { bend: 10 })
|
|
250
251
|
assert.deepEqual(drag, { kind: 'value', input: 'bend', unit: 'deg', value: 30 })
|
|
251
252
|
/* Held still, moved 30 along X: an offset across the post's top on X, not along the turned frame. */
|
|
252
|
-
const still = set(a, { ...jointOf(a, 'bend'),
|
|
253
|
+
const still = set(a, { ...jointOf(a, 'bend'), motion: undefined })
|
|
253
254
|
const s = drawn(still, 'link')
|
|
254
255
|
const moved = jointDragOf(still, 'link', s as never, { position: [s.pose.t[0]!, s.pose.t[1]!, s.pose.t[2]! + 30] }, {}) as { joint: Dynamic }
|
|
255
256
|
assert.deepEqual(moved.joint.origin, { parentRotate: { x: 90 }, align: { z: { mm: 30 } } })
|
|
@@ -260,8 +261,8 @@ test('a rotated joint frame: a turn about its turned axis is its value, and a mo
|
|
|
260
261
|
test('a fan stands about a vector axis, as it does about the same token', () => {
|
|
261
262
|
let a = box(createFigureAsset('Fan'), 'base', 200, 100, 200, 50)
|
|
262
263
|
a = box(a, 'arm', 400, 50, 50, 125)
|
|
263
|
-
a = set(a, { id: 'base',
|
|
264
|
-
const arm = (axis: unknown) => set(a, { id: 'arm',
|
|
264
|
+
a = set(a, { id: 'base', body0: { plane: 'mounting-plane' }, body1: { part: 'base' } })
|
|
265
|
+
const arm = (axis: unknown) => set(a, { id: 'arm', body0: { part: 'base', face: 'top' }, body1: { part: 'arm' }, motion: [{ type: 'revolute', axis, lowerLimit: -45, upperLimit: 90 }] })
|
|
265
266
|
const token = jointFanOf(revisionOf(arm('Y')), 'arm') as never as { pose: { t: number[]; r: number[][] } }
|
|
266
267
|
const vector = jointFanOf(revisionOf(arm({ x: 0, y: 2, z: 0 })), 'arm') as never as { pose: { t: number[]; r: number[][] } }
|
|
267
268
|
assert.ok(token && vector, 'a fan for both')
|
|
@@ -280,8 +281,8 @@ test('editing a motion keeps the control named after the joint when it has no st
|
|
|
280
281
|
let a = createFigureAsset('Hinge')
|
|
281
282
|
a = addFigureBox(a, { id: 'base', size: { x: 200, y: 100, z: 200 }, position: { x: 0, y: 0, z: 0 } })
|
|
282
283
|
a = addFigureBox(a, { id: 'lid', size: { x: 200, y: 20, z: 200 }, position: { x: 0, y: 0, z: 0 } })
|
|
283
|
-
a = applyFigureAuthoring(a, { kind: 'set-joint', joint: { id: 'base',
|
|
284
|
-
a = applyFigureAuthoring(a, { kind: 'set-joint', joint: { id: 'lid',
|
|
284
|
+
a = applyFigureAuthoring(a, { kind: 'set-joint', joint: { id: 'base', body0: { plane: 'mounting-plane' }, body1: { part: 'base' } } })
|
|
285
|
+
a = applyFigureAuthoring(a, { kind: 'set-joint', joint: { id: 'lid', body0: { part: 'base', face: 'top' }, body1: { part: 'lid' }, motion: [{ type: 'revolute', axis: 'X', lowerLimit: -90, upperLimit: 90 }] } })
|
|
285
286
|
const lid = a.joints!.find(j => j.id === 'lid')!
|
|
286
287
|
const form = motionFormOf(lid)
|
|
287
288
|
assert.equal(form.id, 'lid', 'the form starts from the input the joint is driven by')
|
|
@@ -292,8 +293,8 @@ test('editing a motion keeps the control named after the joint when it has no st
|
|
|
292
293
|
test("a child posed on two axes (childRotate, Rz·Ry·Rx): a turn about the joint's axis is its value, whatever its pose", () => {
|
|
293
294
|
let a = box(createFigureAsset('Leaning'), 'post', 100, 200, 100, 100)
|
|
294
295
|
a = box(a, 'link', 60, 300, 60, 350)
|
|
295
|
-
a = set(a, { id: 'post',
|
|
296
|
-
a = set(a, { id: 'bend',
|
|
296
|
+
a = set(a, { id: 'post', body0: { plane: 'mounting-plane' }, body1: { part: 'post' } })
|
|
297
|
+
a = set(a, { id: 'bend', body0: { part: 'post', face: 'top' }, body1: { part: 'link' }, origin: { childRotate: { x: 30, z: 40 } }, motion: [{ type: 'revolute', axis: 'Y', lowerLimit: -90, upperLimit: 90 }] })
|
|
297
298
|
const at = drawn(a, 'link', { bend: 10 })
|
|
298
299
|
const after = drawn(a, 'link', { bend: 35 }).pose.r
|
|
299
300
|
const ry = Math.asin(Math.max(-1, Math.min(1, after[0]![2]!)))
|
|
@@ -306,9 +307,9 @@ test("a child posed on two axes (childRotate, Rz·Ry·Rx): a turn about the join
|
|
|
306
307
|
test("a child turned on its own end (childRotate): a turn about the joint's axis is its value -- the child's own turn is not the joint's", () => {
|
|
307
308
|
let a = box(createFigureAsset('Laid'), 'post', 100, 200, 100, 100)
|
|
308
309
|
a = box(a, 'link', 60, 300, 60, 350)
|
|
309
|
-
a = set(a, { id: 'post',
|
|
310
|
+
a = set(a, { id: 'post', body0: { plane: 'mounting-plane' }, body1: { part: 'post' } })
|
|
310
311
|
/* The link laid on its side on the post's top (a quarter turn about X), turning about the figure's Y. */
|
|
311
|
-
a = set(a, { id: 'bend',
|
|
312
|
+
a = set(a, { id: 'bend', body0: { part: 'post', face: 'top' }, body1: { part: 'link' }, origin: { childRotate: { x: 90 } }, motion: [{ type: 'revolute', axis: 'Y', lowerLimit: -90, upperLimit: 90 }] })
|
|
312
313
|
const at = drawn(a, 'link', { bend: 10 })
|
|
313
314
|
const before = at.pose.r
|
|
314
315
|
const c = Math.cos((20 * Math.PI) / 180),
|
package/test/joints.ts
CHANGED
|
@@ -14,7 +14,6 @@ export function moving(
|
|
|
14
14
|
const at = (axis: 'x' | 'z') => ({ mm: asset.designInputs[`${part}.position.${axis}`] ?? 0 })
|
|
15
15
|
const above: FigureJoint = asset.joints?.find(j => j.body1.part === part) ?? {
|
|
16
16
|
id: part,
|
|
17
|
-
type: 'fixed',
|
|
18
17
|
body0: { plane: 'mounting-plane' },
|
|
19
18
|
body1: { part },
|
|
20
19
|
origin: { align: { x: at('x'), z: at('z') } }
|
|
@@ -23,11 +22,7 @@ export function moving(
|
|
|
23
22
|
kind: 'set-joint',
|
|
24
23
|
joint: {
|
|
25
24
|
...above,
|
|
26
|
-
type: spec.kind === 'slide' ? 'prismatic' : 'revolute',
|
|
27
|
-
axis: spec.axis.toUpperCase() as 'X' | 'Y' | 'Z',
|
|
28
|
-
lowerLimit: spec.min,
|
|
29
|
-
upperLimit: spec.max,
|
|
30
|
-
state: { id: spec.id, ...(spec.start !== undefined ? { start: spec.start } : {}) }
|
|
25
|
+
motion: [{ type: spec.kind === 'slide' ? 'prismatic' : 'revolute', axis: spec.axis.toUpperCase() as 'X' | 'Y' | 'Z', lowerLimit: spec.min, upperLimit: spec.max, state: { id: spec.id, ...(spec.start !== undefined ? { start: spec.start } : {}) } }]
|
|
31
26
|
}
|
|
32
27
|
})
|
|
33
28
|
}
|