@things-factory/figure-ui 10.1.200 → 10.1.202

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.
Files changed (73) hide show
  1. package/client/modeller/editor.ts +7 -3
  2. package/client/modeller/fasten-form.ts +18 -35
  3. package/client/modeller/fasten-geometry.ts +67 -115
  4. package/client/modeller/figure-fasten-dialog.ts +4 -12
  5. package/client/modeller/figure-links.ts +6 -7
  6. package/client/modeller/joint-drag.ts +5 -5
  7. package/client/modeller/links.ts +35 -99
  8. package/client/modeller/refusals.ts +2 -6
  9. package/dist-client/modeller/editor.js +7 -2
  10. package/dist-client/modeller/editor.js.map +1 -1
  11. package/dist-client/modeller/fasten-form.d.ts +5 -8
  12. package/dist-client/modeller/fasten-form.js +16 -28
  13. package/dist-client/modeller/fasten-form.js.map +1 -1
  14. package/dist-client/modeller/fasten-geometry.d.ts +20 -19
  15. package/dist-client/modeller/fasten-geometry.js +54 -113
  16. package/dist-client/modeller/fasten-geometry.js.map +1 -1
  17. package/dist-client/modeller/figure-fasten-dialog.js +4 -12
  18. package/dist-client/modeller/figure-fasten-dialog.js.map +1 -1
  19. package/dist-client/modeller/figure-links.d.ts +1 -1
  20. package/dist-client/modeller/figure-links.js +6 -7
  21. package/dist-client/modeller/figure-links.js.map +1 -1
  22. package/dist-client/modeller/joint-drag.js +5 -5
  23. package/dist-client/modeller/joint-drag.js.map +1 -1
  24. package/dist-client/modeller/links.d.ts +19 -26
  25. package/dist-client/modeller/links.js +36 -94
  26. package/dist-client/modeller/links.js.map +1 -1
  27. package/dist-client/modeller/refusals.d.ts +1 -1
  28. package/dist-client/modeller/refusals.js +2 -6
  29. package/dist-client/modeller/refusals.js.map +1 -1
  30. package/dist-client/tsconfig.tsbuildinfo +1 -1
  31. package/package.json +8 -8
  32. package/test/ai-proposal.test.ts +1 -1
  33. package/test/browser/acceptance-page.js +11 -11
  34. package/test/browser/detail-page.js +1 -1
  35. package/test/browser/detail-tabs-page.js +1 -1
  36. package/test/browser/editing-page.js +1 -1
  37. package/test/browser/fix-alignment.check.mjs +3 -3
  38. package/test/browser/links.check.mjs +20 -8
  39. package/test/browser/modeller-page.js +3 -3
  40. package/test/browser/part-ids.check.mjs +2 -2
  41. package/test/browser/parts-page.js +2 -2
  42. package/test/browser/product-page.check.mjs +3 -3
  43. package/test/browser/product-page.js +1 -1
  44. package/test/browser/row-preview.check.mjs +2 -2
  45. package/test/browser/screen-page.js +3 -3
  46. package/test/browser/section-page.js +3 -3
  47. package/test/browser/section.check.mjs +4 -4
  48. package/test/clip-ai-proposal.test.ts +1 -1
  49. package/test/clip-consent.test.ts +1 -1
  50. package/test/command-preview.test.ts +8 -8
  51. package/test/face-mapping.test.ts +2 -2
  52. package/test/fasten-drawing.test.ts +14 -2
  53. package/test/fasten-form.test.ts +11 -3
  54. package/test/fasten-geometry.test.ts +22 -31
  55. package/test/fix-alignment.test.ts +16 -4
  56. package/test/gizmo.test.ts +2 -2
  57. package/test/joint-drag.test.ts +13 -2
  58. package/test/joint-fan.test.ts +1 -1
  59. package/test/joint-frames.test.ts +45 -23
  60. package/test/joints.ts +1 -1
  61. package/test/links.test.ts +68 -68
  62. package/test/live.test.ts +2 -2
  63. package/test/preview-size.test.ts +1 -1
  64. package/test/refusals.test.ts +18 -20
  65. package/test/resize-with-placement.test.ts +6 -6
  66. package/test/revision.test.ts +4 -5
  67. package/test/section-panel.test.ts +1 -1
  68. package/translations/en.json +9 -11
  69. package/translations/es.json +9 -11
  70. package/translations/ja.json +9 -11
  71. package/translations/ko.json +9 -11
  72. package/translations/ms.json +9 -11
  73. package/translations/zh.json +9 -11
