@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.
Files changed (74) 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 +9 -11
  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 +9 -11
  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/dist-server/tsconfig.tsbuildinfo +1 -1
  32. package/package.json +6 -6
  33. package/test/ai-proposal.test.ts +1 -1
  34. package/test/browser/acceptance-page.js +11 -11
  35. package/test/browser/detail-page.js +1 -1
  36. package/test/browser/detail-tabs-page.js +1 -1
  37. package/test/browser/editing-page.js +1 -1
  38. package/test/browser/fix-alignment.check.mjs +3 -3
  39. package/test/browser/links.check.mjs +20 -8
  40. package/test/browser/modeller-page.js +3 -3
  41. package/test/browser/part-ids.check.mjs +2 -2
  42. package/test/browser/parts-page.js +2 -2
  43. package/test/browser/product-page.check.mjs +3 -3
  44. package/test/browser/product-page.js +1 -1
  45. package/test/browser/row-preview.check.mjs +2 -2
  46. package/test/browser/screen-page.js +3 -3
  47. package/test/browser/section-page.js +3 -3
  48. package/test/browser/section.check.mjs +4 -4
  49. package/test/clip-ai-proposal.test.ts +1 -1
  50. package/test/clip-consent.test.ts +1 -1
  51. package/test/command-preview.test.ts +8 -8
  52. package/test/face-mapping.test.ts +2 -2
  53. package/test/fasten-drawing.test.ts +14 -2
  54. package/test/fasten-form.test.ts +11 -3
  55. package/test/fasten-geometry.test.ts +22 -31
  56. package/test/fix-alignment.test.ts +16 -4
  57. package/test/gizmo.test.ts +2 -2
  58. package/test/joint-drag.test.ts +13 -2
  59. package/test/joint-fan.test.ts +1 -1
  60. package/test/joint-frames.test.ts +74 -45
  61. package/test/joints.ts +1 -1
  62. package/test/links.test.ts +68 -68
  63. package/test/live.test.ts +2 -2
  64. package/test/preview-size.test.ts +1 -1
  65. package/test/refusals.test.ts +18 -20
  66. package/test/resize-with-placement.test.ts +6 -6
  67. package/test/revision.test.ts +4 -5
  68. package/test/section-panel.test.ts +1 -1
  69. package/translations/en.json +9 -11
  70. package/translations/es.json +9 -11
  71. package/translations/ja.json +9 -11
  72. package/translations/ko.json +9 -11
  73. package/translations/ms.json +9 -11
  74. package/translations/zh.json +9 -11
