@things-factory/figure-ui 10.1.198 → 10.1.201
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/modeller/editor.ts +7 -3
- package/client/modeller/fasten-form.ts +18 -35
- package/client/modeller/fasten-geometry.ts +67 -115
- package/client/modeller/figure-fasten-dialog.ts +4 -12
- package/client/modeller/figure-links.ts +6 -7
- package/client/modeller/joint-drag.ts +9 -11
- package/client/modeller/links.ts +35 -99
- package/client/modeller/refusals.ts +2 -6
- package/dist-client/modeller/editor.js +7 -2
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/fasten-form.d.ts +5 -8
- package/dist-client/modeller/fasten-form.js +16 -28
- package/dist-client/modeller/fasten-form.js.map +1 -1
- package/dist-client/modeller/fasten-geometry.d.ts +20 -19
- package/dist-client/modeller/fasten-geometry.js +54 -113
- package/dist-client/modeller/fasten-geometry.js.map +1 -1
- package/dist-client/modeller/figure-fasten-dialog.js +4 -12
- package/dist-client/modeller/figure-fasten-dialog.js.map +1 -1
- package/dist-client/modeller/figure-links.d.ts +1 -1
- package/dist-client/modeller/figure-links.js +6 -7
- package/dist-client/modeller/figure-links.js.map +1 -1
- package/dist-client/modeller/joint-drag.js +9 -11
- package/dist-client/modeller/joint-drag.js.map +1 -1
- package/dist-client/modeller/links.d.ts +19 -26
- package/dist-client/modeller/links.js +36 -94
- package/dist-client/modeller/links.js.map +1 -1
- package/dist-client/modeller/refusals.d.ts +1 -1
- package/dist-client/modeller/refusals.js +2 -6
- package/dist-client/modeller/refusals.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +6 -6
- package/test/ai-proposal.test.ts +1 -1
- package/test/browser/acceptance-page.js +11 -11
- 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/fix-alignment.check.mjs +3 -3
- package/test/browser/links.check.mjs +20 -8
- package/test/browser/modeller-page.js +3 -3
- package/test/browser/part-ids.check.mjs +2 -2
- package/test/browser/parts-page.js +2 -2
- package/test/browser/product-page.check.mjs +3 -3
- package/test/browser/product-page.js +1 -1
- package/test/browser/row-preview.check.mjs +2 -2
- package/test/browser/screen-page.js +3 -3
- package/test/browser/section-page.js +3 -3
- package/test/browser/section.check.mjs +4 -4
- package/test/clip-ai-proposal.test.ts +1 -1
- package/test/clip-consent.test.ts +1 -1
- package/test/command-preview.test.ts +8 -8
- package/test/face-mapping.test.ts +2 -2
- package/test/fasten-drawing.test.ts +14 -2
- package/test/fasten-form.test.ts +11 -3
- package/test/fasten-geometry.test.ts +22 -31
- package/test/fix-alignment.test.ts +16 -4
- package/test/gizmo.test.ts +2 -2
- package/test/joint-drag.test.ts +13 -2
- package/test/joint-fan.test.ts +1 -1
- package/test/joint-frames.test.ts +74 -45
- package/test/joints.ts +1 -1
- package/test/links.test.ts +68 -68
- package/test/live.test.ts +2 -2
- package/test/preview-size.test.ts +1 -1
- package/test/refusals.test.ts +18 -20
- package/test/resize-with-placement.test.ts +6 -6
- package/test/revision.test.ts +4 -5
- package/test/section-panel.test.ts +1 -1
- package/translations/en.json +9 -11
- package/translations/es.json +9 -11
- package/translations/ja.json +9 -11
- package/translations/ko.json +9 -11
- package/translations/ms.json +9 -11
- package/translations/zh.json +9 -11
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* **관절 틀 (ADR-0107 결정 1·4) 을 화면이 읽고 그대로 돌려 쓴다** — 어느 방향이든 되는 축, 부품 기준점에 놓인 관절 끝,
|
|
3
|
-
* `origin.pivot` · `offset` · `
|
|
4
|
-
* 다시 열어도 그대로다.
|
|
3
|
+
* `origin.pivot` · `offset` · `parentRotate`(ADR-0140 3.6). 움직임 폼 · 붙이기 폼 · 기즈모 끌기 · 움직임 없애기가 이 값들을
|
|
4
|
+
* 바꾸지 않고, 저장하고 다시 열어도 그대로다.
|
|
5
5
|
*/
|
|
6
6
|
import { test } from 'node:test'
|
|
7
7
|
import assert from 'node:assert/strict'
|
|
@@ -21,18 +21,6 @@ const box = (asset: FigureAsset, id: string, x: number, y: number, z: number, at
|
|
|
21
21
|
const drawn = (asset: FigureAsset, id: string, state: Record<string, number> = {}) =>
|
|
22
22
|
compileFigureAsset(asset).evaluate(state).geometry.find(g => g.id === id) as never as { id: string; pose: { t: number[]; r: number[][] } }
|
|
23
23
|
|
|
24
|
-
/** Whether this kernel knows the joint frames: an axis that is a vector is refused by one that does not. */
|
|
25
|
-
const FRAMES = (() => {
|
|
26
|
-
try {
|
|
27
|
-
let a = box(createFigureAsset('probe'), 'p', 10, 10, 10, 5)
|
|
28
|
-
a = set(a, { id: 'p', type: 'revolute', body0: { plane: 'mounting-plane' }, body1: { part: 'p', face: 'bottom' }, axis: { x: 0, y: 1, z: 0 } })
|
|
29
|
-
return !!a.joints?.some(j => typeof j.axis === 'object')
|
|
30
|
-
} catch {
|
|
31
|
-
return false
|
|
32
|
-
}
|
|
33
|
-
})()
|
|
34
|
-
const framesOnly = { skip: FRAMES ? false : 'the kernel has no ADR-0107 joint frames' }
|
|
35
|
-
|
|
36
24
|
const SUMMED = { sum: [{ from: { of: 'part', part: 'link', axis: 'y' }, times: 0.5 }], plus: 10 }
|
|
37
25
|
|
|
38
26
|
/**
|
|
@@ -44,18 +32,18 @@ function framed(): FigureAsset {
|
|
|
44
32
|
a = box(a, 'link', 100, 400, 100, 300)
|
|
45
33
|
a = box(a, 'tool', 60, 120, 40)
|
|
46
34
|
a = box(a, 'cap', 20, 20, 20)
|
|
47
|
-
a = set(a, { id: 'base', type: 'fixed', body0: { plane: 'mounting-plane' }, body1: { part: 'base'
|
|
48
|
-
a = set(a, { id: 'link', type: 'revolute', body0: { part: 'base', face: 'top' }, body1: { part: 'link'
|
|
49
|
-
a = set(a, { id: 'tool', type: 'revolute', body0: { part: 'link', at: 'origin' }, body1: { part: 'tool', at: 'origin' }, origin: { offset: { x: 120, y: SUMMED, z: -30 },
|
|
50
|
-
return set(a, { id: 'cap', type: 'fixed', body0: { part: 'tool', at: 'origin' }, body1: { part: 'cap'
|
|
35
|
+
a = set(a, { id: 'base', type: 'fixed', body0: { plane: 'mounting-plane' }, body1: { part: 'base' } })
|
|
36
|
+
a = set(a, { id: 'link', type: 'revolute', body0: { part: 'base', face: 'top' }, body1: { part: 'link' }, origin: { pivot: 'child-face', offset: { x: 20 }, parentRotate: { x: 10 } }, axis: { x: 1, y: 1, z: 0 }, lowerLimit: -90, upperLimit: 90 })
|
|
37
|
+
a = set(a, { id: 'tool', type: 'revolute', 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 } }, axis: 'Z', lowerLimit: -45, upperLimit: 45 })
|
|
38
|
+
return set(a, { id: 'cap', type: 'fixed', body0: { part: 'tool', at: 'origin' }, body1: { part: 'cap' }, origin: { offset: { y: 40 } } })
|
|
51
39
|
}
|
|
52
40
|
|
|
53
41
|
/** The old shapes only: a token axis, face ends, a gap and an edge. */
|
|
54
42
|
function plain(): FigureAsset {
|
|
55
43
|
let a = box(createFigureAsset('Plain'), 'base', 300, 100, 300, 50)
|
|
56
44
|
a = box(a, 'link', 100, 400, 100, 300)
|
|
57
|
-
a = set(a, { id: 'base', type: 'fixed', body0: { plane: 'mounting-plane' }, body1: { part: 'base'
|
|
58
|
-
return set(a, { id: 'link', type: 'revolute', body0: { part: 'base', face: 'top' }, body1: { part: 'link'
|
|
45
|
+
a = set(a, { id: 'base', type: 'fixed', body0: { plane: 'mounting-plane' }, body1: { part: 'base' } })
|
|
46
|
+
return set(a, { id: 'link', type: 'revolute', body0: { part: 'base', face: 'top' }, body1: { part: 'link' }, origin: { gap: 5, align: { x: 'min' } }, axis: 'Y', lowerLimit: -90, upperLimit: 90 })
|
|
59
47
|
}
|
|
60
48
|
|
|
61
49
|
const reopen = (asset: FigureAsset) => parseFigureAsset(serializeFigureAsset(asset) as unknown as string)
|
|
@@ -67,7 +55,7 @@ const frameOf = (j: Dynamic, keepType = true) => ({
|
|
|
67
55
|
body1: j.body1,
|
|
68
56
|
pivot: j.origin?.pivot,
|
|
69
57
|
offset: j.origin?.offset,
|
|
70
|
-
|
|
58
|
+
parentRotate: j.origin?.parentRotate
|
|
71
59
|
})
|
|
72
60
|
|
|
73
61
|
/** The fastening form's 붙이기, as figure-links builds it from what `fasteningOf` reads; nothing where it writes none. */
|
|
@@ -75,15 +63,13 @@ function fasteningSave(asset: FigureAsset, part: string): FigureAuthoringAction
|
|
|
75
63
|
const f = fasteningOf(revisionOf(asset), part) as Dynamic
|
|
76
64
|
if (!f || f.atOrigin) return null
|
|
77
65
|
const aim = f.toPlane ? ('plane' as const) : { part: f.target, face: f.targetFace }
|
|
78
|
-
const align = alignOf(asset,
|
|
66
|
+
const align = alignOf(asset, aim, f.align, f.alignMm, f.alignInput)
|
|
79
67
|
return {
|
|
80
68
|
kind: 'attach',
|
|
81
69
|
part,
|
|
82
|
-
face: f.face,
|
|
83
70
|
to: f.toPlane ? { plane: 'mounting-plane' } : { part: f.target, face: f.targetFace },
|
|
84
|
-
...(f.toPlane ? {} : { facing: f.facing }),
|
|
71
|
+
...(f.toPlane ? (f.facing === 'flush' ? { facing: f.facing } : {}) : { facing: f.facing }),
|
|
85
72
|
...(f.gap ? { gap: f.gap } : {}),
|
|
86
|
-
twist: f.twist,
|
|
87
73
|
...(align ? { align } : {}),
|
|
88
74
|
replace: true
|
|
89
75
|
} as FigureAuthoringAction
|
|
@@ -127,15 +113,26 @@ test('the axis reader: a token is its axis, a vector its unit vector, unrounded;
|
|
|
127
113
|
test('a follower is not a turn without end: it has no limits because its leader gives its range', () => {
|
|
128
114
|
let a = box(createFigureAsset('Pair'), 'a', 100, 100, 100, 50)
|
|
129
115
|
a = box(a, 'b', 50, 50, 50, 125)
|
|
130
|
-
a = set(a, { id: 'a', type: 'revolute', body0: { plane: 'mounting-plane' }, body1: { part: 'a'
|
|
131
|
-
a = set(a, { id: 'b', type: 'revolute', body0: { part: 'a', face: 'top' }, body1: { part: 'b'
|
|
116
|
+
a = set(a, { id: 'a', type: 'revolute', body0: { plane: 'mounting-plane' }, body1: { part: 'a' }, axis: 'Y', lowerLimit: -90, upperLimit: 90 })
|
|
117
|
+
a = set(a, { id: 'b', type: 'revolute', body0: { part: 'a', face: 'top' }, body1: { part: 'b' }, axis: 'Y', mimic: { joint: 'a', multiplier: -1 } })
|
|
132
118
|
assert.equal(motionOf(a, 'b')?.kind, 'turn')
|
|
133
119
|
assert.equal(motionFormOf(jointOf(a, 'b')).kind, 'turn')
|
|
134
120
|
assert.equal(motionOf(a, 'a')?.kind, 'turn')
|
|
135
|
-
const free = set(a, { id: 'a', type: 'revolute', body0: { plane: 'mounting-plane' }, body1: { part: 'a'
|
|
121
|
+
const free = set(a, { id: 'a', type: 'revolute', body0: { plane: 'mounting-plane' }, body1: { part: 'a' }, axis: 'Y' })
|
|
136
122
|
assert.equal(motionOf(free, 'a')?.kind, 'continuous', 'a joint of its own without limits still turns without end')
|
|
137
123
|
})
|
|
138
124
|
|
|
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
|
+
let a = box(createFigureAsset('Pair'), 'a', 100, 100, 100, 50)
|
|
127
|
+
a = box(a, 'b', 50, 50, 50, 125)
|
|
128
|
+
a = set(a, { id: 'a', type: 'revolute', body0: { plane: 'mounting-plane' }, body1: { part: 'a' }, axis: 'Y', lowerLimit: -90, upperLimit: 90 })
|
|
129
|
+
a = set(a, { id: 'b', type: 'revolute', body0: { part: 'a', face: 'top' }, body1: { part: 'b' }, origin: { gap: 5 }, axis: 'Y', lowerLimit: -30, upperLimit: 30, state: { id: 'b-turn' } })
|
|
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', 'type', 'axis', 'mimic'])
|
|
132
|
+
assert.doesNotThrow(() => set(a, follower))
|
|
133
|
+
assert.deepEqual(Object.keys(fixedJointOf(jointOf(a, 'b'))), ['id', 'type', 'body0', 'body1', 'origin'])
|
|
134
|
+
})
|
|
135
|
+
|
|
139
136
|
test('a range need not hold 0; it needs its most above its least', () => {
|
|
140
137
|
const form = motionFormOf(jointOf(plain(), 'link'))
|
|
141
138
|
assert.equal(motionFormRefusalOf({ ...form, min: 10, max: 90 }, ''), undefined)
|
|
@@ -166,7 +163,7 @@ test('old shapes: every form path writes the joint back as it was, saved and reo
|
|
|
166
163
|
assert.equal(paths.drag.origin?.gap, 5)
|
|
167
164
|
})
|
|
168
165
|
|
|
169
|
-
test('a vector axis: the form keeps it, the summary says it, saved and reopened it is the same vector',
|
|
166
|
+
test('a vector axis: the form keeps it, the summary says it, saved and reopened it is the same vector', () => {
|
|
170
167
|
const asset = framed()
|
|
171
168
|
const form = motionFormOf(jointOf(asset, 'link'))
|
|
172
169
|
assert.deepEqual(form.axisVector, { x: 1, y: 1, z: 0 })
|
|
@@ -177,7 +174,7 @@ test('a vector axis: the form keeps it, the summary says it, saved and reopened
|
|
|
177
174
|
assert.equal(jointOf(saved, 'link').upperLimit, 60)
|
|
178
175
|
})
|
|
179
176
|
|
|
180
|
-
test('frames: every form path keeps the vector axis, pivot, offset (summed too),
|
|
177
|
+
test('frames: every form path keeps the vector axis, pivot, offset (summed too), parentRotate and origin ends, saved and reopened', () => {
|
|
181
178
|
const asset = framed()
|
|
182
179
|
for (const id of ['link', 'tool', 'cap']) {
|
|
183
180
|
const was = jointOf(asset, id)
|
|
@@ -197,24 +194,23 @@ test('frames: every form path keeps the vector axis, pivot, offset (summed too),
|
|
|
197
194
|
assert.deepEqual(up, { kind: 'refused', reason: 'origin-follows', joint: 'tool' })
|
|
198
195
|
})
|
|
199
196
|
|
|
200
|
-
test('placed by an origin: said as the origin, no row offered, and the fastening form writes nothing over it',
|
|
197
|
+
test('placed by an origin: said as the origin, no row offered, and the fastening form writes nothing over it', () => {
|
|
201
198
|
const asset = framed()
|
|
202
199
|
assert.equal(endPlaceKeyOf(jointOf(asset, 'tool').body0), 'figure.label.joint-origin')
|
|
203
200
|
assert.equal(endPlaceKeyOf(jointOf(asset, 'link').body0), 'figure.label.face-top')
|
|
204
201
|
assert.equal((fasteningOf(revisionOf(asset), 'tool') as Dynamic).atOrigin, true)
|
|
205
202
|
assert.equal((fasteningOf(revisionOf(asset), 'cap') as Dynamic).atOrigin, true)
|
|
206
|
-
assert.equal((fasteningOf(revisionOf(asset), 'cap') as Dynamic).face, 'centre')
|
|
207
203
|
assert.equal((fasteningOf(revisionOf(asset), 'link') as Dynamic).atOrigin, undefined)
|
|
208
204
|
assert.equal(fasteningSave(asset, 'tool'), null)
|
|
209
205
|
assert.deepEqual(rowAxesOf(asset, 'cap'), [])
|
|
210
206
|
assert.equal(rowBlockOf(asset, 'cap'), 'at-origin')
|
|
211
207
|
})
|
|
212
208
|
|
|
213
|
-
test('a drag about a vector axis sets the value; off it, it is refused and the vector said',
|
|
209
|
+
test('a drag about a vector axis sets the value; off it, it is refused and the vector said', () => {
|
|
214
210
|
let a = box(createFigureAsset('Tilt'), 'post', 100, 200, 100, 100)
|
|
215
211
|
a = box(a, 'head', 60, 60, 60, 230)
|
|
216
|
-
a = set(a, { id: 'post', type: 'fixed', body0: { plane: 'mounting-plane' }, body1: { part: 'post'
|
|
217
|
-
a = set(a, { id: 'head', type: 'revolute', body0: { part: 'post', face: 'top' }, body1: { part: 'head'
|
|
212
|
+
a = set(a, { id: 'post', type: 'fixed', body0: { plane: 'mounting-plane' }, body1: { part: 'post' } })
|
|
213
|
+
a = set(a, { id: 'head', type: 'revolute', body0: { part: 'post', face: 'top' }, body1: { part: 'head' }, axis: { x: 0, y: 3, z: 4 }, lowerLimit: -90, upperLimit: 90 })
|
|
218
214
|
const at = drawn(a, 'head')
|
|
219
215
|
/* 30° about (0, 0.6, 0.8), as XYZ degrees. */
|
|
220
216
|
const q = [0, 0.6, 0.8].map(v => v * Math.sin(Math.PI / 12))
|
|
@@ -232,11 +228,11 @@ test('a drag about a vector axis sets the value; off it, it is refused and the v
|
|
|
232
228
|
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 })
|
|
233
229
|
})
|
|
234
230
|
|
|
235
|
-
test('a rotated joint frame: a turn about its turned axis is its value, and a move is measured before the rotation',
|
|
231
|
+
test('a rotated joint frame: a turn about its turned axis is its value, and a move is measured before the rotation', () => {
|
|
236
232
|
let a = box(createFigureAsset('Rotated'), 'post', 100, 200, 100, 100)
|
|
237
233
|
a = box(a, 'link', 60, 300, 60, 350)
|
|
238
|
-
a = set(a, { id: 'post', type: 'fixed', body0: { plane: 'mounting-plane' }, body1: { part: 'post'
|
|
239
|
-
a = set(a, { id: 'bend', type: 'revolute', body0: { part: 'post', face: 'top' }, body1: { part: 'link'
|
|
234
|
+
a = set(a, { id: 'post', type: 'fixed', body0: { plane: 'mounting-plane' }, body1: { part: 'post' } })
|
|
235
|
+
a = set(a, { id: 'bend', type: 'revolute', body0: { part: 'post', face: 'top' }, body1: { part: 'link' }, origin: { parentRotate: { x: 90 } }, axis: 'Z', lowerLimit: -90, upperLimit: 90 })
|
|
240
236
|
/* Turned 90° about X, the frame's Z is the figure's -Y: 20° about -Y is 20 on the joint. */
|
|
241
237
|
const at = drawn(a, 'link', { bend: 10 })
|
|
242
238
|
const before = at.pose.r
|
|
@@ -256,16 +252,16 @@ test('a rotated joint frame: a turn about its turned axis is its value, and a mo
|
|
|
256
252
|
const still = set(a, { ...jointOf(a, 'bend'), type: 'fixed', axis: undefined, lowerLimit: undefined, upperLimit: undefined })
|
|
257
253
|
const s = drawn(still, 'link')
|
|
258
254
|
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 }
|
|
259
|
-
assert.deepEqual(moved.joint.origin, {
|
|
255
|
+
assert.deepEqual(moved.joint.origin, { parentRotate: { x: 90 }, align: { z: { mm: 30 } } })
|
|
260
256
|
const up = jointDragOf(still, 'link', s as never, { position: [s.pose.t[0]!, s.pose.t[1]! + 25, s.pose.t[2]!] }, {}) as { joint: Dynamic }
|
|
261
|
-
assert.deepEqual(up.joint.origin, {
|
|
257
|
+
assert.deepEqual(up.joint.origin, { parentRotate: { x: 90 }, gap: 25 })
|
|
262
258
|
})
|
|
263
259
|
|
|
264
|
-
test('a fan stands about a vector axis, as it does about the same token',
|
|
260
|
+
test('a fan stands about a vector axis, as it does about the same token', () => {
|
|
265
261
|
let a = box(createFigureAsset('Fan'), 'base', 200, 100, 200, 50)
|
|
266
262
|
a = box(a, 'arm', 400, 50, 50, 125)
|
|
267
|
-
a = set(a, { id: 'base', type: 'fixed', body0: { plane: 'mounting-plane' }, body1: { part: 'base'
|
|
268
|
-
const arm = (axis: unknown) => set(a, { id: 'arm', type: 'revolute', body0: { part: 'base', face: 'top' }, body1: { part: 'arm'
|
|
263
|
+
a = set(a, { id: 'base', type: 'fixed', body0: { plane: 'mounting-plane' }, body1: { part: 'base' } })
|
|
264
|
+
const arm = (axis: unknown) => set(a, { id: 'arm', type: 'revolute', body0: { part: 'base', face: 'top' }, body1: { part: 'arm' }, axis, lowerLimit: -45, upperLimit: 90 })
|
|
269
265
|
const token = jointFanOf(revisionOf(arm('Y')), 'arm') as never as { pose: { t: number[]; r: number[][] } }
|
|
270
266
|
const vector = jointFanOf(revisionOf(arm({ x: 0, y: 2, z: 0 })), 'arm') as never as { pose: { t: number[]; r: number[][] } }
|
|
271
267
|
assert.ok(token && vector, 'a fan for both')
|
|
@@ -284,11 +280,44 @@ test('editing a motion keeps the control named after the joint when it has no st
|
|
|
284
280
|
let a = createFigureAsset('Hinge')
|
|
285
281
|
a = addFigureBox(a, { id: 'base', size: { x: 200, y: 100, z: 200 }, position: { x: 0, y: 0, z: 0 } })
|
|
286
282
|
a = addFigureBox(a, { id: 'lid', size: { x: 200, y: 20, z: 200 }, position: { x: 0, y: 0, z: 0 } })
|
|
287
|
-
a = applyFigureAuthoring(a, { kind: 'set-joint', joint: { id: 'base', type: 'fixed', body0: { plane: 'mounting-plane' }, body1: { part: 'base'
|
|
288
|
-
a = applyFigureAuthoring(a, { kind: 'set-joint', joint: { id: 'lid', type: 'revolute', body0: { part: 'base', face: 'top' }, body1: { part: 'lid'
|
|
283
|
+
a = applyFigureAuthoring(a, { kind: 'set-joint', joint: { id: 'base', type: 'fixed', body0: { plane: 'mounting-plane' }, body1: { part: 'base' } } })
|
|
284
|
+
a = applyFigureAuthoring(a, { kind: 'set-joint', joint: { id: 'lid', type: 'revolute', body0: { part: 'base', face: 'top' }, body1: { part: 'lid' }, axis: 'X', lowerLimit: -90, upperLimit: 90 } })
|
|
289
285
|
const lid = a.joints!.find(j => j.id === 'lid')!
|
|
290
286
|
const form = motionFormOf(lid)
|
|
291
287
|
assert.equal(form.id, 'lid', 'the form starts from the input the joint is driven by')
|
|
292
288
|
const edited = applyFigureAuthoring(a, { kind: 'set-joint', joint: motionJoint(lid, { ...form, max: 80 }, 'lid') })
|
|
293
289
|
assert.deepEqual(edited.document.model.inputs.filter(i => i.role === 'state').map(i => i.id), ['lid'], 'the control keeps its name after the edit')
|
|
294
290
|
})
|
|
291
|
+
|
|
292
|
+
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
|
+
let a = box(createFigureAsset('Leaning'), 'post', 100, 200, 100, 100)
|
|
294
|
+
a = box(a, 'link', 60, 300, 60, 350)
|
|
295
|
+
a = set(a, { id: 'post', type: 'fixed', body0: { plane: 'mounting-plane' }, body1: { part: 'post' } })
|
|
296
|
+
a = set(a, { id: 'bend', type: 'revolute', body0: { part: 'post', face: 'top' }, body1: { part: 'link' }, origin: { childRotate: { x: 30, z: 40 } }, axis: 'Y', lowerLimit: -90, upperLimit: 90 })
|
|
297
|
+
const at = drawn(a, 'link', { bend: 10 })
|
|
298
|
+
const after = drawn(a, 'link', { bend: 35 }).pose.r
|
|
299
|
+
const ry = Math.asin(Math.max(-1, Math.min(1, after[0]![2]!)))
|
|
300
|
+
const rx = Math.atan2(-after[1]![2]!, after[2]![2]!)
|
|
301
|
+
const rz = Math.atan2(-after[0]![1]!, after[0]![0]!)
|
|
302
|
+
const drag = jointDragOf(a, 'link', at as never, { rotation: [rx, ry, rz].map(v => (v * 180) / Math.PI) as [number, number, number] }, { bend: 10 })
|
|
303
|
+
assert.deepEqual(drag, { kind: 'value', input: 'bend', unit: 'deg', value: 35 })
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
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
|
+
let a = box(createFigureAsset('Laid'), 'post', 100, 200, 100, 100)
|
|
308
|
+
a = box(a, 'link', 60, 300, 60, 350)
|
|
309
|
+
a = set(a, { id: 'post', type: 'fixed', body0: { plane: 'mounting-plane' }, body1: { part: 'post' } })
|
|
310
|
+
/* 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', type: 'revolute', body0: { part: 'post', face: 'top' }, body1: { part: 'link' }, origin: { childRotate: { x: 90 } }, axis: 'Y', lowerLimit: -90, upperLimit: 90 })
|
|
312
|
+
const at = drawn(a, 'link', { bend: 10 })
|
|
313
|
+
const before = at.pose.r
|
|
314
|
+
const c = Math.cos((20 * Math.PI) / 180),
|
|
315
|
+
s = Math.sin((20 * Math.PI) / 180)
|
|
316
|
+
const turnY = [[c, 0, s], [0, 1, 0], [-s, 0, c]]
|
|
317
|
+
const r = turnY.map(row => [0, 1, 2].map(j => row.reduce((sum, v, k) => sum + v * before[k]![j]!, 0)))
|
|
318
|
+
const ry = Math.asin(Math.max(-1, Math.min(1, r[0]![2]!)))
|
|
319
|
+
const rx = Math.atan2(-r[1]![2]!, r[2]![2]!)
|
|
320
|
+
const rz = Math.atan2(-r[0]![1]!, r[0]![0]!)
|
|
321
|
+
const drag = jointDragOf(a, 'link', at as never, { rotation: [rx, ry, rz].map(v => (v * 180) / Math.PI) as [number, number, number] }, { bend: 10 })
|
|
322
|
+
assert.deepEqual(drag, { kind: 'value', input: 'bend', unit: 'deg', value: 30 })
|
|
323
|
+
})
|
package/test/joints.ts
CHANGED
package/test/links.test.ts
CHANGED
|
@@ -41,7 +41,7 @@ test('크기 — 제 치수 · 인스턴스 크기 · 다른 부품의 치수를
|
|
|
41
41
|
test('붙이기 — 누구에게 붙었나, 붙일 수 있는 모양인가', () => {
|
|
42
42
|
const asset = pair()
|
|
43
43
|
assert.equal(fastenedTo(asset, 'block'), null)
|
|
44
|
-
const fastened = applyFigureAuthoring(asset, { kind: 'attach', part: 'block',
|
|
44
|
+
const fastened = applyFigureAuthoring(asset, { kind: 'attach', part: 'block', to: { part: 'plate', face: 'top' } })
|
|
45
45
|
assert.equal(fastenedTo(fastened, 'block'), 'plate')
|
|
46
46
|
assert.equal(fastenedTo(applyFigureAuthoring(fastened, { kind: 'detach', part: 'block' }), 'block'), null)
|
|
47
47
|
const withDrum = addFigurePrimitive(asset, { id: 'drum', kind: 'cylinder', dimensions: { radius: 50, length: 100 }, position: { x: 0, y: 50, z: 0 } })
|
|
@@ -69,7 +69,7 @@ test('움직임 — 미끄러짐 · 돌기, 축, 무엇이 모느냐를 읽는
|
|
|
69
69
|
|
|
70
70
|
import { readFileSync } from 'node:fs'
|
|
71
71
|
import { fileURLToPath } from 'node:url'
|
|
72
|
-
import { alignRowsOf, choiceOf, followCommandOf
|
|
72
|
+
import { alignRowsOf, choiceOf, followCommandOf } from '../client/modeller/links.ts'
|
|
73
73
|
import { faceViewOf } from '../client/modeller/fasten-geometry.ts'
|
|
74
74
|
import { moving } from './joints.ts'
|
|
75
75
|
|
|
@@ -244,8 +244,8 @@ test('한 비율로 커지는 도형 — 자기 숫자인 길이를 세고, 한
|
|
|
244
244
|
let asset = createFigureAsset('Probe')
|
|
245
245
|
asset = addFigureBox(asset, { id: 'base', size: { x: 400, y: 100, z: 400 }, position: { x: 0, y: 0, z: 0 } })
|
|
246
246
|
asset = addFigureBox(asset, { id: 'lamp', size: { x: 60, y: 60, z: 60 }, position: { x: 0, y: 0, z: 0 } })
|
|
247
|
-
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'base',
|
|
248
|
-
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'lamp',
|
|
247
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'base', to: { plane: 'mounting-plane' } })
|
|
248
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'lamp', to: { part: 'base', face: 'top' }, align: { x: { mm: 120 } } })
|
|
249
249
|
asset = applyFigureAuthoring(asset, { kind: 'declare-instance-size', range: { x: { min: 40, max: 4000 }, y: { min: 16, max: 1600 }, z: { min: 40, max: 4000 } } })
|
|
250
250
|
assert.equal(proportionToFollow(asset), undefined, '한 비율이 아니면 권하지 않는다')
|
|
251
251
|
asset = applyFigureAuthoring(asset, { kind: 'scale-uniformly' })
|
|
@@ -271,20 +271,20 @@ test('붙일 때 면 위 어디에 — 그 면을 가로지르는 두 축만,
|
|
|
271
271
|
two = addFigureBox(two, { id: 'jamb', size: { x: 100, y: 2100, z: 150 }, position: { x: 0, y: 1050, z: 0 } })
|
|
272
272
|
two = addFigureBox(two, { id: 'leaf', size: { x: 900, y: 2000, z: 40 }, position: { x: 600, y: 1000, z: 0 } })
|
|
273
273
|
const right = { part: 'jamb', face: 'right' }
|
|
274
|
-
assert.deepEqual(alignAxesOf(two,
|
|
275
|
-
assert.deepEqual(alignAxesOf(two, '
|
|
274
|
+
assert.deepEqual(alignAxesOf(two, right), [{ axis: 'y', edges: true }, { axis: 'z', edges: true }])
|
|
275
|
+
assert.deepEqual(alignAxesOf(two, 'plane'), [{ axis: 'x', edges: false }, { axis: 'z', edges: false }])
|
|
276
276
|
/* A door on the jamb's right face: bottoms level, fronts level. The x choice is along the face and is not sent. */
|
|
277
|
-
assert.deepEqual(alignOf(two,
|
|
278
|
-
assert.deepEqual(alignOf(two,
|
|
279
|
-
assert.equal(alignOf(two, '
|
|
280
|
-
assert.equal(alignOf(two,
|
|
277
|
+
assert.deepEqual(alignOf(two, right, { x: 'max', y: 'min', z: 'max' }, {}), { y: 'min', z: 'max' })
|
|
278
|
+
assert.deepEqual(alignOf(two, { part: 'jamb', face: 'top' }, { x: 'mm', z: 'centre' }, { x: 120 }), { x: { mm: 120 } })
|
|
279
|
+
assert.equal(alignOf(two, 'plane', { x: 'min' }, {}), undefined, '바닥에는 맞출 모서리가 없다')
|
|
280
|
+
assert.equal(alignOf(two, { part: 'jamb', face: 'top' }, {}, {}), undefined)
|
|
281
281
|
|
|
282
282
|
/* What the form sends is what hangs the part on the edge: the door's pivot is on the jamb's front edge line. */
|
|
283
283
|
let asset = createFigureAsset('Door')
|
|
284
284
|
asset = addFigureBox(asset, { id: 'jamb', size: { x: 100, y: 2100, z: 150 }, position: { x: 0, y: 1050, z: 0 } })
|
|
285
285
|
asset = addFigureBox(asset, { id: 'leaf', size: { x: 900, y: 2000, z: 40 }, position: { x: 600, y: 1000, z: 0 } })
|
|
286
|
-
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'jamb',
|
|
287
|
-
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'leaf',
|
|
286
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'jamb', to: { plane: 'mounting-plane' } })
|
|
287
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'leaf', to: { part: 'jamb', face: 'right' }, align: alignOf(asset, { part: 'jamb', face: 'right' }, { y: 'min', z: 'max' }, {}) })
|
|
288
288
|
const { compileFigureAsset } = await import('@hatiolab/figure-model')
|
|
289
289
|
const leaf = compileFigureAsset(asset).evaluate().geometry.find(g => g.id === 'leaf')!
|
|
290
290
|
assert.deepEqual(leaf.pose.t.map(Math.round), [500, 1000, 55], '문짝 아래가 문틀 아래에, 앞이 문틀 앞에')
|
|
@@ -295,13 +295,13 @@ test('the fastening form asks across the face in rows only where the drawing can
|
|
|
295
295
|
asset = addFigureBox(asset, { id: 'jamb', size: { x: 100, y: 2100, z: 150 }, position: { x: 0, y: 1050, z: 0 } })
|
|
296
296
|
asset = addFigureBox(asset, { id: 'leaf', size: { x: 900, y: 2000, z: 40 }, position: { x: 600, y: 1000, z: 0 } })
|
|
297
297
|
const right = { part: 'jamb', face: 'right' }
|
|
298
|
-
const view = faceViewOf(revisionOf(asset), 'leaf', right)
|
|
298
|
+
const view = faceViewOf(revisionOf(asset), 'leaf', right, 'meet')
|
|
299
299
|
assert.deepEqual(view?.axes, ['y', 'z'])
|
|
300
|
-
assert.deepEqual(alignRowsOf(asset,
|
|
301
|
-
assert.deepEqual(alignRowsOf(asset,
|
|
302
|
-
assert.deepEqual(alignRowsOf(asset,
|
|
300
|
+
assert.deepEqual(alignRowsOf(asset, right, view), [], 'the drawing sets both')
|
|
301
|
+
assert.deepEqual(alignRowsOf(asset, right, undefined), [{ axis: 'y', edges: true }, { axis: 'z', edges: true }], 'with no drawing, both are asked')
|
|
302
|
+
assert.deepEqual(alignRowsOf(asset, right, view, { y: 'leaf.position.y' }), [{ axis: 'y', edges: true }], 'the axis that follows a position field')
|
|
303
303
|
const sized = applyFigureAuthoring(asset, { kind: 'declare-instance-size', range: { x: { min: 100, max: 5000 }, y: { min: 100, max: 5000 }, z: { min: 10, max: 500 } }, at: { x: 1000, y: 2100, z: 150 } })
|
|
304
|
-
assert.deepEqual(alignRowsOf(sized,
|
|
304
|
+
assert.deepEqual(alignRowsOf(sized, right, faceViewOf(revisionOf(sized), 'leaf', right, 'meet')).map(r => r.axis), ['y', 'z'], 'the axes a share of the figure size may set')
|
|
305
305
|
})
|
|
306
306
|
|
|
307
307
|
test('붙여도 제자리 — 폼은 지금 서 있는 자리를 가운데서 거리로 채운다 (문짝이 문틀 앞 100 mm 에서 가운데로 옮겨졌다, 09-26)', async () => {
|
|
@@ -313,9 +313,9 @@ test('붙여도 제자리 — 폼은 지금 서 있는 자리를 가운데서
|
|
|
313
313
|
const where = (a: FigureAsset) => compileFigureAsset(a).evaluate().geometry.find(g => g.id === 'leaf')!.pose.t.map(v => Math.round(v))
|
|
314
314
|
for (const target of ['plane', { part: 'frame', face: 'top' }] as const) {
|
|
315
315
|
const kept = placeKeptOf(revisionOf(asset), 'leaf', target)
|
|
316
|
-
const align = alignOf(asset,
|
|
316
|
+
const align = alignOf(asset, target, kept.align, kept.alignMm)
|
|
317
317
|
const to = target === 'plane' ? { plane: 'mounting-plane' as const } : target
|
|
318
|
-
const fastened = applyFigureAuthoring(asset, { kind: 'attach', part: 'leaf',
|
|
318
|
+
const fastened = applyFigureAuthoring(asset, { kind: 'attach', part: 'leaf', to, ...(align ? { align } : {}) } as never)
|
|
319
319
|
const [before, after] = [where(asset), where(fastened)]
|
|
320
320
|
assert.deepEqual([after[0], after[2]], [before[0], before[2]], `${JSON.stringify(target)}: 앞뒤·좌우는 그대로`)
|
|
321
321
|
}
|
|
@@ -328,8 +328,8 @@ test('붙여도 제자리 — 폼은 지금 서 있는 자리를 가운데서
|
|
|
328
328
|
let row = addFigureBox(createFigureAsset('Row'), { id: 'plate', size: { x: 1000, y: 50, z: 400 }, position: { x: 0, y: 700, z: 0 } })
|
|
329
329
|
row = addFigureBox(row, { id: 'roller', size: { x: 60, y: 60, z: 400 }, position: { x: 0, y: 760, z: 0 } })
|
|
330
330
|
row = addFigureBox(row, { id: 'cap', size: { x: 20, y: 20, z: 20 }, position: { x: 0, y: 800, z: 0 } })
|
|
331
|
-
row = applyFigureAuthoring(row, { kind: 'attach', part: 'roller',
|
|
332
|
-
row = applyFigureAuthoring(row, { kind: 'attach', part: 'cap',
|
|
331
|
+
row = applyFigureAuthoring(row, { kind: 'attach', part: 'roller', to: { part: 'plate', face: 'top' } })
|
|
332
|
+
row = applyFigureAuthoring(row, { kind: 'attach', part: 'cap', to: { part: 'roller', face: 'top' } })
|
|
333
333
|
assert.deepEqual([hangsFrom(row, 'roller', 'plate'), hangsFrom(row, 'cap', 'plate'), hangsFrom(row, 'plate', 'roller')], [true, true, false])
|
|
334
334
|
})
|
|
335
335
|
|
|
@@ -340,10 +340,10 @@ test('따라 움직이는 관절 — 같은 방식으로 움직이고 제 조작
|
|
|
340
340
|
asset = addFigureBox(asset, { id: 'palm', size: { x: 240, y: 70, z: 160 }, position: { x: 0, y: 35, z: 0 } })
|
|
341
341
|
for (const id of ['left', 'right']) asset = addFigureBox(asset, { id, size: { x: 30, y: 140, z: 100 }, position: { x: 0, y: 200, z: 0 } })
|
|
342
342
|
asset = addFigureBox(asset, { id: 'wrist', size: { x: 60, y: 60, z: 60 }, position: { x: 0, y: 400, z: 0 } })
|
|
343
|
-
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'palm',
|
|
344
|
-
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'left',
|
|
345
|
-
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'right',
|
|
346
|
-
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'wrist',
|
|
343
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'palm', to: { plane: 'mounting-plane' } })
|
|
344
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'left', to: { part: 'palm', face: 'top' }, align: { x: 'min' } })
|
|
345
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'right', to: { part: 'palm', face: 'top' }, align: { x: 'max' } })
|
|
346
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'wrist', to: { part: 'palm', face: 'bottom' } })
|
|
347
347
|
const form = { kind: 'slide' as const, axis: 'x' as const, id: 'grip', min: 0, max: 60, follow: '', multiplier: 1, offset: 0 }
|
|
348
348
|
const jointOf = (part: string) => asset.joints!.find(j => j.body1.part === part)!
|
|
349
349
|
asset = applyFigureAuthoring(asset, { kind: 'set-joint', joint: motionJoint(jointOf('left'), form, 'left') })
|
|
@@ -385,8 +385,8 @@ test('면은 도형이 정한다 — 문틀(portal)은 바깥 여섯에 개구
|
|
|
385
385
|
assert.equal(canFasten(asset, 'ball'), false, '공은 붙일 면이 없다')
|
|
386
386
|
assert.equal(canFasten(asset, 'frame'), true)
|
|
387
387
|
assert.deepEqual(facesOf(asset, 'cone'), ['bottom', 'top'])
|
|
388
|
-
assert.deepEqual(alignAxesOf(asset,
|
|
389
|
-
assert.deepEqual(alignAxesOf(asset,
|
|
388
|
+
assert.deepEqual(alignAxesOf(asset, { part: 'frame', face: 'opening-left' }), [{ axis: 'y', edges: true }, { axis: 'z', edges: true }])
|
|
389
|
+
assert.deepEqual(alignAxesOf(asset, { part: 'cone', face: 'top' }), [{ axis: 'x', edges: false }, { axis: 'z', edges: false }], '옆면이 평면이 아니면 모서리도 없다')
|
|
390
390
|
})
|
|
391
391
|
|
|
392
392
|
test('되풀이는 붙어 있고 움직이지 않고 아무것도 매달지 않은 부품에 — 붙인 면을 가로지르는 축으로 (빈틈 B, 2026-09-25)', async () => {
|
|
@@ -395,24 +395,30 @@ test('되풀이는 붙어 있고 움직이지 않고 아무것도 매달지 않
|
|
|
395
395
|
asset = addFigureBox(asset, { id: 'frame', size: { x: 2000, y: 100, z: 600 }, position: { x: 0, y: 0, z: 0 } })
|
|
396
396
|
asset = addFigureBox(asset, { id: 'slat', size: { x: 60, y: 20, z: 560 }, position: { x: 0, y: 0, z: 0 } })
|
|
397
397
|
assert.deepEqual(rowAxesOf(asset, 'slat'), [], '붙지 않은 부품은 되풀이를 권하지 않는다')
|
|
398
|
-
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'frame',
|
|
399
|
-
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'slat',
|
|
398
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'frame', to: { plane: 'mounting-plane' } })
|
|
399
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'slat', to: { part: 'frame', face: 'top' } })
|
|
400
400
|
assert.deepEqual(rowAxesOf(asset, 'frame'), [], '바닥에 선 부품은 채울 길이가 없다')
|
|
401
401
|
assert.deepEqual(rowAxesOf(asset, 'slat'), ['x', 'z'], '윗면을 가로지르는 두 축')
|
|
402
|
+
assert.deepEqual(rowAxesOf(applyFigureAuthoring(asset, { kind: 'attach', part: 'slat', to: { part: 'frame', face: 'front' }, replace: true }), 'slat'), ['x', 'y'], '앞면이면 앞면을 가로지르는 두 축')
|
|
402
403
|
asset = applyFigureAuthoring(asset, { kind: 'make-repeat', part: 'slat', axis: 'x', pitch: 100 })
|
|
403
404
|
assert.deepEqual(rowOf(asset, 'slat'), { axis: 'X', pitch: 100 })
|
|
404
405
|
assert.deepEqual(rowAxesOf(asset, 'slat'), [], '이미 되풀이면 다시 권하지 않는다')
|
|
405
406
|
})
|
|
406
407
|
|
|
407
|
-
test('
|
|
408
|
+
test('돌려 놓인 부품에도 모서리 맞춤을 내놓는다 — 지금 자세의 실제 끝으로 맞춘다 (ADR-0140 3.4)', async () => {
|
|
408
409
|
const { alignAxesOf, alignOf } = await import('../client/modeller/links.ts')
|
|
409
410
|
let asset = createFigureAsset('Fan')
|
|
410
411
|
asset = addFigureBox(asset, { id: 'hub', size: { x: 120, y: 120, z: 80 }, position: { x: 0, y: 0, z: 0 } })
|
|
411
|
-
asset = addFigureBox(asset, { id: 'blade', size: { x: 300, y: 60, z: 8 }, position: { x: 0, y: 0, z: 0 } })
|
|
412
|
+
asset = addFigureBox(asset, { id: 'blade', size: { x: 300, y: 60, z: 8 }, position: { x: 0, y: 0, z: 0 }, rotation: { x: 0, y: 0, z: 30 } })
|
|
412
413
|
const front = { part: 'hub', face: 'front' }
|
|
413
|
-
assert.deepEqual(alignAxesOf(asset,
|
|
414
|
-
assert.deepEqual(alignOf(asset,
|
|
415
|
-
|
|
414
|
+
assert.deepEqual(alignAxesOf(asset, front), [{ axis: 'x', edges: true }, { axis: 'y', edges: true }])
|
|
415
|
+
assert.deepEqual(alignOf(asset, front, { x: 'max', y: 'mm' }, { y: 50 }), { x: 'max', y: { mm: 50 } })
|
|
416
|
+
/* Fastened so, its real reach is level with the hub's right side, turned as it is. */
|
|
417
|
+
const { compileFigureAsset, figureFaceOverhangOf } = await import('@hatiolab/figure-model')
|
|
418
|
+
const on = applyFigureAuthoring(asset, { kind: 'attach', part: 'blade', to: front, align: { x: 'max' } })
|
|
419
|
+
assert.equal(figureFaceOverhangOf(on, 'blade')!.state, 'larger', 'wider than the hub: past its left edge, level with its right')
|
|
420
|
+
assert.deepEqual(figureFaceOverhangOf(on, 'blade')!.sides.filter(s => s.axis === 'x').map(s => s.side), ['low'])
|
|
421
|
+
void compileFigureAsset
|
|
416
422
|
})
|
|
417
423
|
|
|
418
424
|
test('모델러에서 만든 도형은 부품에서 재어 도형 크기를 선언한다 — 그 뒤에야 부품이 도형 크기를 따를 수 있다 (사용자 09-25)', async () => {
|
|
@@ -421,7 +427,7 @@ test('모델러에서 만든 도형은 부품에서 재어 도형 크기를 선
|
|
|
421
427
|
let asset = createFigureAsset('Conveyor')
|
|
422
428
|
assert.equal(instanceRangeToDeclare(asset), undefined, '부품이 없으면 잴 것이 없다')
|
|
423
429
|
asset = addFigureBox(asset, { id: 'frame', size: { x: 2000, y: 100, z: 600 }, position: { x: 0, y: 0, z: 0 } })
|
|
424
|
-
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'frame',
|
|
430
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'frame', to: { plane: 'mounting-plane' } })
|
|
425
431
|
assert.equal(hasInstanceSize(asset, 'x'), false, '새 도형에는 도형 크기가 없다')
|
|
426
432
|
const toDeclare = instanceRangeToDeclare(asset)!
|
|
427
433
|
assert.deepEqual(toDeclare.at, { x: 2000, y: 100, z: 600 }, '지금 부품의 크기가 선언할 크기')
|
|
@@ -456,8 +462,8 @@ test('미끄러짐의 이동 거리가 부품 길이를 따른다 — 레일이
|
|
|
456
462
|
let asset = createFigureAsset('Gantry')
|
|
457
463
|
asset = addFigureBox(asset, { id: 'rail', size: { x: 3000, y: 120, z: 120 }, position: { x: 0, y: 0, z: 0 } })
|
|
458
464
|
asset = addFigureBox(asset, { id: 'bridge', size: { x: 160, y: 160, z: 1300 }, position: { x: 0, y: 0, z: 0 } })
|
|
459
|
-
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'rail',
|
|
460
|
-
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'bridge',
|
|
465
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'rail', to: { plane: 'mounting-plane' } })
|
|
466
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'bridge', to: { part: 'rail', face: 'top' }, align: { x: 'min' } })
|
|
461
467
|
const joint = asset.joints!.find(j => j.body1.part === 'bridge')!
|
|
462
468
|
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 }
|
|
463
469
|
const record = motionJoint(joint, form, 'bridge')
|
|
@@ -480,14 +486,14 @@ test('면 위 거리를 도형 크기 비율로 — 도형이 커지면 그 자
|
|
|
480
486
|
let asset = createFigureAsset('Panel')
|
|
481
487
|
asset = addFigureBox(asset, { id: 'panel', size: { x: 1000, y: 800, z: 40 }, position: { x: 0, y: 0, z: 0 } })
|
|
482
488
|
asset = addFigureBox(asset, { id: 'lamp', size: { x: 60, y: 60, z: 30 }, position: { x: 0, y: 0, z: 0 } })
|
|
483
|
-
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'panel',
|
|
489
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'panel', to: { plane: 'mounting-plane' } })
|
|
484
490
|
const front = { part: 'panel', face: 'front' }
|
|
485
|
-
assert.deepEqual(alignOf(asset,
|
|
491
|
+
assert.deepEqual(alignOf(asset, front, { x: 'share' }, { x: 300 }), undefined, '도형 크기가 없으면 비율을 보낼 수 없다')
|
|
486
492
|
asset = applyFigureAuthoring(asset, { kind: 'declare-instance-size', range: { x: { min: 200, max: 5000 }, y: { min: 200, max: 5000 }, z: { min: 10, max: 500 } }, at: { x: 1000, y: 800, z: 40 } })
|
|
487
493
|
asset = applyFigureAuthoring(asset, { kind: 'link-dimension', part: 'panel', axis: 'x', source: { of: 'instance', axis: 'x' } })
|
|
488
|
-
const align = alignOf(asset,
|
|
494
|
+
const align = alignOf(asset, front, { x: 'share' }, { x: 300 })!
|
|
489
495
|
assert.deepEqual(align, { x: { mm: { from: { of: 'instance', axis: 'x' }, times: 0.3 } } })
|
|
490
|
-
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'lamp',
|
|
496
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'lamp', to: front, align })
|
|
491
497
|
const x = (size: number) => Math.round(compileFigureAsset({ ...asset, designInputs: { ...asset.designInputs, 'size.x': size } }).evaluate().geometry.find(g => g.id === 'lamp')!.pose.t[0]!)
|
|
492
498
|
assert.equal(x(1000), 300)
|
|
493
499
|
assert.equal(x(2000), 600, '도형이 두 배면 거리도 두 배')
|
|
@@ -498,8 +504,8 @@ test('선언할 크기는 부품이 쉬고 있을 때의 크기 — 문짝이
|
|
|
498
504
|
let asset = createFigureAsset('Door')
|
|
499
505
|
asset = addFigureBox(asset, { id: 'jamb', size: { x: 100, y: 2100, z: 150 }, position: { x: 0, y: 0, z: 0 } })
|
|
500
506
|
asset = addFigureBox(asset, { id: 'leaf', size: { x: 890, y: 2095, z: 40 }, position: { x: 0, y: 0, z: 0 } })
|
|
501
|
-
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'jamb',
|
|
502
|
-
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'leaf',
|
|
507
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'jamb', to: { plane: 'mounting-plane' } })
|
|
508
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'leaf', to: { part: 'jamb', face: 'right' }, align: { y: 'min', z: 'max' }, gap: 5 })
|
|
503
509
|
const j = asset.joints!.find(o => o.id === 'leaf')!
|
|
504
510
|
asset = applyFigureAuthoring(asset, { kind: 'set-joint', joint: { ...j, type: 'revolute', axis: 'Y', lowerLimit: -90, upperLimit: 0, state: { id: 'open' } } })
|
|
505
511
|
assert.deepEqual(instanceRangeToDeclare(asset)!.at, { x: 995, y: 2100, z: 150 })
|
|
@@ -508,15 +514,15 @@ test('선언할 크기는 부품이 쉬고 있을 때의 크기 — 문짝이
|
|
|
508
514
|
test('바닥에 선 부품도 도형 크기를 선언한 도형이면 도형의 옆면에 맞출 수 있다 (2026-09-25)', async () => {
|
|
509
515
|
const { alignAxesOf, alignOf } = await import('../client/modeller/links.ts')
|
|
510
516
|
let asset = addFigureBox(createFigureAsset('Gate'), { id: 'post', size: { x: 100, y: 2000, z: 150 }, position: { x: 0, y: 0, z: 0 } })
|
|
511
|
-
assert.deepEqual(alignAxesOf(asset, '
|
|
517
|
+
assert.deepEqual(alignAxesOf(asset, 'plane'), [{ axis: 'x', edges: false }, { axis: 'z', edges: false }], '크기가 없으면 옆면도 없다')
|
|
512
518
|
asset = applyFigureAuthoring(asset, { kind: 'declare-instance-size', range: { x: { min: 200, max: 5000 }, y: { min: 500, max: 5000 }, z: { min: 50, max: 500 } }, at: { x: 1100, y: 2000, z: 150 } })
|
|
513
|
-
assert.deepEqual(alignAxesOf(asset, '
|
|
514
|
-
assert.deepEqual(alignOf(asset, '
|
|
519
|
+
assert.deepEqual(alignAxesOf(asset, 'plane'), [{ axis: 'x', edges: true }, { axis: 'z', edges: true }])
|
|
520
|
+
assert.deepEqual(alignOf(asset, 'plane', { x: 'min' }, {}), { x: 'min' })
|
|
515
521
|
})
|
|
516
522
|
|
|
517
523
|
test('움직임은 그 자리에서 고친다 — 폼에 지금 관절을 채우고, 그대로 보내면 같은 관절이다 (2026-09-25)', async () => {
|
|
518
524
|
const { motionFormOf, motionJoint } = await import('../client/modeller/links.ts')
|
|
519
|
-
const base = { id: 'j', body0: { part: 'a', face: 'top' as const }, body1: { part: 'b'
|
|
525
|
+
const base = { id: 'j', body0: { part: 'a', face: 'top' as const }, body1: { part: 'b' } }
|
|
520
526
|
const joints = [
|
|
521
527
|
{ ...base, type: 'prismatic' as const, axis: 'Z' as const, lowerLimit: 0, upperLimit: 2500, state: { id: 'extend' } },
|
|
522
528
|
{ ...base, type: 'revolute' as const, axis: 'Y' as const, lowerLimit: -90, upperLimit: 0, state: { id: 'open' } },
|
|
@@ -540,25 +546,21 @@ test('바닥에 선 부품도 무엇에 붙어 있는지 읽는다 — 그래야
|
|
|
540
546
|
asset = addFigureBox(asset, { id: 'jamb', size: { x: 100, y: 2100, z: 150 }, position: { x: 0, y: 0, z: 0 } })
|
|
541
547
|
asset = addFigureBox(asset, { id: 'head', size: { x: 1100, y: 100, z: 150 }, position: { x: 0, y: 0, z: 0 } })
|
|
542
548
|
assert.equal(heldBy(asset, 'jamb'), null)
|
|
543
|
-
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'jamb',
|
|
544
|
-
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'head',
|
|
549
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'jamb', to: { plane: 'mounting-plane' } })
|
|
550
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'head', to: { part: 'jamb', face: 'top' } })
|
|
545
551
|
assert.deepEqual(heldBy(asset, 'jamb'), { plane: true }, '장착 평면')
|
|
546
552
|
assert.deepEqual(heldBy(asset, 'head'), { part: 'jamb' })
|
|
547
553
|
/* What the form then sends: a fastening that replaces the one there. */
|
|
548
|
-
assert.doesNotThrow(() => applyFigureAuthoring(asset, { kind: 'attach', part: 'jamb',
|
|
554
|
+
assert.doesNotThrow(() => applyFigureAuthoring(asset, { kind: 'attach', part: 'jamb', to: { plane: 'mounting-plane' }, align: { x: { mm: -500 } }, replace: !!heldBy(asset, 'jamb') }))
|
|
549
555
|
})
|
|
550
556
|
|
|
551
|
-
test('벽 배치 — 장착 평면은 Z = 0,
|
|
557
|
+
test('벽 배치 — 장착 평면은 Z = 0, 벽에서 나와 서고 가로질러 X · Y 로 맞춘다(사용자 · 아키텍트 09-25, ADR-0140 3.11)', async () => {
|
|
552
558
|
const { mountingPlaneOf, alignAxesOf } = await import('../client/modeller/links.ts')
|
|
553
559
|
let a = addFigureBox(createFigureAsset('Lamp'), { id: 'plate', size: { x: 200, y: 300, z: 20 }, position: { x: 0, y: 0, z: 0 } })
|
|
554
|
-
assert.deepEqual(mountingPlaneOf(a), { normal: 'y',
|
|
560
|
+
assert.deepEqual(mountingPlaneOf(a), { normal: 'y', facings: ['meet', 'flush'] }, '배치 선언 전에는 바닥 — 올려 서거나 아래에 매달림')
|
|
555
561
|
a = applyFigureAuthoring(a, { kind: 'declare-occupancy', proposal: (await import('@hatiolab/figure-model')).proposeFigureOccupancy(a), placement: 'wall' as never })
|
|
556
|
-
assert.deepEqual(mountingPlaneOf(a), { normal: 'z',
|
|
557
|
-
assert.deepEqual(alignAxesOf(a, '
|
|
558
|
-
assert.deepEqual(ownFacesOf(a, 'plate', true), ['back'], 'on the plane, only the face the plane takes')
|
|
559
|
-
assert.deepEqual(ownFacesOf(a, 'plate', false), ['left', 'right', 'bottom', 'top', 'back', 'front', 'centre'], 'to a part, its six outer faces and its centre')
|
|
560
|
-
const floor = addFigureBox(createFigureAsset('Floor'), { id: 'plate', size: { x: 200, y: 300, z: 20 }, position: { x: 0, y: 0, z: 0 } })
|
|
561
|
-
assert.deepEqual(ownFacesOf(floor, 'plate', true), ['bottom', 'top'], 'on the floor, its bottom or its top')
|
|
562
|
+
assert.deepEqual(mountingPlaneOf(a), { normal: 'z', facings: ['meet'] })
|
|
563
|
+
assert.deepEqual(alignAxesOf(a, 'plane').map(x => x.axis), ['x', 'y'])
|
|
562
564
|
})
|
|
563
565
|
|
|
564
566
|
test('부품 칸의 두 제목이 같은 말이 아니다 — 자리 숫자는 「위치와 크기」, 붙이기 칸은 「붙이기」 (09-26)', () => {
|
|
@@ -578,8 +580,8 @@ test('보이는 때 — 명령이 남긴 것을 읽고, 폼은 그 명령을 낸
|
|
|
578
580
|
asset = addFigureBox(asset, { id: 'frame', size: { x: 1200, y: 60, z: 200 }, position: { x: 0, y: 30, z: 0 } })
|
|
579
581
|
asset = addFigureBox(asset, { id: 'door', size: { x: 1000, y: 2000, z: 40 }, position: { x: 0, y: 1060, z: 0 } })
|
|
580
582
|
asset = addFigureBox(asset, { id: 'lamp', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 2200, z: 0 } })
|
|
581
|
-
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'frame',
|
|
582
|
-
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'door',
|
|
583
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'frame', to: { plane: 'mounting-plane' } })
|
|
584
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'door', to: { part: 'frame', face: 'top' } })
|
|
583
585
|
assert.deepEqual(visibilityControlsOf(asset), [], '조작값이 없으면 고를 것도 없다')
|
|
584
586
|
asset = moving(asset, 'door', { kind: 'slide', axis: 'x', id: 'gate-open', min: 0, max: 900 })
|
|
585
587
|
assert.deepEqual(visibilityControlsOf(asset).map(c => [c.id, c.min, c.max, c.unit]), [['gate-open', 0, 900, 'mm']])
|
|
@@ -629,20 +631,18 @@ import { wordsOf } from './words.ts'
|
|
|
629
631
|
|
|
630
632
|
test('붙이기 칸은 부품 제 붙임으로 열린다 — 가운데로 붙은 롤러는 가운데 · 상대 · 간격 그대로', () => {
|
|
631
633
|
let asset = addFigureBox(createFigureAsset('Rollers'), { id: 'bed', size: { x: 2000, y: 400, z: 600 }, position: { x: 0, y: 200, z: 0 } })
|
|
632
|
-
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'bed',
|
|
634
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'bed', to: { plane: 'mounting-plane' } })
|
|
633
635
|
asset = addFigurePrimitive(asset, { id: 'roller', kind: 'cylinder', dimensions: { radius: 40, length: 560 }, position: { x: 0, y: 0, z: 0 }, rotation: { x: 90, y: 0, z: 0 } })
|
|
634
|
-
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'roller',
|
|
636
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'roller', facing: 'centre', to: { part: 'bed', face: 'top' }, gap: 40, align: { x: 'min' } })
|
|
635
637
|
const roller = fasteningOf(revisionOf(asset), 'roller')!
|
|
636
|
-
assert.equal(roller.
|
|
638
|
+
assert.equal(roller.facing, 'centre')
|
|
637
639
|
assert.equal(roller.target, 'bed')
|
|
638
640
|
assert.equal(roller.targetFace, 'top')
|
|
639
641
|
assert.equal(roller.gap, 40)
|
|
640
642
|
assert.equal(roller.align.x, 'min', '기록의 정렬')
|
|
641
643
|
const bed = fasteningOf(revisionOf(asset), 'bed')!
|
|
642
|
-
assert.deepEqual([bed.
|
|
644
|
+
assert.deepEqual([bed.facing, bed.toPlane], ['meet', true])
|
|
643
645
|
assert.equal(fasteningOf(revisionOf(addFigureBox(createFigureAsset('Loose'), { id: 'b', size: { x: 1, y: 1, z: 1 }, position: { x: 0, y: 0, z: 0 } })), 'b'), null, '안 붙은 부품은 기본값으로')
|
|
644
|
-
assert.equal(ownFacesOf(asset, 'roller', false).at(-1), 'centre', 'its own faces include its centre')
|
|
645
|
-
assert.ok(!ownFacesOf(asset, 'roller', true).includes('centre'), 'on the floor, by a face only')
|
|
646
646
|
})
|
|
647
647
|
|
|
648
648
|
test('a worked-out gap is kept by the fastening form: opening it on the part moves nothing (the robot dog, the forklift, 2026-10-13)', async () => {
|
|
@@ -652,7 +652,7 @@ test('a worked-out gap is kept by the fastening form: opening it on the part mov
|
|
|
652
652
|
Object.fromEntries((compileFigureAsset(asset).evaluate().geometry as unknown as Array<{ id: string; pose?: { t: number[] } }>).map(g => [g.id, g.pose?.t.map(v => Math.round(v * 1000) / 1000)]))
|
|
653
653
|
/* What the form sends when nothing in it is changed: the attach it would make of the part's own fastening. */
|
|
654
654
|
const reattach = (asset: FigureAsset, part: string, form: Parameters<typeof fastenGapOf>[0]) =>
|
|
655
|
-
applyFigureAuthoring(asset, { kind: 'attach', part,
|
|
655
|
+
applyFigureAuthoring(asset, { kind: 'attach', part, to: { plane: 'mounting-plane' }, ...fastenGapOf(form), replace: true } as never)
|
|
656
656
|
|
|
657
657
|
const dog = robotDogFigure().asset
|
|
658
658
|
const body = fasteningOf(revisionOf(dog), 'body')!
|
|
@@ -665,7 +665,7 @@ test('a worked-out gap is kept by the fastening form: opening it on the part mov
|
|
|
665
665
|
let lift = addFigureBox(createFigureAsset('Lift'), { id: 'body', size: { x: 1000, y: 800, z: 600 }, position: { x: 0, y: 500, z: 0 } })
|
|
666
666
|
lift.document.model.inputs.push({ id: 'size.y', unit: 'mm', min: 100, max: 20000, role: 'design' } as never)
|
|
667
667
|
lift.designInputs['size.y'] = 1800
|
|
668
|
-
lift = applyFigureAuthoring(lift, { kind: 'attach', part: 'body',
|
|
668
|
+
lift = applyFigureAuthoring(lift, { kind: 'attach', part: 'body', to: { plane: 'mounting-plane' }, gap: { from: { of: 'instance', axis: 'y' }, times: 0.05 } } as never)
|
|
669
669
|
const kept = fasteningOf(revisionOf(lift), 'body')!
|
|
670
670
|
assert.deepEqual(kept.gapKept, { from: { of: 'instance', axis: 'y' }, times: 0.05 })
|
|
671
671
|
assert.deepEqual(posesOf(reattach(lift, 'body', kept)), posesOf(lift))
|