@things-factory/figure-ui 10.1.200 → 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 +5 -5
- 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 +5 -5
- 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/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 +45 -23
- 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
package/test/fasten-form.test.ts
CHANGED
|
@@ -14,7 +14,7 @@ import { revisionOf } from './revision-of.ts'
|
|
|
14
14
|
|
|
15
15
|
const box = (asset: FigureAsset, id: string, [x, y, z]: number[], at = [0, 0, 0]) => addFigureBox(asset, { id, size: { x, y, z }, position: { x: at[0]!, y: at[1]!, z: at[2]! } })
|
|
16
16
|
const fasten = (asset: FigureAsset, part: string, to: string, more = {}) =>
|
|
17
|
-
applyFigureAuthoring(asset, { kind: 'attach', part,
|
|
17
|
+
applyFigureAuthoring(asset, { kind: 'attach', part, to: to === 'plane' ? { plane: 'mounting-plane' } : { part: to, face: 'top' }, ...more } as never)
|
|
18
18
|
|
|
19
19
|
/* A frame on the floor, a roller on the frame 300 mm off its centre, a loose lid beside them. */
|
|
20
20
|
function conveyor() {
|
|
@@ -32,12 +32,20 @@ test('opened on what the part has; a loose part opens on nothing chosen', () =>
|
|
|
32
32
|
assert.equal(fastenPlanOf(r, 'lid', fastenFormOf(r, 'lid')).action, undefined, 'nothing to apply until something is chosen')
|
|
33
33
|
})
|
|
34
34
|
|
|
35
|
+
test('on the plane: a floor takes a part standing on it or hanging below it, a wall only one standing out of it', () => {
|
|
36
|
+
const loose = addFigureBox(createFigureAsset('Lamp'), { id: 'lamp', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 0, z: 0 } })
|
|
37
|
+
const hung = { ...freshFasten(), toPlane: true, facing: 'flush' as const }
|
|
38
|
+
assert.equal((fastenPlanOf(revisionOf(loose), 'lamp', hung).action as { facing?: string }).facing, 'flush')
|
|
39
|
+
const wall = { ...loose, placement: 'wall' as const }
|
|
40
|
+
assert.equal((fastenPlanOf(revisionOf(wall), 'lamp', hung).action as { facing?: string }).facing, undefined, 'a wall: standing out of it')
|
|
41
|
+
})
|
|
42
|
+
|
|
35
43
|
test('the one it has, unchanged, comes to the attach that replaces it with the same place across the face', () => {
|
|
36
44
|
const r = conveyor()
|
|
37
45
|
const plan = fastenPlanOf(r, 'roller', fastenFormOf(r, 'roller'))
|
|
38
|
-
assert.deepEqual(plan.action, { kind: 'attach', part: 'roller',
|
|
46
|
+
assert.deepEqual(plan.action, { kind: 'attach', part: 'roller', to: { part: 'frame', face: 'top' }, facing: 'meet', align: { x: { mm: 300 } }, replace: true })
|
|
39
47
|
assert.deepEqual(plan.face, { part: 'frame', face: 'top' }, 'the face it meets, lit while tried')
|
|
40
|
-
assert.equal(fastenSentenceOf(
|
|
48
|
+
assert.equal(fastenSentenceOf(fastenFormOf(r, 'roller'), plan, '바닥', say), 'figure.text.fasten-sentence-meet(to=frame의 figure.label.face-top())')
|
|
41
49
|
})
|
|
42
50
|
|
|
43
51
|
test('aimed at a part, the loose lid keeps its place across the face; aimed at the plane, nothing is said of a part face', () => {
|
|
@@ -1,52 +1,43 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* **붙이기 그림이 읽는 것** (사용자 2026-10-01: 「면을 선택하는 것이 복잡해보인다」).
|
|
3
3
|
*
|
|
4
|
-
* 붙이기는 상대 면과 방식(올려놓기 · 같은 면 · 가운데를 면에)만 고른다.
|
|
5
|
-
*
|
|
6
|
-
* 받아 부품을 정말 그 자리에 두는지까지 잰다.
|
|
4
|
+
* 붙이기는 상대 면과 방식(올려놓기 · 같은 면 · 가운데를 면에)만 고른다. 부품은 제 면 없이 지금 자세의 실제 모양으로 닿는다
|
|
5
|
+
* (ADR-0140 3.2) — 그림의 발자국도 그 모양을 면 위 두 축으로 잰 것이다(figure-model `figureFaceMeasureOf`). 면 위에 놓은 자리는
|
|
6
|
+
* 가운데·안쪽 끝·바깥 끝에 붙고, 그 밖은 mm 다. figure-model 이 그 명령을 받아 부품을 정말 그 자리에 두는지까지 잰다.
|
|
7
7
|
*/
|
|
8
8
|
import { test } from 'node:test'
|
|
9
9
|
import assert from 'node:assert/strict'
|
|
10
10
|
|
|
11
|
-
import { addFigureBox, applyFigureAuthoring, compileFigureAsset, createFigureAsset
|
|
11
|
+
import { addFigureBox, applyFigureAuthoring, compileFigureAsset, createFigureAsset } from '@hatiolab/figure-model'
|
|
12
12
|
import type { FigureAsset, FigureAuthoringAction } from '@hatiolab/figure-model'
|
|
13
13
|
|
|
14
|
-
import { alignmentAt, faceViewOf,
|
|
14
|
+
import { alignmentAt, faceViewOf, offsetOf, overhangOf, screenOf } from '../client/modeller/fasten-geometry.ts'
|
|
15
15
|
import { revisionOf } from './revision-of.ts'
|
|
16
16
|
|
|
17
17
|
const scene = (lidRotation?: { x: number; y: number; z: number }): FigureAsset => {
|
|
18
18
|
let a = addFigureBox(createFigureAsset('Box'), { id: 'bin', size: { x: 600, y: 300, z: 400 }, position: { x: 0, y: 150, z: 0 } })
|
|
19
|
-
a = applyFigureAuthoring(a, { kind: 'attach', part: 'bin',
|
|
19
|
+
a = applyFigureAuthoring(a, { kind: 'attach', part: 'bin', to: { plane: 'mounting-plane' } })
|
|
20
20
|
return addFigureBox(a, { id: 'lid', size: { x: 300, y: 20, z: 200 }, position: { x: 0, y: 400, z: 0 }, ...(lidRotation ? { rotation: lidRotation } : {}) })
|
|
21
21
|
}
|
|
22
22
|
const at = (a: FigureAsset, id: string) => (compileFigureAsset(a).evaluate().geometry.find(g => (g as { id: string }).id === id) as unknown as { pose: { t: number[] } }).pose.t
|
|
23
23
|
|
|
24
|
-
test('부품의 면은 상대 면과 방식으로 정해진다: 올려놓기는 마주 보는 면, 같은 면은 같은 면, 가운데는 가운데', () => {
|
|
25
|
-
const a = scene()
|
|
26
|
-
assert.equal(myFaceFor(revisionOf(a), 'lid', { part: 'bin', face: 'top' }, 'meet'), 'bottom')
|
|
27
|
-
assert.equal(myFaceFor(revisionOf(a), 'lid', { part: 'bin', face: 'top' }, 'flush'), 'top')
|
|
28
|
-
assert.equal(myFaceFor(revisionOf(a), 'lid', { part: 'bin', face: 'front' }, 'meet'), 'back')
|
|
29
|
-
assert.equal(myFaceFor(revisionOf(a), 'lid', { part: 'bin', face: 'top' }, 'centre'), 'centre')
|
|
30
|
-
assert.equal(myFaceFor(revisionOf(a), 'lid', 'plane', 'meet'), 'bottom', 'on a floor: the face looking down')
|
|
31
|
-
assert.equal(myFaceFor(revisionOf(a), 'lid', 'plane', 'centre'), undefined, 'the plane takes a face, never a centre')
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
test('돌려 놓인 부품이면 지금 아래를 보는 면이다 — 이름이 아니라 방향으로', () => {
|
|
35
|
-
const a = scene({ x: 90, y: 0, z: 0 })
|
|
36
|
-
const face = myFaceFor(revisionOf(a), 'lid', { part: 'bin', face: 'top' }, 'meet')!
|
|
37
|
-
assert.notEqual(face, 'bottom', 'turned a quarter about X, its bottom no longer looks down')
|
|
38
|
-
const n = figurePartFaceOf(a, 'lid', face)!.normal
|
|
39
|
-
assert.ok(Math.abs(n[1]! + 1) < 1e-6, `${face} looks down: ${n}`)
|
|
40
|
-
})
|
|
41
|
-
|
|
42
24
|
test('면 그림: 상대 면의 크기와 부품 발자국, 가장자리 맞춤이 되는 축', () => {
|
|
43
|
-
const v = faceViewOf(revisionOf(scene()), 'lid', { part: 'bin', face: 'top' })!
|
|
25
|
+
const v = faceViewOf(revisionOf(scene()), 'lid', { part: 'bin', face: 'top' }, 'meet')!
|
|
44
26
|
assert.deepEqual(v.axes, ['x', 'z'])
|
|
45
27
|
assert.deepEqual([v.face.x, v.face.z], [600, 400])
|
|
46
28
|
assert.deepEqual([v.part.x, v.part.z], [300, 200])
|
|
47
29
|
assert.deepEqual(v.edges, { x: true, z: true })
|
|
48
30
|
})
|
|
49
31
|
|
|
32
|
+
test('돌려 놓인 부품의 발자국은 지금 자세의 실제 모양이다 — 제 면 이름이 아니라', () => {
|
|
33
|
+
/* Turned a quarter about X: its 20 mm thickness now lies along Z across the face. */
|
|
34
|
+
const v = faceViewOf(revisionOf(scene({ x: 90, y: 0, z: 0 })), 'lid', { part: 'bin', face: 'top' }, 'meet')!
|
|
35
|
+
assert.deepEqual([v.part.x, v.part.z].map(Math.round), [300, 20])
|
|
36
|
+
/* Turned 30° about Y: its footprint is the turned box's reach, 300 cos 30 + 200 sin 30 across X. */
|
|
37
|
+
const turned = faceViewOf(revisionOf(scene({ x: 0, y: 30, z: 0 })), 'lid', { part: 'bin', face: 'top' }, 'meet')!
|
|
38
|
+
assert.ok(Math.abs(turned.part.x - (300 * Math.cos(Math.PI / 6) + 200 * 0.5)) < 1e-6, String(turned.part.x))
|
|
39
|
+
})
|
|
40
|
+
|
|
50
41
|
test('놓은 자리: 가운데, 안쪽 끝(가장자리와 나란히), 바깥 끝(가장자리에 닿게), 그 밖은 mm', () => {
|
|
51
42
|
assert.deepEqual(alignmentAt(4, 600, 300, true, 10), { how: 'centre', caught: 'centre' })
|
|
52
43
|
assert.deepEqual(alignmentAt(146, 600, 300, true, 10), { how: 'max', caught: 'inside' })
|
|
@@ -58,18 +49,18 @@ test('놓은 자리: 가운데, 안쪽 끝(가장자리와 나란히), 바깥
|
|
|
58
49
|
})
|
|
59
50
|
|
|
60
51
|
test('면 밖으로 나온 곳과 닿지 않는 것을 말한다', () => {
|
|
61
|
-
const v = faceViewOf(revisionOf(scene()), 'lid', { part: 'bin', face: 'top' })!
|
|
62
|
-
assert.deepEqual(overhangOf(v, { x: 0, y: 0, z: 0 }), { touching: true, sides: [] })
|
|
63
|
-
assert.deepEqual(overhangOf(v, { x: 200, y: 0, z: 0 }), { touching: true, sides: [{ axis: 'x', side: 'high', mm: 50 }] })
|
|
64
|
-
assert.
|
|
52
|
+
const v = faceViewOf(revisionOf(scene()), 'lid', { part: 'bin', face: 'top' }, 'meet')!
|
|
53
|
+
assert.deepEqual(overhangOf(v, { x: 0, y: 0, z: 0 }), { touching: true, contact: 'on', sides: [] })
|
|
54
|
+
assert.deepEqual(overhangOf(v, { x: 200, y: 0, z: 0 }), { touching: true, contact: 'on', sides: [{ axis: 'x', side: 'high', mm: 50 }] })
|
|
55
|
+
assert.deepEqual(overhangOf(v, { x: 460, y: 0, z: 0 }), { touching: false, contact: 'off', sides: [{ axis: 'x', side: 'high', mm: 310 }] })
|
|
65
56
|
})
|
|
66
57
|
|
|
67
58
|
test('그 자리가 명령이 되면 figure-model 이 부품을 정말 거기 둔다: 오른쪽 끝, 같은 면, 바깥 끝', () => {
|
|
68
59
|
const a = scene()
|
|
69
60
|
const attach = (extra: Partial<Extract<FigureAuthoringAction, { kind: 'attach' }>>) =>
|
|
70
|
-
applyFigureAuthoring(a, { kind: 'attach', part: 'lid',
|
|
61
|
+
applyFigureAuthoring(a, { kind: 'attach', part: 'lid', to: { part: 'bin', face: 'top' }, ...extra } as FigureAuthoringAction)
|
|
71
62
|
assert.deepEqual(at(attach({ align: { x: 'max' } }), 'lid').map(Math.round), [150, 310, 0])
|
|
72
|
-
assert.deepEqual(at(attach({
|
|
63
|
+
assert.deepEqual(at(attach({ facing: 'flush' }), 'lid').map(Math.round), [0, 290, 0])
|
|
73
64
|
assert.deepEqual(at(attach({ align: { x: { mm: 450 } } }), 'lid').map(Math.round), [450, 310, 0], 'past the edge: fastened all the same')
|
|
74
65
|
})
|
|
75
66
|
|
|
@@ -1,14 +1,24 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* 정렬·간격 고치기 (fasten-geometry `insideAlignmentOf`): the alignment that brings a part inside the face it is fastened
|
|
3
|
-
* on -- the least move on each side it is out, the centre when it is wider -- or why there is nothing to move.
|
|
3
|
+
* on -- the least move on each side it is out, the centre when it is wider -- or why there is nothing to move. The move
|
|
4
|
+
* is figure-model's (`figureFaceInsideAlignOf`), measured on the part's real shape; this reads it in the form's words.
|
|
4
5
|
*/
|
|
5
6
|
import { test } from 'node:test'
|
|
6
7
|
import assert from 'node:assert/strict'
|
|
7
8
|
|
|
8
|
-
import {
|
|
9
|
+
import { addFigureBox, applyFigureAuthoring, createFigureAsset } from '@hatiolab/figure-model'
|
|
10
|
+
|
|
11
|
+
import { faceViewOf, insideAlignmentOf, type FaceView } from '../client/modeller/fasten-geometry.js'
|
|
12
|
+
import { revisionOf } from './revision-of.ts'
|
|
9
13
|
|
|
10
14
|
/* A 400 × 1000 top, a 100 × 100 footprint on it; edges offered on X, a distance only on Z. */
|
|
11
|
-
const view = (part = { x: 100, y: 100, z: 100 }): FaceView =>
|
|
15
|
+
const view = (part = { x: 100, y: 100, z: 100 }): FaceView => {
|
|
16
|
+
let a = addFigureBox(createFigureAsset('Top'), { id: 'top', size: { x: 400, y: 40, z: 1000 }, position: { x: 0, y: 20, z: 0 } })
|
|
17
|
+
a = applyFigureAuthoring(a, { kind: 'attach', part: 'top', to: { plane: 'mounting-plane' } })
|
|
18
|
+
a = addFigureBox(a, { id: 'box', size: part, position: { x: 0, y: 200, z: 0 } })
|
|
19
|
+
const v = faceViewOf(revisionOf(a), 'box', { part: 'top', face: 'top' }, 'meet')!
|
|
20
|
+
return { ...v, edges: { x: true, y: true, z: false } }
|
|
21
|
+
}
|
|
12
22
|
|
|
13
23
|
test('out past the low edge: moved to that edge from inside, the other axis left as it was', () => {
|
|
14
24
|
const fixed = insideAlignmentOf(view(), { x: 'mm', z: 'centre' }, { x: -1000 })
|
|
@@ -28,5 +38,7 @@ test('wider than the face: centred, the most of it on the face', () => {
|
|
|
28
38
|
test('nothing of it out: inside -- what is out is fastened on it; no face size: unmeasured', () => {
|
|
29
39
|
assert.deepEqual(insideAlignmentOf(view(), { x: 'max', z: 'centre' }, {}), { cannot: 'inside' })
|
|
30
40
|
assert.deepEqual(insideAlignmentOf(undefined, {}, {}), { cannot: 'unmeasured' })
|
|
31
|
-
|
|
41
|
+
/* The plane of a figure that declares no size has no edges to measure against. */
|
|
42
|
+
const loose = addFigureBox(createFigureAsset('Loose'), { id: 'box', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 } })
|
|
43
|
+
assert.deepEqual(insideAlignmentOf(faceViewOf(revisionOf(loose), 'box', 'plane', 'meet'), {}, {}), { cannot: 'unmeasured' })
|
|
32
44
|
})
|
package/test/gizmo.test.ts
CHANGED
|
@@ -53,7 +53,7 @@ test('다른 부품에 붙인 부품은 옮기지 않는다 — 다음 평가가
|
|
|
53
53
|
/* attach takes box-shaped parts only. */
|
|
54
54
|
const lidded = addFigureBox(probe(), { id: 'lid', size: { x: 100, y: 20, z: 100 }, position: { x: 0, y: 300, z: 0 } })
|
|
55
55
|
assert.equal(gizmoReachOf(lidded as never, 'lid').translate, true, '전제: 붙이기 전에는 옮길 수 있다')
|
|
56
|
-
const fastened = applyFigureAuthoring(lidded, { kind: 'attach', part: 'lid',
|
|
56
|
+
const fastened = applyFigureAuthoring(lidded, { kind: 'attach', part: 'lid', to: { part: 'body', face: 'top' } })
|
|
57
57
|
assert.equal(gizmoReachOf(fastened as never, 'lid').translate, false)
|
|
58
58
|
})
|
|
59
59
|
|
|
@@ -98,7 +98,7 @@ test('기즈모의 말은 다섯 말 모두에 있다', () => {
|
|
|
98
98
|
test('떼어 낸 부품은 다시 옮길 수 있다 — 움직임이 있는 부품은 아니다', () => {
|
|
99
99
|
let asset = probe()
|
|
100
100
|
asset = addFigureBox(asset, { id: 'lid', size: { x: 100, y: 20, z: 100 }, position: { x: 0, y: 300, z: 0 } })
|
|
101
|
-
const fastened = applyFigureAuthoring(asset, { kind: 'attach', part: 'lid',
|
|
101
|
+
const fastened = applyFigureAuthoring(asset, { kind: 'attach', part: 'lid', to: { part: 'body', face: 'top' } })
|
|
102
102
|
const free = applyFigureAuthoring(fastened, { kind: 'detach', part: 'lid' })
|
|
103
103
|
assert.equal(gizmoReachOf(free as never, 'lid').translate, true)
|
|
104
104
|
const slid = moving(free, 'lid', { kind: 'slide', axis: 'x', id: 'lid-slide', min: 0, max: 100, start: 0 })
|
package/test/joint-drag.test.ts
CHANGED
|
@@ -14,10 +14,10 @@ function column(joint: Partial<FigureJoint>): FigureAsset {
|
|
|
14
14
|
let asset = createFigureAsset('Column')
|
|
15
15
|
asset = addFigureBox(asset, { id: 'post', size: { x: 100, y: 200, z: 100 }, position: { x: 0, y: 100, z: 0 } })
|
|
16
16
|
asset = addFigureBox(asset, { id: 'link', size: { x: 60, y: 300, z: 60 }, position: { x: 0, y: 350, z: 0 } })
|
|
17
|
-
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'post',
|
|
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', type: 'fixed', body0: { part: 'post', face: 'top' }, body1: { part: 'link'
|
|
20
|
+
joint: { id: 'bend', type: 'fixed', body0: { part: 'post', face: 'top' }, body1: { part: 'link' }, ...joint } as FigureJoint
|
|
21
21
|
})
|
|
22
22
|
}
|
|
23
23
|
|
|
@@ -61,6 +61,17 @@ test('고정 관절에 달린 부품을 옮기면 관절이 놓인 자리 —
|
|
|
61
61
|
assert.deepEqual((aside as { joint: FigureJoint }).joint.origin, { align: { x: { mm: 30 } } })
|
|
62
62
|
})
|
|
63
63
|
|
|
64
|
+
test('면 밖으로 끈 만큼 따라온다 — 맞닿음 · 가운데는 간격이 늘고, 같은 면은 줄어든다 (ADR-0140 3.11)', () => {
|
|
65
|
+
for (const facing of ['meet', 'flush', 'centre'] as const) {
|
|
66
|
+
const asset = column({ origin: { facing } })
|
|
67
|
+
const part = drawn(asset, 'link')
|
|
68
|
+
const up = jointDragOf(asset, 'link', part as never, { position: [part.pose.t[0]!, part.pose.t[1]! + 25, part.pose.t[2]!] }, {}) as { joint: FigureJoint }
|
|
69
|
+
assert.equal(up.joint.origin!.gap, facing === 'flush' ? -25 : 25, facing)
|
|
70
|
+
const moved = drawn(applyFigureAuthoring(asset, { kind: 'set-joint', joint: up.joint }), 'link')
|
|
71
|
+
assert.ok(Math.abs(moved.pose.t[1]! - (part.pose.t[1]! + 25)) < 1e-6, `${facing}: the link where it was dragged to`)
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
|
|
64
75
|
test('관계를 끊는 끌기는 거절하고 까닭을 댄다', () => {
|
|
65
76
|
const edge = column({ origin: { align: { x: 'min' } } })
|
|
66
77
|
const part = drawn(edge, 'link')
|
package/test/joint-fan.test.ts
CHANGED
|
@@ -21,7 +21,7 @@ function arm(): FigureAsset {
|
|
|
21
21
|
asset = addFigureBox(asset, { id: 'base', size: { x: 200, y: 100, z: 200 }, position: { x: 0, y: 50, z: 0 } })
|
|
22
22
|
asset = addFigureBox(asset, { id: 'arm', size: { x: 400, y: 50, z: 50 }, position: { x: 200, y: 125, z: 0 } })
|
|
23
23
|
/* On the base's top, level with its left edge: it turns about that edge, at x = -100. */
|
|
24
|
-
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'arm',
|
|
24
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'arm', to: { part: 'base', face: 'top' }, align: { x: 'min' } })
|
|
25
25
|
return moving(asset, 'arm', { kind: 'turn', axis: 'y', id: 'arm.turn', min: -45, max: 90 })
|
|
26
26
|
}
|
|
27
27
|
const PIVOT = [-100, 100, 0]
|
|
@@ -32,18 +32,18 @@ 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', 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'
|
|
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
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'
|
|
38
|
+
return set(a, { id: 'cap', type: 'fixed', 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', 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'
|
|
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 })
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
const reopen = (asset: FigureAsset) => parseFigureAsset(serializeFigureAsset(asset) as unknown as string)
|
|
@@ -63,15 +63,13 @@ function fasteningSave(asset: FigureAsset, part: string): FigureAuthoringAction
|
|
|
63
63
|
const f = fasteningOf(revisionOf(asset), part) as Dynamic
|
|
64
64
|
if (!f || f.atOrigin) return null
|
|
65
65
|
const aim = f.toPlane ? ('plane' as const) : { part: f.target, face: f.targetFace }
|
|
66
|
-
const align = alignOf(asset,
|
|
66
|
+
const align = alignOf(asset, aim, f.align, f.alignMm, f.alignInput)
|
|
67
67
|
return {
|
|
68
68
|
kind: 'attach',
|
|
69
69
|
part,
|
|
70
|
-
face: f.face,
|
|
71
70
|
to: f.toPlane ? { plane: 'mounting-plane' } : { part: f.target, face: f.targetFace },
|
|
72
|
-
...(f.toPlane ? {} : { facing: f.facing }),
|
|
71
|
+
...(f.toPlane ? (f.facing === 'flush' ? { facing: f.facing } : {}) : { facing: f.facing }),
|
|
73
72
|
...(f.gap ? { gap: f.gap } : {}),
|
|
74
|
-
twist: f.twist,
|
|
75
73
|
...(align ? { align } : {}),
|
|
76
74
|
replace: true
|
|
77
75
|
} as FigureAuthoringAction
|
|
@@ -115,15 +113,26 @@ test('the axis reader: a token is its axis, a vector its unit vector, unrounded;
|
|
|
115
113
|
test('a follower is not a turn without end: it has no limits because its leader gives its range', () => {
|
|
116
114
|
let a = box(createFigureAsset('Pair'), 'a', 100, 100, 100, 50)
|
|
117
115
|
a = box(a, 'b', 50, 50, 50, 125)
|
|
118
|
-
a = set(a, { id: 'a', type: 'revolute', body0: { plane: 'mounting-plane' }, body1: { part: 'a'
|
|
119
|
-
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 } })
|
|
120
118
|
assert.equal(motionOf(a, 'b')?.kind, 'turn')
|
|
121
119
|
assert.equal(motionFormOf(jointOf(a, 'b')).kind, 'turn')
|
|
122
120
|
assert.equal(motionOf(a, 'a')?.kind, 'turn')
|
|
123
|
-
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' })
|
|
124
122
|
assert.equal(motionOf(free, 'a')?.kind, 'continuous', 'a joint of its own without limits still turns without end')
|
|
125
123
|
})
|
|
126
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
|
+
|
|
127
136
|
test('a range need not hold 0; it needs its most above its least', () => {
|
|
128
137
|
const form = motionFormOf(jointOf(plain(), 'link'))
|
|
129
138
|
assert.equal(motionFormRefusalOf({ ...form, min: 10, max: 90 }, ''), undefined)
|
|
@@ -191,7 +200,6 @@ test('placed by an origin: said as the origin, no row offered, and the fastening
|
|
|
191
200
|
assert.equal(endPlaceKeyOf(jointOf(asset, 'link').body0), 'figure.label.face-top')
|
|
192
201
|
assert.equal((fasteningOf(revisionOf(asset), 'tool') as Dynamic).atOrigin, true)
|
|
193
202
|
assert.equal((fasteningOf(revisionOf(asset), 'cap') as Dynamic).atOrigin, true)
|
|
194
|
-
assert.equal((fasteningOf(revisionOf(asset), 'cap') as Dynamic).face, 'centre')
|
|
195
203
|
assert.equal((fasteningOf(revisionOf(asset), 'link') as Dynamic).atOrigin, undefined)
|
|
196
204
|
assert.equal(fasteningSave(asset, 'tool'), null)
|
|
197
205
|
assert.deepEqual(rowAxesOf(asset, 'cap'), [])
|
|
@@ -201,8 +209,8 @@ test('placed by an origin: said as the origin, no row offered, and the fastening
|
|
|
201
209
|
test('a drag about a vector axis sets the value; off it, it is refused and the vector said', () => {
|
|
202
210
|
let a = box(createFigureAsset('Tilt'), 'post', 100, 200, 100, 100)
|
|
203
211
|
a = box(a, 'head', 60, 60, 60, 230)
|
|
204
|
-
a = set(a, { id: 'post', type: 'fixed', body0: { plane: 'mounting-plane' }, body1: { part: 'post'
|
|
205
|
-
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 })
|
|
206
214
|
const at = drawn(a, 'head')
|
|
207
215
|
/* 30° about (0, 0.6, 0.8), as XYZ degrees. */
|
|
208
216
|
const q = [0, 0.6, 0.8].map(v => v * Math.sin(Math.PI / 12))
|
|
@@ -223,8 +231,8 @@ test('a drag about a vector axis sets the value; off it, it is refused and the v
|
|
|
223
231
|
test('a rotated joint frame: a turn about its turned axis is its value, and a move is measured before the rotation', () => {
|
|
224
232
|
let a = box(createFigureAsset('Rotated'), 'post', 100, 200, 100, 100)
|
|
225
233
|
a = box(a, 'link', 60, 300, 60, 350)
|
|
226
|
-
a = set(a, { id: 'post', type: 'fixed', body0: { plane: 'mounting-plane' }, body1: { part: 'post'
|
|
227
|
-
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 })
|
|
228
236
|
/* Turned 90° about X, the frame's Z is the figure's -Y: 20° about -Y is 20 on the joint. */
|
|
229
237
|
const at = drawn(a, 'link', { bend: 10 })
|
|
230
238
|
const before = at.pose.r
|
|
@@ -252,8 +260,8 @@ test('a rotated joint frame: a turn about its turned axis is its value, and a mo
|
|
|
252
260
|
test('a fan stands about a vector axis, as it does about the same token', () => {
|
|
253
261
|
let a = box(createFigureAsset('Fan'), 'base', 200, 100, 200, 50)
|
|
254
262
|
a = box(a, 'arm', 400, 50, 50, 125)
|
|
255
|
-
a = set(a, { id: 'base', type: 'fixed', body0: { plane: 'mounting-plane' }, body1: { part: 'base'
|
|
256
|
-
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 })
|
|
257
265
|
const token = jointFanOf(revisionOf(arm('Y')), 'arm') as never as { pose: { t: number[]; r: number[][] } }
|
|
258
266
|
const vector = jointFanOf(revisionOf(arm({ x: 0, y: 2, z: 0 })), 'arm') as never as { pose: { t: number[]; r: number[][] } }
|
|
259
267
|
assert.ok(token && vector, 'a fan for both')
|
|
@@ -272,8 +280,8 @@ test('editing a motion keeps the control named after the joint when it has no st
|
|
|
272
280
|
let a = createFigureAsset('Hinge')
|
|
273
281
|
a = addFigureBox(a, { id: 'base', size: { x: 200, y: 100, z: 200 }, position: { x: 0, y: 0, z: 0 } })
|
|
274
282
|
a = addFigureBox(a, { id: 'lid', size: { x: 200, y: 20, z: 200 }, position: { x: 0, y: 0, z: 0 } })
|
|
275
|
-
a = applyFigureAuthoring(a, { kind: 'set-joint', joint: { id: 'base', type: 'fixed', body0: { plane: 'mounting-plane' }, body1: { part: 'base'
|
|
276
|
-
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 } })
|
|
277
285
|
const lid = a.joints!.find(j => j.id === 'lid')!
|
|
278
286
|
const form = motionFormOf(lid)
|
|
279
287
|
assert.equal(form.id, 'lid', 'the form starts from the input the joint is driven by')
|
|
@@ -281,12 +289,26 @@ test('editing a motion keeps the control named after the joint when it has no st
|
|
|
281
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')
|
|
282
290
|
})
|
|
283
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
|
+
|
|
284
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", () => {
|
|
285
307
|
let a = box(createFigureAsset('Laid'), 'post', 100, 200, 100, 100)
|
|
286
308
|
a = box(a, 'link', 60, 300, 60, 350)
|
|
287
|
-
a = set(a, { id: 'post', type: 'fixed', body0: { plane: 'mounting-plane' }, body1: { part: 'post'
|
|
309
|
+
a = set(a, { id: 'post', type: 'fixed', body0: { plane: 'mounting-plane' }, body1: { part: 'post' } })
|
|
288
310
|
/* The link laid on its side on the post's top (a quarter turn about X), turning about the figure's Y. */
|
|
289
|
-
a = set(a, { id: 'bend', type: 'revolute', body0: { part: 'post', face: 'top' }, body1: { part: 'link'
|
|
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 })
|
|
290
312
|
const at = drawn(a, 'link', { bend: 10 })
|
|
291
313
|
const before = at.pose.r
|
|
292
314
|
const c = Math.cos((20 * Math.PI) / 180),
|
package/test/joints.ts
CHANGED