@@ -48,9 +48,9 @@ const said = () => read(() => [...(findDeep('section[row]')?.querySelectorAll('[
48
48
  /* A 400 mm board, a 3 mm bin fastened on it: a row of it along X. */
49
49
  function board() {
50
50
  let a = addFigureBox(createFigureAsset('Board'), { id: 'board', size: { x: 400, y: 50, z: 1000 }, position: { x: 0, y: 25, z: 0 } })
51
- a = applyFigureAuthoring(a, { kind: 'attach', part: 'board', face: 'bottom', to: { plane: 'mounting-plane' } })
51
+ a = applyFigureAuthoring(a, { kind: 'attach', part: 'board', to: { plane: 'mounting-plane' } })
52
52
  a = addFigureBox(a, { id: 'bin', size: { x: 3, y: 100, z: 100 }, position: { x: 0, y: 100, z: 0 } })
53
- return applyFigureAuthoring(a, { kind: 'attach', part: 'bin', face: 'bottom', to: { part: 'board', face: 'top' } })
53
+ return applyFigureAuthoring(a, { kind: 'attach', part: 'bin', to: { part: 'board', face: 'top' } })
54
54
  }
55
55
 
56
56
  describe('a row\'s preview follows its basis', () => {
@@ -57,7 +57,7 @@ const FIGURES = {
57
57
  /* A lighting tower that follows its height only: the wider variants ask a width it does not declare. */
58
58
  tower: () => {
59
59
  let a = addFigureBox(named('Tower'), { id: 'mast', size: { x: 200, y: 2000, z: 300 }, position: { x: 0, y: 0, z: 0 } })
60
- a = applyFigureAuthoring(a, { kind: 'attach', part: 'mast', face: 'bottom', to: { plane: 'mounting-plane' } })
60
+ a = applyFigureAuthoring(a, { kind: 'attach', part: 'mast', to: { plane: 'mounting-plane' } })
61
61
  return applyFigureAuthoring(a, { kind: 'declare-instance-size', range: { x: { min: 200, max: 200 }, y: { min: 1000, max: 3000 }, z: { min: 300, max: 300 } } })
62
62
  },
63
63
  /*
@@ -67,7 +67,7 @@ const FIGURES = {
67
67
  stack: () => {
68
68
  let a = addFigureBox(named('Stack'), { id: 'base', size: { x: 1000, y: 200, z: 600 }, position: { x: 0, y: 100, z: 0 } })
69
69
  a = addFigureBox(a, { id: 'top', size: { x: 200, y: 200, z: 200 }, position: { x: 0, y: 300, z: 0 } })
70
- a = applyFigureAuthoring(a, { kind: 'attach', part: 'top', face: 'bottom', to: { part: 'base', face: 'top' } })
70
+ a = applyFigureAuthoring(a, { kind: 'attach', part: 'top', to: { part: 'base', face: 'top' } })
71
71
  a = applyFigureAuthoring(a, { kind: 'declare-occupancy', proposal: proposeFigureOccupancy(a) })
72
72
  return applyFigureAuthoring(a, { kind: 'set-mounting-face', part: 'base', face: 'bottom' })
73
73
  },
@@ -75,7 +75,7 @@ const FIGURES = {
75
75
  stretch: () => {
76
76
  let a = addFigureBox(named('Stretch'), { id: 'frame', size: { x: 2000, y: 100, z: 600 }, position: { x: 0, y: 50, z: 0 } })
77
77
  a = addFigurePrimitive(a, { id: 'roller', kind: 'cylinder', dimensions: { radius: 30, length: 560 }, position: { x: 0, y: 0, z: 0 }, rotation: { x: 90, y: 0, z: 0 } })
78
- a = applyFigureAuthoring(a, { kind: 'attach', part: 'roller', face: 'bottom', to: { part: 'frame', face: 'top' } })
78
+ a = applyFigureAuthoring(a, { kind: 'attach', part: 'roller', to: { part: 'frame', face: 'top' } })
79
79
  a = applyFigureAuthoring(a, { kind: 'follow-size', part: 'frame', axes: ['x'] })
80
80
  a = applyFigureAuthoring(a, { kind: 'make-repeat', part: 'roller', axis: 'x', pitch: 200 })
81
81
  /* A space wider than the parts, as a spin swept or a padding gives: the box is the space, not the size (2026-10-16). */
@@ -27,7 +27,7 @@ const FIGURES = {
27
27
  /* The triangle with a plate on its top: its end cannot be drawn to one point while the plate stands there (A5-1). */
28
28
  'capped-wedge': () => {
29
29
  const a = addFigureBox(polygon(figure('Capped'), 'wedge', TRIANGLE), { id: 'cap', size: { x: 100, y: 20, z: 100 }, position: { x: 0, y: 0, z: 0 } })
30
- return applyFigureAuthoring(a, { kind: 'attach', part: 'cap', face: 'bottom', to: { part: 'wedge', face: 'top' } })
30
+ return applyFigureAuthoring(a, { kind: 'attach', part: 'cap', to: { part: 'wedge', face: 'top' } })
31
31
  },
32
32
  pyramid: () => applyFigureAuthoring(polygon(figure('Pyramid'), 'wedge', TRIANGLE), { kind: 'set-section', part: 'wedge', apex: { x: 0, z: 0 } }),
33
33
  /* A box standing on the floor: a length change keeps its bottom. */
@@ -51,10 +51,10 @@ const FIGURES = {
51
51
  /* A tote on the floor with a divider standing on its inside floor, and a box beside it (ADR-0100 A8). */
52
52
  tote: () => {
53
53
  let a = addFigureBox(figure('Tote'), { id: 'bin', size: { x: 600, y: 300, z: 400 }, position: { x: 0, y: 150, z: 0 } })
54
- a = applyFigureAuthoring(a, { kind: 'attach', part: 'bin', face: 'bottom', to: { plane: 'mounting-plane' } })
54
+ a = applyFigureAuthoring(a, { kind: 'attach', part: 'bin', to: { plane: 'mounting-plane' } })
55
55
  a = applyFigureAuthoring(a, { kind: 'set-cavity', part: 'bin', cavity: { wall: 10, start: { closed: 15 }, end: 'open' } })
56
56
  a = addFigureBox(a, { id: 'divider', size: { x: 10, y: 200, z: 380 }, position: { x: 0, y: 0, z: 0 } })
57
- return beside(applyFigureAuthoring(a, { kind: 'attach', part: 'divider', face: 'bottom', to: { part: 'bin', face: 'inside-bottom' } }))
57
+ return beside(applyFigureAuthoring(a, { kind: 'attach', part: 'divider', to: { part: 'bin', face: 'inside-bottom' } }))
58
58
  }
59
59
  }
60
60
 
@@ -574,7 +574,7 @@ describe('the fastening drawing in the fastening window', () => {
574
574
  assert.deepEqual([now.body0.face, now.origin?.facing], [face, 'flush'])
575
575
  })
576
576
 
577
- test('fastened to the plane: the plane is named by the placement, chosen, and set on only', async () => {
577
+ test('fastened to the plane: the plane is named by the placement, chosen, and set on it or hung below it', async () => {
578
578
  await mount('rollers')
579
579
  await pick('bed')
580
580
  await change()
@@ -582,7 +582,7 @@ describe('the fastening drawing in the fastening window', () => {
582
582
  const plane = list.find(t => t.target === '__plane')
583
583
  assert.deepEqual([plane.selected, plane.text], [true, 'figure.label.resize-place-floor'])
584
584
  assert.equal((await labels())[1], 'figure.label.fasten-face-on-plane', 'the face asked on the floor, not of a part')
585
- assert.deepEqual((await read(() => probe.fastenDrawing())).modes, [], 'one way only: no choice offered')
585
+ assert.deepEqual((await read(() => probe.fastenDrawing())).modes, ['meet', 'flush'], 'the floor: stood on, or hung below (ADR-0140 3.11)')
586
586
  })
587
587
 
588
588
  /* The window is a popup over the work: closed, nothing written, before another part is picked. */
@@ -602,7 +602,7 @@ describe('the fastening drawing in the fastening window', () => {
602
602
  assert.deepEqual((await read(() => probe.fastenDrawing())).modes, ['meet', 'flush'])
603
603
  })
604
604
 
605
- test('a new fastening waits for 붙이기; the part\'s own face follows the target\'s face and how', async () => {
605
+ test('a new fastening waits for 붙이기; the part meets the target\'s face, by no face of its own', async () => {
606
606
  await mount('door')
607
607
  await pick('leaf')
608
608
  await press('button[fasten-start]')
@@ -617,7 +617,7 @@ describe('the fastening drawing in the fastening window', () => {
617
617
  await until(r => probe.state().revision === r + 1, 'one step', start.revision)
618
618
  const joint = await read(() => probe.fastening('leaf'))
619
619
  assert.deepEqual(joint.body0, { part: 'frame', face: 'front' })
620
- assert.equal(joint.body1.face, 'back', 'set on the front, by its back')
620
+ assert.deepEqual([joint.body1, joint.origin?.facing], [{ part: 'leaf' }, 'meet'], 'set on the front by its real shape (ADR-0140 3.2)')
621
621
  })
622
622
  })
623
623
 
@@ -21,7 +21,7 @@ function door(): string {
21
21
  a = addFigureBox(a, { id: 'frame', size: { x: 1000, y: 100, z: 200 }, position: { x: 0, y: 50, z: 0 } })
22
22
  a = addFigureBox(a, { id: 'leaf', size: { x: 900, y: 1800, z: 40 }, position: { x: 0, y: 1000, z: 0 } })
23
23
  a = addFigureBox(a, { id: 'sign', size: { x: 200, y: 100, z: 10 }, position: { x: 0, y: 2200, z: 200 } })
24
- a = applyFigureAuthoring(a, { kind: 'attach', part: 'leaf', face: 'bottom', to: { part: 'frame', face: 'top' } })
24
+ a = applyFigureAuthoring(a, { kind: 'attach', part: 'leaf', to: { part: 'frame', face: 'top' } })
25
25
  const j = a.joints!.find(k => k.body1.part === 'leaf')!
26
26
  a = applyFigureAuthoring(a, { kind: 'set-joint', joint: { ...j, type: 'revolute', axis: 'Y', state: { id: 'open' } } })
27
27
  a = editFigurePart(a, { kind: 'set-driver', state: 'open', clip: 'swing', duration: 2, keys: [{ seconds: 0, value: 0 }, { seconds: 2, value: 80 }] })
@@ -20,7 +20,7 @@ function doors(): string {
20
20
  a = addFigureBox(a, { id: 'frame', size: { x: 2000, y: 100, z: 200 }, position: { x: 0, y: 50, z: 0 } })
21
21
  for (const [id, x] of [['left', -500], ['right', 500]] as const) {
22
22
  a = addFigureBox(a, { id, size: { x: 900, y: 1800, z: 40 }, position: { x, y: 1000, z: 0 } })
23
- a = applyFigureAuthoring(a, { kind: 'attach', part: id, face: 'bottom', to: { part: 'frame', face: 'top' } })
23
+ a = applyFigureAuthoring(a, { kind: 'attach', part: id, to: { part: 'frame', face: 'top' } })
24
24
  const j = a.joints!.find(k => k.body1.part === id)!
25
25
  a = applyFigureAuthoring(a, { kind: 'set-joint', joint: { ...j, type: 'revolute', axis: 'Y', state: { id: `${id}-open` } } })
26
26
  a = editFigurePart(a, { kind: 'set-driver', state: `${id}-open`, clip: `${id}-swing`, duration: 2, keys: [{ seconds: 0, value: 0 }, { seconds: 2, value: 80 }] })
@@ -26,11 +26,11 @@ function bench(): string {
26
26
  a = addFigureBox(a, { id: 'leg', size: { x: 60, y: 700, z: 60 }, position: { x: -400, y: 350, z: 0 } })
27
27
  a = addFigureBox(a, { id: 'top', size: { x: 1200, y: 40, z: 600 }, position: { x: 0, y: 900, z: 0 } })
28
28
  a = addFigureBox(a, { id: 'lamp', size: { x: 80, y: 80, z: 80 }, position: { x: 200, y: 960, z: 100 } })
29
- a = applyFigureAuthoring(a, { kind: 'attach', part: 'lamp', face: 'bottom', to: { part: 'top', face: 'top' } })
29
+ a = applyFigureAuthoring(a, { kind: 'attach', part: 'lamp', to: { part: 'top', face: 'top' } })
30
30
  return serializeFigureAsset(a)
31
31
  }
32
32
  const store = { load: async (id: string) => ({ id, source: bench() }), save: async (q: { id?: string; source: string }) => ({ id: q.id ?? 'x', source: q.source }) }
33
- const onLeg = { kind: 'attach', part: 'top', face: 'bottom', to: { part: 'leg', face: 'top' } } as never
33
+ const onLeg = { kind: 'attach', part: 'top', to: { part: 'leg', face: 'top' } } as never
34
34
 
35
35
  test('seen, not done: the session\'s preview writes nothing, and names the version it was worked out on', async () => {
36
36
  const s = createFigureProductSession(store)
@@ -78,7 +78,7 @@ test('the face it would meet is lit where attaching reads it, just off the face'
78
78
  test('the details fold, and what they hold that is not the default stays said -- nothing for the default', () => {
79
79
  for (const lang of ['en', 'ja', 'ko', 'ms', 'zh', 'es']) {
80
80
  const t = JSON.parse(read(`../translations/${lang}.json`))
81
- for (const key of ['preview-refused', 'preview-stays', 'preview-moves', 'preview-turns-named', 'preview-turns-slanted', 'preview-carries', 'preview-copies', 'preview-clips', 'preview-face-unknown', 'target-would-loop', 'fasten-summary-gap', 'fasten-summary-twist'])
81
+ for (const key of ['preview-refused', 'preview-stays', 'preview-moves', 'preview-turns-named', 'preview-turns-slanted', 'preview-carries', 'preview-copies', 'preview-clips', 'preview-face-unknown', 'target-would-loop', 'fasten-summary-gap'])
82
82
  assert.ok(t[`figure.text.${key}`], `${lang} ${key}`)
83
83
  assert.ok(t['figure.label.fasten-detail'] && t['figure.label.facing'], lang)
84
84
  }
@@ -106,7 +106,7 @@ test('a row is seen before it is made, read as figure-model draws it -- and said
106
106
  a = addFigureBox(a, { id: 'frame', size: { x: 2000, y: 100, z: 600 }, position: { x: 0, y: 50, z: 0 } })
107
107
  a = addFigureBox(a, { id: 'roller', size: { x: 60, y: 60, z: 560 }, position: { x: 0, y: 130, z: 0 } })
108
108
  assert.equal(rowBlockOf(a, 'roller'), 'unfastened')
109
- a = applyFigureAuthoring(a, { kind: 'attach', part: 'roller', face: 'bottom', to: { part: 'frame', face: 'top' } })
109
+ a = applyFigureAuthoring(a, { kind: 'attach', part: 'roller', to: { part: 'frame', face: 'top' } })
110
110
  assert.equal(rowBlockOf(a, 'roller'), undefined)
111
111
  const seen = previewFigureCommand(a, { kind: 'make-repeat', part: 'roller', axis: 'x', pitch: 200 } as never)
112
112
  const held: HeldPreview = { context: 'c', revision: 0, key: 'k', geometry: seen.geometry, moves: seen.moves, added: seen.added, row: figureRowOf(seen.asset, 'roller') }
@@ -127,7 +127,7 @@ test('a published pitch typed into its field is one edit, undone once', async ()
127
127
  let a = createFigureAsset('Conveyor')
128
128
  a = addFigureBox(a, { id: 'frame', size: { x: 2000, y: 100, z: 600 }, position: { x: 0, y: 50, z: 0 } })
129
129
  a = addFigureBox(a, { id: 'roller', size: { x: 60, y: 60, z: 560 }, position: { x: 0, y: 130, z: 0 } })
130
- a = applyFigureAuthoring(a, { kind: 'attach', part: 'roller', face: 'bottom', to: { part: 'frame', face: 'top' } })
130
+ a = applyFigureAuthoring(a, { kind: 'attach', part: 'roller', to: { part: 'frame', face: 'top' } })
131
131
  a = applyFigureAuthoring(a, { kind: 'make-repeat', part: 'roller', axis: 'x', pitch: 200 })
132
132
  const source = serializeFigureAsset(a)
133
133
  const s = createFigureProductSession({ load: async (id: string) => ({ id, source }), save: async (q: { id?: string; source: string }) => ({ id: q.id ?? 'x', source: q.source }) })
@@ -157,8 +157,8 @@ test('a motion is named by its parts, and drawn at both ends of its range -- onl
157
157
  a = addFigureBox(a, { id: 'frame', size: { x: 2000, y: 100, z: 200 }, position: { x: 0, y: 50, z: 0 } })
158
158
  a = addFigureBox(a, { id: 'leaf', size: { x: 900, y: 1800, z: 40 }, position: { x: 0, y: 1000, z: 0 } })
159
159
  a = addFigureBox(a, { id: 'gate', size: { x: 400, y: 400, z: 40 }, position: { x: 700, y: 300, z: 0 } })
160
- a = applyFigureAuthoring(a, { kind: 'attach', part: 'leaf', face: 'bottom', to: { part: 'frame', face: 'top' } })
161
- a = applyFigureAuthoring(a, { kind: 'attach', part: 'gate', face: 'bottom', to: { part: 'frame', face: 'top' } })
160
+ a = applyFigureAuthoring(a, { kind: 'attach', part: 'leaf', to: { part: 'frame', face: 'top' } })
161
+ a = applyFigureAuthoring(a, { kind: 'attach', part: 'gate', to: { part: 'frame', face: 'top' } })
162
162
  const j = (part: string) => a.joints!.find(k => k.body1.part === part)!
163
163
  assert.equal(motionEndsOf(revisionOf(a), 'leaf'), null, 'not moving: nothing drawn')
164
164
  a = applyFigureAuthoring(a, { kind: 'set-joint', joint: { ...j('leaf'), type: 'revolute', axis: 'Y', lowerLimit: 0, upperLimit: 90, state: { id: 'open' } } })
@@ -190,7 +190,7 @@ test('a moving part is made into a row, and a row given or relieved of a motion
190
190
  let a = createFigureAsset('Conveyor')
191
191
  a = addFigureBox(a, { id: 'frame', size: { x: 2000, y: 100, z: 600 }, position: { x: 0, y: 50, z: 0 } })
192
192
  a = addFigureBox(a, { id: 'roller', size: { x: 60, y: 60, z: 560 }, position: { x: 0, y: 130, z: 0 } })
193
- a = applyFigureAuthoring(a, { kind: 'attach', part: 'roller', face: 'bottom', to: { part: 'frame', face: 'top' } })
193
+ a = applyFigureAuthoring(a, { kind: 'attach', part: 'roller', to: { part: 'frame', face: 'top' } })
194
194
  const source = serializeFigureAsset(a)
195
195
  const s = createFigureProductSession({ load: async (id: string) => ({ id, source }), save: async (q: { id?: string; source: string }) => ({ id: q.id ?? 'x', source: q.source }) })
196
196
  await s.open('conveyor')
@@ -17,10 +17,10 @@ import { DETACH, faceMappingRowsOf, remapOf, rowKey } from '../client/modeller/f
17
17
  /** A tote with a floor inside and a divider standing on it. */
18
18
  function tote(): FigureAsset {
19
19
  let asset = addFigureBox(createFigureAsset('Tote'), { id: 'bin', size: { x: 600, y: 300, z: 400 }, position: { x: 0, y: 150, z: 0 } })
20
- asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'bin', face: 'bottom', to: { plane: 'mounting-plane' } })
20
+ asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'bin', to: { plane: 'mounting-plane' } })
21
21
  asset = applyFigureAuthoring(asset, { kind: 'set-cavity', part: 'bin', cavity: { wall: 10, start: { closed: 15 }, end: 'open' } })
22
22
  asset = addFigureBox(asset, { id: 'divider', size: { x: 10, y: 200, z: 380 }, position: { x: 0, y: 0, z: 0 } })
23
- return applyFigureAuthoring(asset, { kind: 'attach', part: 'divider', face: 'bottom', to: { part: 'bin', face: 'inside-bottom' as never } })
23
+ return applyFigureAuthoring(asset, { kind: 'attach', part: 'divider', to: { part: 'bin', face: 'inside-bottom' as never } })
24
24
  }
25
25
  const open = { kind: 'set-cavity', part: 'bin', cavity: { wall: 10, start: 'open', end: 'open' } } as const
26
26
  const needOf = (asset: FigureAsset): FigureFaceMappingNeed => {
@@ -14,9 +14,21 @@ const ko = JSON.parse(read('../translations/ko.json')) as Record<string, string>
14
14
  const say = (key: string, params: Record<string, string>) => Object.entries(params).reduce((s, [k, v]) => s.replace(`{${k}}`, v), ko[key] ?? key)
15
15
 
16
16
  test('화면 문구는 결과와 예외만 — 한 줄 요약, 가장자리 밖, 닿지 않음', () => {
17
- assert.equal(say('figure.text.fasten-sentence-meet', { part: 'lid', face: '아랫면', to: 'bin의 윗면' }), 'lid의 아랫면을 bin의 윗면에 맞닿게')
18
- assert.equal(say('figure.text.fasten-sentence-centre', { part: 'roller', face: '', to: 'frame의 앞면' }), 'roller의 가운데를 frame의 앞면 위에')
17
+ assert.equal(say('figure.text.fasten-sentence-meet', { to: 'bin의 윗면' }), 'bin의 윗면에 올려놓기')
18
+ assert.equal(say('figure.text.fasten-sentence-flush', { to: 'bin의 윗면' }), 'bin의 윗면과 같은 면에 붙이기')
19
+ assert.equal(say('figure.text.fasten-sentence-centre', { to: 'frame의 앞면' }), 'frame의 앞면 가운데에 붙이기')
19
20
  for (const key of ['figure.text.fasten-off-face', 'figure.text.fasten-past-edges', 'figure.label.fasten-mode-meet', 'figure.label.fasten-mode-flush', 'figure.label.fasten-mode-centre'])
20
21
  for (const lang of ['ko', 'en', 'ja', 'zh', 'ms', 'es']) assert.ok((JSON.parse(read(`../translations/${lang}.json`)) as Record<string, string>)[key], `${lang} ${key}`)
21
22
  assert.ok(!/끌어 옮깁니다|면 밖으로도/.test(JSON.stringify(ko)), 'no instructions on the screen')
23
+ /* The part meets by its real shape, by no face of its own (ADR-0140 3.2): no sentence names one, in any language (UX lane 10-19). */
24
+ for (const lang of ['ko', 'en', 'ja', 'zh', 'ms', 'es']) {
25
+ const t = JSON.parse(read(`../translations/${lang}.json`)) as Record<string, string>
26
+ for (const key of ['fasten-sentence-meet', 'fasten-sentence-flush', 'fasten-sentence-centre', 'held-on-part', 'held-on-plane', 'held-on-plane-worked-out'])
27
+ assert.ok(!t[`figure.text.${key}`]!.includes('{face}'), `${lang} ${key}`)
28
+ /* The part the window is open on is not named again: a name's last letter picks the particle (원기둥를, 판를). */
29
+ for (const key of ['fasten-sentence-meet', 'fasten-sentence-flush', 'fasten-sentence-centre']) assert.ok(!t[`figure.text.${key}`]!.includes('{part}'), `${lang} ${key}`)
30
+ /* No particle or bracket stuck to a placeholder in the placement refusal: the parts are listed after a colon. */
31
+ assert.deepEqual(t['figure.text.placement-plane-refused']!.match(/\{[a-z]+\}/g), ['{parts}'], lang)
32
+ assert.ok(!('figure.text.fasten-editing' in t), `${lang}: a key no screen says is not kept`)
33
+ }
22
34
  })
@@ -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]