@@ -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, face: 'bottom', to: to === 'plane' ? { plane: 'mounting-plane' } : { part: to, face: 'top' }, ...more } as never)
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', face: 'bottom', to: { part: 'frame', face: 'top' }, facing: 'meet', twist: 0, align: { x: { mm: 300 } }, replace: true })
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('roller', fastenFormOf(r, 'roller'), plan, '바닥', say), 'figure.text.fasten-sentence-meet(part=roller,face=figure.label.face-bottom(),to=frame의 figure.label.face-top())')
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
- * 지금 그 방향을 보는 면이다. 면 위에 놓은 자리는 가운데·안쪽 끝·바깥 끝에 붙고, 그 밖은 mm 다. figure-model 이 그 명령을
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, figurePartFaceOf } from '@hatiolab/figure-model'
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, myFaceFor, offsetOf, overhangOf, screenOf } from '../client/modeller/fasten-geometry.ts'
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', face: 'bottom', to: { plane: 'mounting-plane' } })
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.equal(overhangOf(v, { x: 460, y: 0, z: 0 }).touching, false)
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', face: 'bottom', to: { part: 'bin', face: 'top' }, ...extra } as FigureAuthoringAction)
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({ face: 'top', facing: 'flush' }), 'lid').map(Math.round), [0, 290, 0])
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 { insideAlignmentOf, type FaceView } from '../client/modeller/fasten-geometry.js'
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 => ({ axes: ['x', 'z'], face: { x: 400, z: 1000 }, part, edges: { x: true, y: true, z: false } })
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
- assert.deepEqual(insideAlignmentOf({ ...view(), face: { x: 400 } }, {}, {}), { cannot: 'unmeasured' })
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
  })
@@ -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', face: 'bottom', to: { part: 'body', face: 'top' } })
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', face: 'bottom', to: { part: 'body', face: 'top' } })
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 })
@@ -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', face: 'bottom', to: { plane: 'mounting-plane' } })
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', face: 'bottom' }, ...joint } as FigureJoint
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')
@@ -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', face: 'bottom', to: { part: 'base', face: 'top' }, align: { x: 'min' } })
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', face: 'bottom' } })
36
- a = set(a, { id: 'link', type: 'revolute', body0: { part: 'base', face: 'top' }, body1: { part: 'link', face: 'bottom' }, origin: { pivot: 'child-face', offset: { x: 20 }, parentRotate: { x: 10 } }, axis: { x: 1, y: 1, z: 0 }, lowerLimit: -90, upperLimit: 90 })
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', face: 'centre' }, origin: { offset: { y: 40 } } })
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', face: 'bottom' } })
46
- return set(a, { id: 'link', type: 'revolute', body0: { part: 'base', face: 'top' }, body1: { part: 'link', face: 'bottom' }, origin: { gap: 5, align: { x: 'min' } }, axis: 'Y', lowerLimit: -90, upperLimit: 90 })
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, part, aim, f.align, f.alignMm, !!f.twist, f.alignInput)
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', face: 'bottom' }, axis: 'Y', lowerLimit: -90, upperLimit: 90 })
119
- a = set(a, { id: 'b', type: 'revolute', body0: { part: 'a', face: 'top' }, body1: { part: 'b', face: 'bottom' }, axis: 'Y', mimic: { joint: 'a', multiplier: -1 } })
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', face: 'bottom' }, axis: 'Y' })
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', face: 'bottom' } })
205
- a = set(a, { id: 'head', type: 'revolute', body0: { part: 'post', face: 'top' }, body1: { part: 'head', face: 'bottom' }, axis: { x: 0, y: 3, z: 4 }, lowerLimit: -90, upperLimit: 90 })
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', face: 'bottom' } })
227
- a = set(a, { id: 'bend', type: 'revolute', body0: { part: 'post', face: 'top' }, body1: { part: 'link', face: 'bottom' }, origin: { parentRotate: { x: 90 } }, axis: 'Z', lowerLimit: -90, upperLimit: 90 })
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', face: 'bottom' } })
256
- const arm = (axis: unknown) => set(a, { id: 'arm', type: 'revolute', body0: { part: 'base', face: 'top' }, body1: { part: 'arm', face: 'bottom' }, axis, lowerLimit: -45, upperLimit: 90 })
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', face: 'bottom' } } })
276
- a = applyFigureAuthoring(a, { kind: 'set-joint', joint: { id: 'lid', type: 'revolute', body0: { part: 'base', face: 'top' }, body1: { part: 'lid', face: 'bottom' }, axis: 'X', lowerLimit: -90, upperLimit: 90 } })
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', face: 'bottom' } })
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', face: 'bottom' }, origin: { childRotate: { x: 90 } }, axis: 'Y', lowerLimit: -90, upperLimit: 90 })
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
@@ -16,7 +16,7 @@ export function moving(
16
16
  id: part,
17
17
  type: 'fixed',
18
18
  body0: { plane: 'mounting-plane' },
19
- body1: { part, face: 'bottom' },
19
+ body1: { part },
20
20
  origin: { align: { x: at('x'), z: at('z') } }
21
21
  }
22
22
  return applyFigureAuthoring(asset, {