@things-factory/figure-ui 10.1.208 → 10.1.210
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 +74 -57
- package/client/modeller/fasten-geometry.ts +2 -1
- package/client/modeller/figure-links.ts +26 -120
- package/client/modeller/figure-size-editor.ts +196 -0
- package/client/modeller/joint-drag.ts +25 -21
- package/client/modeller/joint-fan.ts +4 -3
- package/client/modeller/links.ts +36 -24
- 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 +9 -1
- package/dist-client/modeller/editor.js +80 -58
- 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 +1 -4
- package/dist-client/modeller/figure-links.js +26 -107
- package/dist-client/modeller/figure-links.js.map +1 -1
- package/dist-client/modeller/figure-size-editor.d.ts +27 -0
- package/dist-client/modeller/figure-size-editor.js +221 -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 +9 -6
- package/dist-client/modeller/links.js +33 -21
- package/dist-client/modeller/links.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/route.d.ts +1 -1
- 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 +28 -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 +51 -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 +21 -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 +44 -10
- package/translations/es.json +44 -10
- package/translations/ja.json +44 -10
- package/translations/ko.json +49 -15
- package/translations/ms.json +44 -10
- package/translations/zh.json +44 -10
|
@@ -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
|
}
|
package/test/links.test.ts
CHANGED
|
@@ -67,7 +67,7 @@ test('움직임 — 미끄러짐 · 돌기, 축, 무엇이 모느냐를 읽는
|
|
|
67
67
|
const turning = moving(asset, 'block', { kind: 'turn', axis: 'y', id: 'swing', min: -90, max: 90, start: 0 })
|
|
68
68
|
assert.deepEqual([motionOf(turning, 'block')!.kind, motionOf(turning, 'block')!.axis], ['turn', 'y'])
|
|
69
69
|
const joint = sliding.joints!.find(j => j.body1.part === 'block')!
|
|
70
|
-
const still = applyFigureAuthoring(sliding, { kind: 'set-joint', joint: { id: joint.id,
|
|
70
|
+
const still = applyFigureAuthoring(sliding, { kind: 'set-joint', joint: { id: joint.id, body0: joint.body0, body1: joint.body1 } })
|
|
71
71
|
assert.equal(motionOf(still, 'block'), undefined, 'a fixed joint does not move')
|
|
72
72
|
})
|
|
73
73
|
|
|
@@ -256,7 +256,7 @@ test('한 비율로 커지는 도형 — 자기 숫자인 길이를 세고, 한
|
|
|
256
256
|
asset = applyFigureAuthoring(asset, { kind: 'scale-uniformly' })
|
|
257
257
|
assert.deepEqual(proportionToFollow(asset), { count: 3 + 3 + 1, kept: [] }, '치수 여섯과 자리 하나')
|
|
258
258
|
const lamp = asset.joints!.find(j => j.id === 'lamp')!
|
|
259
|
-
asset = applyFigureAuthoring(asset, { kind: 'set-joint', joint: { ...lamp, type: 'prismatic', axis: 'Y', lowerLimit: 0, upperLimit: 50, state: { id: 'rise' } } })
|
|
259
|
+
asset = applyFigureAuthoring(asset, { kind: 'set-joint', joint: { ...lamp, motion: [{ type: 'prismatic', axis: 'Y', lowerLimit: 0, upperLimit: 50, state: { id: 'rise' } }] } })
|
|
260
260
|
asset = applyFigureAuthoring(asset, { kind: 'follow-size-in-proportion' })
|
|
261
261
|
assert.deepEqual(proportionToFollow(asset), { count: 0, kept: ['lamp.motion'] }, '다 따르고, mm 로 움직이는 것만 남는다')
|
|
262
262
|
assert.equal(Math.round(valueNow(revisionOf(asset), 'lamp.dimension.x.value')! * 1e6) / 1e6, 60, '따르게 해도 지금 크기는 그대로')
|
|
@@ -359,9 +359,9 @@ test('따라 움직이는 관절 — 같은 방식으로 움직이고 제 조작
|
|
|
359
359
|
assert.deepEqual(leadersFor(asset, 'left', 'slide'), [], '제 관절은 고르지 않는다')
|
|
360
360
|
|
|
361
361
|
const follower = motionJoint(jointOf('right'), { ...form, id: 'ignored', follow: 'left', multiplier: -1 }, 'right')
|
|
362
|
-
assert.deepEqual(follower.mimic, {
|
|
363
|
-
assert.equal(follower.state, undefined, '따라가는 관절은 제 조작값이 없다')
|
|
364
|
-
assert.equal(follower.lowerLimit, undefined)
|
|
362
|
+
assert.deepEqual(follower.motion![0]!.mimic, { motion: 'left', multiplier: -1 })
|
|
363
|
+
assert.equal(follower.motion![0]!.state, undefined, '따라가는 관절은 제 조작값이 없다')
|
|
364
|
+
assert.equal(follower.motion![0]!.lowerLimit, undefined)
|
|
365
365
|
asset = applyFigureAuthoring(asset, { kind: 'set-joint', joint: follower })
|
|
366
366
|
assert.deepEqual(leadersFor(asset, 'wrist', 'slide').map(j => j.id), ['left'], '따라가는 관절은 앞장서지 않는다')
|
|
367
367
|
|
|
@@ -477,8 +477,8 @@ test('미끄러짐의 이동 거리가 부품 길이를 따른다 — 레일이
|
|
|
477
477
|
const joint = asset.joints!.find(j => j.body1.part === 'bridge')!
|
|
478
478
|
const form = { kind: 'slide' as const, axis: 'x' as const, id: 'x', min: 0, max: 500, follow: '', multiplier: 1, offset: 0, travel: 'rail:x', travelPlus: -160 }
|
|
479
479
|
const record = motionJoint(joint, form, 'bridge')
|
|
480
|
-
assert.deepEqual(record.travel, { source: { of: 'part', part: 'rail', axis: 'x' }, plus: -160 })
|
|
481
|
-
assert.equal(record.lowerLimit, undefined, '거리는 레일이 정한다 — mm 범위는 없다')
|
|
480
|
+
assert.deepEqual(record.motion![0]!.travel, { source: { of: 'part', part: 'rail', axis: 'x' }, plus: -160 })
|
|
481
|
+
assert.equal(record.motion![0]!.lowerLimit, undefined, '거리는 레일이 정한다 — mm 범위는 없다')
|
|
482
482
|
asset = applyFigureAuthoring(asset, { kind: 'set-joint', joint: record })
|
|
483
483
|
assert.deepEqual(motionOf(asset, 'bridge')?.travel, { part: 'rail', axis: 'x', plus: -160 }, '화면은 무엇을 따르는지 읽는다')
|
|
484
484
|
const end = (railLength: number) => {
|
|
@@ -517,7 +517,7 @@ test('선언할 크기는 부품이 쉬고 있을 때의 크기 — 문짝이
|
|
|
517
517
|
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'jamb', to: { plane: 'mounting-plane' } })
|
|
518
518
|
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'leaf', to: { part: 'jamb', face: 'right' }, align: { y: 'min', z: 'max' }, gap: 5 })
|
|
519
519
|
const j = asset.joints!.find(o => o.id === 'leaf')!
|
|
520
|
-
asset = applyFigureAuthoring(asset, { kind: 'set-joint', joint: { ...j, type: 'revolute', axis: 'Y', lowerLimit: -90, upperLimit: 0, state: { id: 'open' } } })
|
|
520
|
+
asset = applyFigureAuthoring(asset, { kind: 'set-joint', joint: { ...j, motion: [{ type: 'revolute', axis: 'Y', lowerLimit: -90, upperLimit: 0, state: { id: 'open' } }] } })
|
|
521
521
|
assert.deepEqual(instanceSizeToDeclare(asset)!.at, { x: 995, y: 2100, z: 150 })
|
|
522
522
|
})
|
|
523
523
|
|
|
@@ -534,20 +534,21 @@ test('움직임은 그 자리에서 고친다 — 폼에 지금 관절을 채우
|
|
|
534
534
|
const { motionFormOf, motionJoint } = await import('../client/modeller/links.ts')
|
|
535
535
|
const base = { id: 'j', body0: { part: 'a', face: 'top' as const }, body1: { part: 'b' } }
|
|
536
536
|
const joints = [
|
|
537
|
-
{ ...base, type: 'prismatic' as const, axis: 'Z' as const, lowerLimit: 0, upperLimit: 2500, state: { id: 'extend' } },
|
|
538
|
-
{ ...base, type: 'revolute' as const, axis: 'Y' as const, lowerLimit: -90, upperLimit: 0, state: { id: 'open' } },
|
|
539
|
-
{ ...base, type: 'revolute' as const, axis: 'Y' as const, state: { id: 'spin' } },
|
|
540
|
-
{ ...base, type: 'prismatic' as const, axis: 'X' as const, mimic: {
|
|
541
|
-
{ ...base, type: 'prismatic' as const, axis: 'X' as const, travel: { source: { of: 'part' as const, part: 'rail', axis: 'x' as const }, plus: -160 }, state: { id: 'x' } }
|
|
537
|
+
{ ...base, motion: [{ type: 'prismatic' as const, axis: 'Z' as const, lowerLimit: 0, upperLimit: 2500, state: { id: 'extend' } }] },
|
|
538
|
+
{ ...base, motion: [{ type: 'revolute' as const, axis: 'Y' as const, lowerLimit: -90, upperLimit: 0, state: { id: 'open' } }] },
|
|
539
|
+
{ ...base, motion: [{ type: 'revolute' as const, axis: 'Y' as const, state: { id: 'spin' } }] },
|
|
540
|
+
{ ...base, motion: [{ type: 'prismatic' as const, axis: 'X' as const, mimic: { motion: 'k', multiplier: -1 } }] },
|
|
541
|
+
{ ...base, motion: [{ type: 'prismatic' as const, axis: 'X' as const, travel: { source: { of: 'part' as const, part: 'rail', axis: 'x' as const }, plus: -160 }, state: { id: 'x' } }] }
|
|
542
542
|
]
|
|
543
543
|
for (const joint of joints) {
|
|
544
544
|
const form = motionFormOf(joint)
|
|
545
|
-
|
|
545
|
+
const a = joint.motion[0]!
|
|
546
|
+
assert.deepEqual(motionJoint(joint, form, 'b'), joint, `${a.type} ${JSON.stringify(a.mimic ?? a.travel ?? a.lowerLimit ?? 'endless')}`)
|
|
546
547
|
}
|
|
547
548
|
/* A leader's stroke made to follow the base's length, where it is. */
|
|
548
549
|
const changed = motionJoint(joints[0]!, { ...motionFormOf(joints[0]!), travel: 'base:z', travelPlus: -400 }, 'b')
|
|
549
|
-
assert.deepEqual(changed.travel, { source: { of: 'part', part: 'base', axis: 'z' }, plus: -400 })
|
|
550
|
-
assert.equal(changed.state?.id, 'extend', '같은 조작값 이름')
|
|
550
|
+
assert.deepEqual(changed.motion![0]!.travel, { source: { of: 'part', part: 'base', axis: 'z' }, plus: -400 })
|
|
551
|
+
assert.equal(changed.motion![0]!.state?.id, 'extend', '같은 조작값 이름')
|
|
551
552
|
})
|
|
552
553
|
|
|
553
554
|
test('바닥에 선 부품도 무엇에 붙어 있는지 읽는다 — 그래야 옮겨 붙일 수 있다 (문설주, 2026-09-25)', async () => {
|
|
@@ -693,12 +694,12 @@ test('a follower of several leaders and a stroke that is a sum of lengths are wr
|
|
|
693
694
|
assert.equal(form.follow, '', 'no single leader to pick')
|
|
694
695
|
assert.deepEqual(form.followSum, [{ joint: 'j2', multiplier: -1 }, { joint: 'j3', multiplier: -1 }])
|
|
695
696
|
const saved = motionJoint(wrist, { ...form, axis: form.axis }, 'wrist')
|
|
696
|
-
assert.deepEqual(saved.mimic, { sum: [{
|
|
697
|
-
assert.equal(saved.state, undefined, 'and gives it no control of its own')
|
|
697
|
+
assert.deepEqual(saved.motion![0]!.mimic, { sum: [{ motion: 'j2', multiplier: -1 }, { motion: 'j3', multiplier: -1 }] }, 'saving the form keeps the follower a follower')
|
|
698
|
+
assert.equal(saved.motion![0]!.state, undefined, 'and gives it no control of its own')
|
|
698
699
|
assert.deepEqual(motionOf(asset, 'wrist')?.follows, { sum: [{ joint: 'j2', multiplier: -1 }, { joint: 'j3', multiplier: -1 }], offset: 0 })
|
|
699
700
|
|
|
700
|
-
const reach = { ...wrist, id: 'reach', type: 'prismatic' as const, mimic: undefined, axis: 'X' as const, travel: { sum: [{ from: { of: 'part' as const, part: 'base', axis: 'x' as const } }, { from: { of: 'part' as const, part: 'turret', axis: 'x' as const }, times: 0.5 }] } }
|
|
701
|
+
const reach = { ...wrist, id: 'reach', motion: [{ ...(wrist).motion![0]!, type: 'prismatic' as const, mimic: undefined, axis: 'X' as const, travel: { sum: [{ from: { of: 'part' as const, part: 'base', axis: 'x' as const } }, { from: { of: 'part' as const, part: 'turret', axis: 'x' as const }, times: 0.5 }] } }] }
|
|
701
702
|
const slide = motionFormOf(reach)
|
|
702
703
|
assert.equal(slide.travel, '')
|
|
703
|
-
assert.deepEqual(motionJoint(reach, slide, 'wrist').travel, reach.travel, 'saving keeps the summed stroke')
|
|
704
|
+
assert.deepEqual(motionJoint(reach, slide, 'wrist').motion![0]!.travel, reach.motion[0]!.travel, 'saving keeps the summed stroke')
|
|
704
705
|
})
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { addFigureBox, applyFigureAuthoring, createFigureAsset } from '@hatiolab/figure-model'
|
|
2
|
+
|
|
3
|
+
// A relation audit fixture, not a replacement for testing the authoring workflow.
|
|
4
|
+
export function multipartResponseFixture() {
|
|
5
|
+
let a = addFigureBox(createFigureAsset('부품 관계 점검'), {
|
|
6
|
+
id: 'tabletop', size: { x: 1500, y: 80, z: 600 }, position: { x: 0, y: 740, z: 0 }
|
|
7
|
+
})
|
|
8
|
+
a = applyFigureAuthoring(a, { kind: 'follow-size', part: 'tabletop', axes: ['x'] })
|
|
9
|
+
a = addFigureBox(a, { id: 'brace', size: { x: 1300, y: 60, z: 40 }, position: { x: 0, y: 0, z: 0 } })
|
|
10
|
+
a = applyFigureAuthoring(a, { kind: 'link-dimension', part: 'brace', axis: 'x', source: { of: 'part', part: 'tabletop', axis: 'x' }, plus: -200 })
|
|
11
|
+
a = applyFigureAuthoring(a, { kind: 'attach', part: 'brace', to: { part: 'tabletop', face: 'bottom' }, align: { z: 'min' } })
|
|
12
|
+
for (const [id, x] of [['legLeft', 'min'], ['legRight', 'max']]) {
|
|
13
|
+
a = addFigureBox(a, { id, size: { x: 80, y: 700, z: 80 }, position: { x: 0, y: 0, z: 0 } })
|
|
14
|
+
a = applyFigureAuthoring(a, { kind: 'attach', part: id, to: { part: 'tabletop', face: 'bottom' }, align: { x, z: 'max' } })
|
|
15
|
+
}
|
|
16
|
+
return a
|
|
17
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { test } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { compileFigureAsset, applyFigureAuthoring, figureFollowOf } from '@hatiolab/figure-model'
|
|
4
|
+
import { multipartResponseFixture } from './multipart-response-fixture.js'
|
|
5
|
+
const part = (a: ReturnType<typeof multipartResponseFixture>, width: number, id: string) =>
|
|
6
|
+
compileFigureAsset({ ...a, designInputs: { ...a.designInputs, 'size.x': width } }).evaluate().geometry.find(g => g.id === id)!
|
|
7
|
+
|
|
8
|
+
test('linked brace keeps a 200 mm difference while the tabletop responds to figure width', () => {
|
|
9
|
+
const a = multipartResponseFixture()
|
|
10
|
+
assert.deepEqual(figureFollowOf(a, 'brace', 'x'), { of: 'part', part: 'tabletop', axis: 'x', plus: -200 })
|
|
11
|
+
for (const width of [1500, 2000, 3000]) {
|
|
12
|
+
assert.equal(part(a, width, 'tabletop').shape.dimensions['size.x'], width)
|
|
13
|
+
assert.equal(part(a, width, 'brace').shape.dimensions['size.x'], width - 200)
|
|
14
|
+
}
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
test('fixed leg dimensions do not imply fixed position: attachment follows the tabletop edge', () => {
|
|
18
|
+
const a = multipartResponseFixture()
|
|
19
|
+
for (const id of ['legLeft', 'legRight']) {
|
|
20
|
+
for (const axis of ['x', 'y', 'z'] as const) assert.equal(figureFollowOf(a, id, axis).of, 'own')
|
|
21
|
+
assert.deepEqual(part(a, 1500, id).shape, part(a, 3000, id).shape)
|
|
22
|
+
const sign = id === 'legLeft' ? -1 : 1
|
|
23
|
+
assert.equal(part(a, 1500, id).pose.t[0], sign * 710)
|
|
24
|
+
assert.equal(part(a, 3000, id).pose.t[0], sign * 1460)
|
|
25
|
+
assert.equal(part(a, 3000, id).pose.t[1], 350)
|
|
26
|
+
}
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test('a fixed difference and a proportional relation are not interchangeable', () => {
|
|
30
|
+
const a = multipartResponseFixture()
|
|
31
|
+
// Characterize the command currently invoked by the bulk button, not an endorsement of that UI.
|
|
32
|
+
const proportional = applyFigureAuthoring(a, { kind: 'follow-size', part: 'brace' })
|
|
33
|
+
assert.equal(figureFollowOf(proportional, 'brace', 'x').of, 'instance')
|
|
34
|
+
assert.equal(part(a, 3000, 'brace').shape.dimensions['size.x'], 2800)
|
|
35
|
+
assert.ok(Math.abs(part(proportional, 3000, 'brace').shape.dimensions['size.x']! - 2600) < 0.01)
|
|
36
|
+
assert.deepEqual(proportional.joints, a.joints, 'attachment remains while dimension reference is replaced')
|
|
37
|
+
})
|
package/test/refusals.test.ts
CHANGED
|
@@ -99,9 +99,9 @@ test('지우기는 매단 것 · 따라 움직이는 것을 풀어 남기고 그
|
|
|
99
99
|
a = act(a, { kind: 'attach', part: 'top', to: { part: 'leg', face: 'top' } })
|
|
100
100
|
a = act(a, { kind: 'attach', part: 'lamp', to: { part: 'top', face: 'top' }, align: { x: { mm: -300 } } })
|
|
101
101
|
a = act(a, { kind: 'attach', part: 'shade', to: { part: 'top', face: 'top' }, align: { x: { mm: 300 } } })
|
|
102
|
-
const turn = (asset: typeof a, part: string, extra: object) => act(asset, { kind: 'set-joint', joint: { ...asset.joints!.find(j => j.body1.part === part)!, type: 'revolute', axis: 'Y', ...extra } })
|
|
102
|
+
const turn = (asset: typeof a, part: string, extra: object) => act(asset, { kind: 'set-joint', joint: { ...asset.joints!.find(j => j.body1.part === part)!, motion: [{ type: 'revolute', axis: 'Y', ...extra }] } })
|
|
103
103
|
a = turn(a, 'lamp', { state: { id: 'lamp-turn' } })
|
|
104
|
-
a = turn(a, 'shade', { mimic: {
|
|
104
|
+
a = turn(a, 'shade', { mimic: { motion: 'lamp', multiplier: 1 } })
|
|
105
105
|
|
|
106
106
|
const top = fm.removePart(a, { kind: 'remove-part', target: 'top' })
|
|
107
107
|
const told = removalNoticeOf(top.notice, keyed)!
|
|
@@ -156,7 +156,7 @@ test('a part fastened at any angle is sized by the gizmo too: its fastening keep
|
|
|
156
156
|
const { gizmoReachOf } = await import('../client/modeller/gizmo.js')
|
|
157
157
|
let a = addFigureBox(createFigureAsset('UX'), { id: 'hub', size: { x: 200, y: 100, z: 200 }, position: { x: 0, y: 50, z: 0 } })
|
|
158
158
|
a = addFigureBox(a, { id: 'blade', size: { x: 300, y: 20, z: 60 }, position: { x: 0, y: 110, z: 0 } })
|
|
159
|
-
a = applyFigureAuthoring(a, { kind: 'set-joint', joint: { id: 'blade',
|
|
159
|
+
a = applyFigureAuthoring(a, { kind: 'set-joint', joint: { id: 'blade', body0: { part: 'hub', face: 'top' }, body1: { part: 'blade' }, origin: { childRotate: { z: 30 } } } })
|
|
160
160
|
assert.ok(gizmoReachOf(a, 'blade').scale, 'offered')
|
|
161
161
|
const size = [{ kind: 'set-design', input: 'blade.size.x', unit: 'mm', value: 400 }] as never
|
|
162
162
|
assert.deepEqual(figureSizeEditOf(a, 'blade', size, { keep: 'opposite', axis: 'x', side: 1 }), { changes: size, kept: 'relation' })
|
|
@@ -193,3 +193,25 @@ test('the fastening form keeps a place that follows a position field, and fasten
|
|
|
193
193
|
assert.ok('box.position.x' in again.designInputs && 'box.position.z' in again.designInputs, 'the fields stay')
|
|
194
194
|
assert.deepEqual(JSON.stringify(again.joints), JSON.stringify(kept.joints), 'the same fastening')
|
|
195
195
|
})
|
|
196
|
+
|
|
197
|
+
test('fresh frame: width follows without changing height, depth or pose; one undo restores the undeclared figure', async () => {
|
|
198
|
+
const initial = addFigureBox(createFigureAsset('Frame'), { id: 'frame', size: { x: 1500, y: 80, z: 600 }, position: { x: 0, y: 50, z: 0 } })
|
|
199
|
+
const source = serializeFigureAsset(initial)
|
|
200
|
+
const session = createFigureProductSession({ load: async () => ({ id: 'frame', source }), save: async q => ({ id: 'frame', source: q.source }) })
|
|
201
|
+
await session.open('frame')
|
|
202
|
+
const before = session.source()
|
|
203
|
+
session.command({ kind: 'follow-size', part: 'frame', axes: ['x'] })
|
|
204
|
+
const after = session.source()
|
|
205
|
+
const asset = parseFigureAsset(after)
|
|
206
|
+
assert.equal(figureFollowOf(asset, 'frame', 'x').of, 'instance')
|
|
207
|
+
for (const axis of ['y', 'z'] as const) assert.equal(figureFollowOf(asset, 'frame', axis).of, 'own')
|
|
208
|
+
const geometry = (a: typeof asset) => compileFigureAsset(a).evaluate().geometry.find(g => g.id === 'frame')!
|
|
209
|
+
assert.deepEqual(geometry(asset), geometry(initial), 'enabling response preserves the current geometry and pose')
|
|
210
|
+
const enlarged = geometry({ ...asset, designInputs: { ...asset.designInputs, 'size.x': asset.designInputs['size.x']! * 2, 'size.y': asset.designInputs['size.y']! * 2, 'size.z': asset.designInputs['size.z']! * 2 } })
|
|
211
|
+
const expected = geometry(addFigureBox(createFigureAsset('Frame'), { id: 'frame', size: { x: 3000, y: 80, z: 600 }, position: { x: 0, y: 50, z: 0 } }))
|
|
212
|
+
assert.deepEqual(enlarged, expected, 'only width doubles even when every figure dimension doubles')
|
|
213
|
+
session.undo()
|
|
214
|
+
assert.equal(session.source(), before, 'one undo also removes the automatically declared figure size')
|
|
215
|
+
session.redo()
|
|
216
|
+
assert.equal(session.source(), after)
|
|
217
|
+
})
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { test } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { applyFigureAuthoring, compileFigureAsset, figureRowOf, figureFollowOf } from '@hatiolab/figure-model'
|
|
4
|
+
import { sizeRelationSteps, sizeRelationOf, sizeInputOf } from '../client/modeller/size-response.ts'
|
|
5
|
+
import type { FigureRevision } from '../client/modeller/revision.ts'
|
|
6
|
+
import { multipartResponseFixture } from './multipart-response-fixture.js'
|
|
7
|
+
const revision = (asset: ReturnType<typeof multipartResponseFixture>) => ({ asset, inputs: asset.designInputs } as FigureRevision)
|
|
8
|
+
const width = (a: ReturnType<typeof multipartResponseFixture>, id: string) => compileFigureAsset(a).evaluate().geometry.find(g => g.id === id)!.shape.dimensions['size.x']
|
|
9
|
+
|
|
10
|
+
test('switching difference to ratio preserves current size, changes future response, and leaves attachment intact', () => {
|
|
11
|
+
const a = multipartResponseFixture()
|
|
12
|
+
assert.equal(sizeRelationOf(revision(a), 'brace', 'x').mode, 'difference')
|
|
13
|
+
const b = sizeRelationSteps(revision(a), 'brace', 'x', {of:'part',part:'tabletop',axis:'x'}, 'ratio').reduce(applyFigureAuthoring, a)
|
|
14
|
+
assert.equal(width(b,'brace'),1300)
|
|
15
|
+
assert.deepEqual(b.joints,a.joints)
|
|
16
|
+
assert.equal(sizeRelationOf(revision(b),'brace','x').mode,'ratio')
|
|
17
|
+
assert.equal(width({...b,designInputs:{...b.designInputs,'size.x':3000}},'brace'),2600)
|
|
18
|
+
})
|
|
19
|
+
test('own X can keep a difference from another part Y; switching source preserves its current size', () => {
|
|
20
|
+
const a = multipartResponseFixture()
|
|
21
|
+
const b = sizeRelationSteps(revision(a),'brace','x',{of:'part',part:'tabletop',axis:'y'},'difference').reduce(applyFigureAuthoring,a)
|
|
22
|
+
assert.equal(width(b,'brace'),1300)
|
|
23
|
+
assert.deepEqual(figureFollowOf(b,'brace','x'),{of:'part',part:'tabletop',axis:'y',plus:1220})
|
|
24
|
+
assert.deepEqual(b.joints,a.joints)
|
|
25
|
+
})
|
|
26
|
+
test('editing a linked size updates its difference without changing the reference', () => {
|
|
27
|
+
const a=multipartResponseFixture()
|
|
28
|
+
const b=sizeRelationSteps(revision(a),'brace','x',{of:'part',part:'tabletop',axis:'x'},'difference',1250.25).reduce(applyFigureAuthoring,a)
|
|
29
|
+
assert.equal(width(b,'brace'),1250.25)
|
|
30
|
+
assert.equal(width({...b,designInputs:{...b.designInputs,'size.x':3000}},'brace'),2750.25)
|
|
31
|
+
assert.equal(sizeInputOf(a,'brace','x'),undefined)
|
|
32
|
+
assert.ok(sizeInputOf(a,'legLeft','x'))
|
|
33
|
+
assert.throws(()=>sizeRelationSteps(revision(a),'brace','x',{of:'part',part:'brace',axis:'x'},'ratio'),/SIZE_SOURCE/)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test('repeat settings can change count, direction and mode without losing the attachment or size relation', () => {
|
|
37
|
+
let a=applyFigureAuthoring(multipartResponseFixture(), {kind:'make-repeat',part:'legLeft',axis:'x',pitch:200})
|
|
38
|
+
const initial=a.joints!.find(j=>j.body1.part==='legLeft')!
|
|
39
|
+
a=applyFigureAuthoring(a,{kind:'set-joint',joint:{...initial,repeat:{...initial.repeat!,count:5}}})
|
|
40
|
+
assert.equal(figureRowOf(a,'legLeft')!.count,5)
|
|
41
|
+
const current=a.joints!.find(j=>j.body1.part==='legLeft')!
|
|
42
|
+
a=applyFigureAuthoring(a,{kind:'set-joint',joint:{...current,repeat:{...current.repeat!,axis:'Z',count:3}}})
|
|
43
|
+
assert.equal(figureRowOf(a,'legLeft')!.count,3)
|
|
44
|
+
const last=a.joints!.find(j=>j.body1.part==='legLeft')!
|
|
45
|
+
a=applyFigureAuthoring(a,{kind:'set-joint',joint:{...last,repeat:{...last.repeat!,count:undefined,pitch:200}}})
|
|
46
|
+
assert.equal(a.joints!.find(j=>j.body1.part==='legLeft')!.repeat!.count,undefined)
|
|
47
|
+
assert.deepEqual(a.joints!.find(j=>j.body1.part==='legLeft')!.body0,initial.body0)
|
|
48
|
+
assert.deepEqual(figureFollowOf(a,'brace','x'),{of:'part',part:'tabletop',axis:'x',plus:-200})
|
|
49
|
+
})
|