@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/figure-ui",
3
- "version": "10.1.200",
3
+ "version": "10.1.202",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "@things-factory:registry": "https://registry.npmjs.org"
@@ -27,8 +27,8 @@
27
27
  "clean": "npm run clean:server && npm run clean:client"
28
28
  },
29
29
  "dependencies": {
30
- "@hatiolab/figure-model": "^0.8.0",
31
- "@hatiolab/things-scene": "^10.1.93",
30
+ "@hatiolab/figure-model": "^0.9.3",
31
+ "@hatiolab/things-scene": "^10.1.95",
32
32
  "@material/web": "^2.0.0",
33
33
  "@operato/data-grist": "^10.29.43",
34
34
  "@operato/graphql": "^10.29.43",
@@ -42,11 +42,11 @@
42
42
  "@operato/slot-layout-editor": "^10.29.43",
43
43
  "@operato/styles": "^10.0.0",
44
44
  "@operato/utils": "^10.29.43",
45
- "@things-factory/ai-assistant": "^10.1.198",
46
- "@things-factory/figure-service": "^10.1.200",
47
- "@things-factory/figure-viewer": "^10.1.200",
48
- "@things-factory/setting-base": "^10.1.198",
45
+ "@things-factory/ai-assistant": "^10.1.202",
46
+ "@things-factory/figure-service": "^10.1.202",
47
+ "@things-factory/figure-viewer": "^10.1.201",
48
+ "@things-factory/setting-base": "^10.1.202",
49
49
  "three": "^0.185.1"
50
50
  },
51
- "gitHead": "549b5b571862f9c52ab407fe85d117fd33bd8d1f"
51
+ "gitHead": "3243b81eb1a4d3d903a01929f3a85d5b8df438dd"
52
52
  }
@@ -26,7 +26,7 @@ const store = {
26
26
  }
27
27
  const steps = [
28
28
  { kind: 'add-part', part: { id: 'lid', kind: 'box', dimensions: { width: 100, height: 10, depth: 100 } } },
29
- { kind: 'attach', part: 'lid', face: 'bottom', to: { part: 'body', face: 'top' } },
29
+ { kind: 'attach', part: 'lid', to: { part: 'body', face: 'top' } },
30
30
  { kind: 'rename', name: 'Box with lid' }
31
31
  ] as any[]
32
32
 
@@ -53,7 +53,7 @@ export const MODELS = {
53
53
  a.palette = { ...a.palette, ...PALETTE }
54
54
  a = addFigurePrimitive(a, { id: 'wedge', kind: 'polygon', dimensions: { height: 200 }, points: [{ x: -300, z: -200 }, { x: 300, z: -200 }, { x: 0, z: 250 }], position: { x: 0, y: 100, z: 0 } })
55
55
  a = addFigureBox(a, { id: 'cap', size: { x: 100, y: 20, z: 100 }, position: { x: 0, y: 0, z: 0 } })
56
- a = applyFigureAuthoring(a, { kind: 'attach', part: 'cap', face: 'bottom', to: { part: 'wedge', face: 'top' } })
56
+ a = applyFigureAuthoring(a, { kind: 'attach', part: 'cap', to: { part: 'wedge', face: 'top' } })
57
57
  return serializeFigureAsset(a)
58
58
  },
59
59
  /* A lid fastened to the top of a bin standing on the floor: the fastening drawn and dragged (user 2026-10-01). */
@@ -61,9 +61,9 @@ export const MODELS = {
61
61
  let a = createFigureAsset('Lid on bin')
62
62
  a.palette = { ...a.palette, ...PALETTE }
63
63
  a = addFigureBox(a, { id: 'bin', size: { x: 600, y: 300, z: 400 }, position: { x: 0, y: 150, z: 0 } })
64
- a = applyFigureAuthoring(a, { kind: 'attach', part: 'bin', face: 'bottom', to: { plane: 'mounting-plane' } })
64
+ a = applyFigureAuthoring(a, { kind: 'attach', part: 'bin', to: { plane: 'mounting-plane' } })
65
65
  a = addFigureBox(a, { id: 'lid', size: { x: 300, y: 20, z: 200 }, position: { x: 0, y: 0, z: 0 } })
66
- a = applyFigureAuthoring(a, { kind: 'attach', part: 'lid', face: 'bottom', to: { part: 'bin', face: 'top' } })
66
+ a = applyFigureAuthoring(a, { kind: 'attach', part: 'lid', to: { part: 'bin', face: 'top' } })
67
67
  return serializeFigureAsset(a)
68
68
  },
69
69
  /* A divider standing on a tote's inside floor: opening the bottom asks where the divider goes (ADR-0100 A8). */
@@ -71,10 +71,10 @@ export const MODELS = {
71
71
  let a = createFigureAsset('Divider in tote')
72
72
  a.palette = { ...a.palette, ...PALETTE }
73
73
  a = addFigureBox(a, { id: 'bin', size: { x: 600, y: 300, z: 400 }, position: { x: 0, y: 150, z: 0 } })
74
- a = applyFigureAuthoring(a, { kind: 'attach', part: 'bin', face: 'bottom', to: { plane: 'mounting-plane' } })
74
+ a = applyFigureAuthoring(a, { kind: 'attach', part: 'bin', to: { plane: 'mounting-plane' } })
75
75
  a = applyFigureAuthoring(a, { kind: 'set-cavity', part: 'bin', cavity: { wall: 10, start: { closed: 15 }, end: 'open' } })
76
76
  a = addFigureBox(a, { id: 'divider', size: { x: 10, y: 200, z: 380 }, position: { x: 0, y: 0, z: 0 } })
77
- a = applyFigureAuthoring(a, { kind: 'attach', part: 'divider', face: 'bottom', to: { part: 'bin', face: 'inside-bottom' } })
77
+ a = applyFigureAuthoring(a, { kind: 'attach', part: 'divider', to: { part: 'bin', face: 'inside-bottom' } })
78
78
  return serializeFigureAsset(a)
79
79
  },
80
80
  /* A box and a drum: rounded corners and an inside from the properties tab (ADR-0100 stage 1). */
@@ -110,7 +110,7 @@ export const MODELS = {
110
110
  a = addFigureBox(a, { id: 'frame', size: { x: 2000, y: 100, z: 200 }, position: { x: 0, y: 50, z: 0 } })
111
111
  for (const [id, x] of [['left', -500], ['right', 500]]) {
112
112
  a = addFigureBox(a, { id, size: { x: 900, y: 1800, z: 40 }, position: { x, y: 1000, z: 0 } })
113
- a = applyFigureAuthoring(a, { kind: 'attach', part: id, face: 'bottom', to: { part: 'frame', face: 'top' } })
113
+ a = applyFigureAuthoring(a, { kind: 'attach', part: id, to: { part: 'frame', face: 'top' } })
114
114
  const j = a.joints.find(k => k.body1.part === id)
115
115
  a = applyFigureAuthoring(a, { kind: 'set-joint', joint: { ...j, type: 'revolute', axis: 'Y', state: { id: `${id}-open` } } })
116
116
  a = editFigurePart(a, { kind: 'set-driver', state: `${id}-open`, clip: `${id}-swing`, duration: 2, keys: [{ seconds: 0, value: 0 }, { seconds: 2, value: 80 }] })
@@ -123,7 +123,7 @@ export const MODELS = {
123
123
  a.palette = { ...a.palette, ...PALETTE }
124
124
  a = addFigureBox(a, { id: 'frame', size: { x: 2000, y: 100, z: 600 }, position: { x: 0, y: 400, z: 0 } })
125
125
  a = addFigureBox(a, { id: 'roller', size: { x: 60, y: 60, z: 560 }, position: { x: 0, y: 480, z: 0 } })
126
- a = applyFigureAuthoring(a, { kind: 'attach', part: 'roller', face: 'bottom', to: { part: 'frame', face: 'top' } })
126
+ a = applyFigureAuthoring(a, { kind: 'attach', part: 'roller', to: { part: 'frame', face: 'top' } })
127
127
  return serializeFigureAsset(a)
128
128
  },
129
129
  /* A device fastened on a bench top 300 mm off centre: the top narrowed on the screen brings it past the edge (C, scenario 1, Q3). */
@@ -133,8 +133,8 @@ export const MODELS = {
133
133
  a = addFigureBox(a, { id: 'leg', size: { x: 60, y: 700, z: 60 }, position: { x: 0, y: 350, z: 0 } })
134
134
  a = addFigureBox(a, { id: 'top', size: { x: 1200, y: 40, z: 600 }, position: { x: 0, y: 720, z: 0 } })
135
135
  a = addFigureBox(a, { id: 'device', size: { x: 100, y: 100, z: 100 }, position: { x: 300, y: 790, z: 100 } })
136
- a = applyFigureAuthoring(a, { kind: 'attach', part: 'top', face: 'bottom', to: { part: 'leg', face: 'top' } })
137
- a = applyFigureAuthoring(a, { kind: 'attach', part: 'device', face: 'bottom', to: { part: 'top', face: 'top' }, align: { x: { mm: 300 }, z: { mm: 100 } } })
136
+ a = applyFigureAuthoring(a, { kind: 'attach', part: 'top', to: { part: 'leg', face: 'top' } })
137
+ a = applyFigureAuthoring(a, { kind: 'attach', part: 'device', to: { part: 'top', face: 'top' }, align: { x: { mm: 300 }, z: { mm: 100 } } })
138
138
  return serializeFigureAsset(a)
139
139
  },
140
140
  /* A door leaf fastened to its frame, not moving yet: its motion is set on the screen (C, scenario 3). */
@@ -144,8 +144,8 @@ export const MODELS = {
144
144
  a = addFigureBox(a, { id: 'frame', size: { x: 1200, y: 100, z: 200 }, position: { x: 0, y: 50, z: 0 } })
145
145
  a = addFigureBox(a, { id: 'leaf', size: { x: 900, y: 1800, z: 40 }, position: { x: 0, y: 1000, z: 0 } })
146
146
  a = addFigureBox(a, { id: 'handle', size: { x: 30, y: 150, z: 60 }, position: { x: 350, y: 1000, z: 50 } })
147
- a = applyFigureAuthoring(a, { kind: 'attach', part: 'leaf', face: 'bottom', to: { part: 'frame', face: 'top' } })
148
- a = applyFigureAuthoring(a, { kind: 'attach', part: 'handle', face: 'back', to: { part: 'leaf', face: 'front' }, align: { x: { mm: 350 } } })
147
+ a = applyFigureAuthoring(a, { kind: 'attach', part: 'leaf', to: { part: 'frame', face: 'top' } })
148
+ a = applyFigureAuthoring(a, { kind: 'attach', part: 'handle', to: { part: 'leaf', face: 'front' }, align: { x: { mm: 350 } } })
149
149
  return serializeFigureAsset(a)
150
150
  },
151
151
  /* A ball: no face to lay on the floor, and not said to be tilted. */
@@ -11,7 +11,7 @@ async function start() {
11
11
  asset = applyFigureAuthoring(asset, { kind: 'declare-instance-size', range: { x: { min: 50, max: 400 }, y: { min: 50, max: 400 }, z: { min: 50, max: 400 } }, at: { x: 100, y: 100, z: 100 } })
12
12
  asset = applyFigureAuthoring(asset, { kind: 'follow-size', part: 'block', axes: ['x', 'y', 'z'] })
13
13
  asset = applyFigureAuthoring(asset, { kind: 'set-joint', joint: {
14
- id: 'block', type: 'prismatic', body0: { plane: 'mounting-plane' }, body1: { part: 'block', face: 'bottom' }, axis: 'X', lowerLimit: 0, upperLimit: 200, state: { id: 'shift', start: 0 }
14
+ id: 'block', type: 'prismatic', body0: { plane: 'mounting-plane' }, body1: { part: 'block' }, axis: 'X', lowerLimit: 0, upperLimit: 200, state: { id: 'shift', start: 0 }
15
15
  } })
16
16
  asset = editFigurePart(asset, { kind: 'set-driver', state: 'shift', clip: 'cycle', duration: 4, keys: [{ seconds: 0, value: 0 }, { seconds: 2, value: 200 }, { seconds: 4, value: 0 }] })
17
17
  /* Its room declared from its parts over the whole motion, as the modeller does before a release. */
@@ -13,7 +13,7 @@ for (let i = 0; i < PARTS; i++) {
13
13
  const id = `slide-${i}`
14
14
  asset = addFigureBox(asset, { id, size: { x: 40, y: 40, z: 40 }, position: { x: i * 60, y: 20, z: 0 } })
15
15
  asset = applyFigureAuthoring(asset, { kind: 'set-joint', joint: {
16
- id, type: 'prismatic', body0: { plane: 'mounting-plane' }, body1: { part: id, face: 'bottom' }, axis: 'Z', lowerLimit: 0, upperLimit: 100, state: { id: `${id}-z`, start: 0 }
16
+ id, type: 'prismatic', body0: { plane: 'mounting-plane' }, body1: { part: id }, axis: 'Z', lowerLimit: 0, upperLimit: 100, state: { id: `${id}-z`, start: 0 }
17
17
  } })
18
18
  asset = editFigurePart(asset, { kind: 'set-driver', state: `${id}-z`, clip: `cycle-${i}`, duration: 2, keys: [{ seconds: 0, value: 0 }, { seconds: 1, value: 100 }, { seconds: 2, value: 0 }] })
19
19
  }
@@ -50,7 +50,7 @@ window.figures = {
50
50
  a = addFigureBox(a, { id: 'frame', size: { x: 2000, y: 100, z: 200 }, position: { x: 0, y: 50, z: 0 } })
51
51
  for (const [id, x] of [['left', -500], ['right', 500]]) {
52
52
  a = addFigureBox(a, { id, size: { x: 900, y: 1800, z: 40 }, position: { x, y: 1000, z: 0 } })
53
- a = applyFigureAuthoring(a, { kind: 'attach', part: id, face: 'bottom', to: { part: 'frame', face: 'top' } })
53
+ a = applyFigureAuthoring(a, { kind: 'attach', part: id, to: { part: 'frame', face: 'top' } })
54
54
  const j = a.joints.find(k => k.body1.part === id)
55
55
  a = applyFigureAuthoring(a, { kind: 'set-joint', joint: { ...j, type: 'revolute', axis: 'Y', state: { id: `${id}-open` } } })
56
56
  a = editFigurePart(a, { kind: 'set-driver', state: `${id}-open`, clip: `${id}-swing`, duration: 2, keys: [{ seconds: 0, value: 0 }, { seconds: 2, value: 80 }] })
@@ -66,13 +66,13 @@ const words = () => read(() => [...(findDeep('div[overhang]')?.querySelectorAll(
66
66
  /* A 400 × 1000 deck; a 100 mm block on it, fastened 1000 mm off its centre along X -- past its low edge. */
67
67
  function figure(withChild = false) {
68
68
  let a = addFigureBox(createFigureAsset('Deck'), { id: 'deck', size: { x: 400, y: 50, z: 1000 }, position: { x: 0, y: 25, z: 0 } })
69
- a = applyFigureAuthoring(a, { kind: 'attach', part: 'deck', face: 'bottom', to: { plane: 'mounting-plane' } })
69
+ a = applyFigureAuthoring(a, { kind: 'attach', part: 'deck', to: { plane: 'mounting-plane' } })
70
70
  a = addFigureBox(a, { id: 'block', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 100, z: 0 } })
71
- a = applyFigureAuthoring(a, { kind: 'attach', part: 'block', face: 'bottom', to: { part: 'deck', face: 'top' }, ...(withChild ? {} : { align: { x: { mm: -1000 } } }) })
71
+ a = applyFigureAuthoring(a, { kind: 'attach', part: 'block', to: { part: 'deck', face: 'top' }, ...(withChild ? {} : { align: { x: { mm: -1000 } } }) })
72
72
  if (!withChild) return a
73
73
  /* The block inside; a cap on it, past the block's edge. */
74
74
  a = addFigureBox(a, { id: 'cap', size: { x: 40, y: 40, z: 40 }, position: { x: 0, y: 170, z: 0 } })
75
- return applyFigureAuthoring(a, { kind: 'attach', part: 'cap', face: 'bottom', to: { part: 'block', face: 'top' }, align: { x: { mm: 300 } } })
75
+ return applyFigureAuthoring(a, { kind: 'attach', part: 'cap', to: { part: 'block', face: 'top' }, align: { x: { mm: 300 } } })
76
76
  }
77
77
 
78
78
  describe('정렬·간격 고치기 moves a part inside the face it is fastened on', () => {
@@ -179,7 +179,7 @@ async function type(field, text) {
179
179
  }
180
180
 
181
181
  const box = (asset, id, [x, y, z], at = [0, 0, 0]) => addFigureBox(asset, { id, size: { x, y, z }, position: { x: at[0], y: at[1], z: at[2] } })
182
- const fasten = (asset, part, to, more = {}) => applyFigureAuthoring(asset, { kind: 'attach', part, face: 'bottom', to: to === 'plane' ? { plane: 'mounting-plane' } : { part: to, face: 'top' }, ...more })
182
+ const fasten = (asset, part, to, more = {}) => applyFigureAuthoring(asset, { kind: 'attach', part, to: to === 'plane' ? { plane: 'mounting-plane' } : { part: to, face: 'top' }, ...more })
183
183
  const SIZE = { x: { min: 100, max: 4000 }, y: { min: 100, max: 4000 }, z: { min: 100, max: 4000 } }
184
184
 
185
185
  describe('a size rule that would follow itself, and a fastening that would hang from itself, are refused and said', () => {
@@ -202,7 +202,7 @@ describe('a size rule that would follow itself, and a fastening that would hang
202
202
  test('a fastened to b, which hangs from it: refused, said as a loop, nothing written', async () => {
203
203
  await pick('a')
204
204
  const start = await state()
205
- await read(() => look.tell('shape', 'command', { action: { kind: 'attach', part: 'a', face: 'bottom', to: { part: 'b', face: 'top' } } }))
205
+ await read(() => look.tell('shape', 'command', { action: { kind: 'attach', part: 'a', to: { part: 'b', face: 'top' } } }))
206
206
  await until(() => probe.state().error === 'figure.text.fasten-would-loop', [], 'the loop said')
207
207
  assert.equal((await state()).revision, start.revision, 'nothing written')
208
208
  })
@@ -419,7 +419,7 @@ describe('a new fastening for a door leaf: chosen in the window, seen before it
419
419
  })
420
420
 
421
421
  test('a refused command clears what was drawn and says why, in the window', async () => {
422
- await read(() => look.tell('dialog', 'preview-command', { action: { kind: 'attach', part: 'leaf', face: 'bottom', to: { part: 'frame', face: 'nowhere' } } }))
422
+ await read(() => look.tell('dialog', 'preview-command', { action: { kind: 'attach', part: 'leaf', to: { part: 'frame', face: 'nowhere' } } }))
423
423
  await until(() => look.all('[preview-words]', 'figure.text.preview-refused').length > 0, [], 'the refusal said')
424
424
  assert.deepEqual(await ghosts(), [], 'nothing of the earlier result drawn')
425
425
  })
@@ -490,15 +490,26 @@ async function pressFixAlignment() {
490
490
  describe('past the edge of the face a part is fastened to', () => {
491
491
  before(async () => {
492
492
  let a = fasten(box(createFigureAsset('Overhangs'), 'frame', [1000, 100, 400], [0, 50, 0]), 'frame', 'plane')
493
- /* Pushed 30 mm past the frame's right edge; the same, turned; on a drum's round end; wider than the frame, off to one side. */
493
+ /* Pushed 30 mm past the frame's right edge; a mesh turned on it, its box past the edge; on a drum's round end; wider than the frame, off to one side. */
494
494
  a = fasten(box(a, 'out', [100, 100, 100]), 'out', 'frame', { align: { x: { mm: 480 } } })
495
- a = fasten(box(a, 'tilted', [100, 100, 100]), 'tilted', 'frame', { align: { x: { mm: 460 } }, twist: 30 })
495
+ const mesh = { asset: 'a'.repeat(64), format: 'glb', unit: 'mm', bounds: { min: { x: -50, y: -50, z: -50 }, max: { x: 50, y: 50, z: 50 } }, triangles: 12, primitives: 1 }
496
+ a = addFigurePrimitive(a, { id: 'tilted', kind: 'mesh', dimensions: { size: 100 }, mesh, position: { x: 0, y: 0, z: 0 } })
497
+ a = applyFigureAuthoring(a, { kind: 'set-joint', joint: { id: 'tilted', type: 'fixed', body0: { part: 'frame', face: 'top' }, body1: { part: 'tilted' }, origin: { align: { x: { mm: 460 } }, childRotate: { y: 30 } } } })
496
498
  a = fasten(addFigurePrimitive(a, { id: 'drum', kind: 'cylinder', dimensions: { radius: 100, length: 200 }, position: { x: 0, y: 0, z: 0 } }), 'drum', 'frame')
497
499
  a = fasten(box(a, 'cap', [50, 50, 50]), 'cap', 'drum')
498
500
  await mount('overhangs', fasten(box(a, 'plate', [1600, 20, 200]), 'plate', 'frame', { align: { x: { mm: 200 } } }))
499
501
  })
500
502
  const overhang = () => read(() => look.all('div[overhang] p').map(p => p.text))
501
503
 
504
+ test('the face a part is held on is lit while the panel names it, whichever part is chosen (UX lane, ADR-0140 3.11)', async () => {
505
+ const litAt = y => Math.abs((document.querySelector('figure-editor').shadowRoot.querySelector('figure-canvas').highlight?.pose.t[1] ?? 0) - y) < 2
506
+ await pick('out')
507
+ await until(() => look.all('div[fastened] p', 'figure.text.held-on-part').length + look.all('p[fasten-sentence]').length > 0, [], 'the panel names where it is held')
508
+ await until(litAt, [102], "the frame's top lit (the lit plate a little over it)")
509
+ await pick('cap')
510
+ await until(litAt, [302], "the drum's top lit")
511
+ })
512
+
502
513
  test('past it: said, and "fix alignment" moves it inside the face as one step', async () => {
503
514
  await pick('out')
504
515
  await until(() => look.all('div[overhang] p', 'figure.text.overhang-out').length === 1, [], 'said past the edge')
@@ -508,7 +519,7 @@ describe('past the edge of the face a part is fastened to', () => {
508
519
  await until(() => look.all('div[overhang] p', 'figure.text.overhang-out').length === 0, [], 'no longer past the edge')
509
520
  })
510
521
 
511
- test('turned, it may be past it: said so', async () => {
522
+ test('a mesh turned on the face, its box past it, may be past it: said so', async () => {
512
523
  await pick('tilted')
513
524
  await until(() => look.all('div[overhang] p', 'figure.text.overhang-possible').length === 1, [], 'said it may be')
514
525
  })
@@ -596,10 +607,11 @@ describe('a bench: what each part is fastened to, what detaching would do, a row
596
607
  })
597
608
  })
598
609
 
599
- /* A blade fastened twisted on a hub: no face figure-model can keep for a size drag. */
610
+ /* A blade fastened tilted on a hub (off a quarter turn): no side of its own figure-model can keep for a size drag. */
600
611
  function twisted() {
601
612
  const a = fasten(box(createFigureAsset('Fan'), 'hub', [200, 100, 200], [0, 50, 0]), 'hub', 'plane')
602
- return fasten(box(a, 'blade', [300, 20, 60], [0, 110, 0]), 'blade', 'hub', { twist: 30 })
613
+ const on = fasten(box(a, 'blade', [300, 20, 60], [0, 110, 0]), 'blade', 'hub')
614
+ return applyFigureAuthoring(on, { kind: 'set-joint', joint: { ...on.joints.find(j => j.id === 'blade'), origin: { childRotate: { z: 30 } } } })
603
615
  }
604
616
 
605
617
  /* The same blade, every side of it following the hub's (as the fan's blade-2 on :3300): no size of its own to type. */
@@ -67,11 +67,11 @@ function sourceOf(kind, text) {
67
67
  let asset = createFigureAsset('Rollers')
68
68
  asset.palette = { ...asset.palette, ...PALETTE }
69
69
  asset = addFigureBox(asset, { id: 'bed', size: { x: 2000, y: 400, z: 600 }, position: { x: 0, y: 200, z: 0 } })
70
- asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'bed', face: 'bottom', to: { plane: 'mounting-plane' } })
70
+ asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'bed', to: { plane: 'mounting-plane' } })
71
71
  asset = addFigurePrimitive(asset, { id: 'roller', kind: 'cylinder', dimensions: { radius: 40, length: 560 }, position: { x: 0, y: 0, z: 0 }, rotation: { x: 90, y: 0, z: 0 } })
72
- asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'roller', face: 'centre', to: { part: 'bed', face: 'top' }, gap: 40 })
72
+ asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'roller', facing: 'centre', to: { part: 'bed', face: 'top' }, gap: 40 })
73
73
  asset = addFigureBox(asset, { id: 'mark', size: { x: 30, y: 30, z: 560 }, position: { x: 0, y: 0, z: 0 } })
74
- asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'mark', face: 'left', to: { part: 'roller', face: 'right' } })
74
+ asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'mark', to: { part: 'roller', face: 'right' } })
75
75
  const j = asset.joints.find(o => o.body1.part === 'roller')
76
76
  asset = applyFigureAuthoring(asset, { kind: 'set-joint', joint: { ...j, type: 'revolute', axis: 'Z', lowerLimit: -360, upperLimit: 360, state: { id: 'spin' } } })
77
77
  return serializeFigureAsset(applyFigureAuthoring(asset, { kind: 'make-repeat', part: 'roller', axis: 'x', pitch: 200 }))
@@ -50,9 +50,9 @@ async function click(selector, options = {}, what = selector) {
50
50
  /* box-1 on the floor, box-2 on it made into a row 300 apart: box-2's id is drawn nowhere as it is. */
51
51
  function rowed() {
52
52
  let a = addFigureBox(createFigureAsset('Rowed'), { id: 'box-1', size: { x: 1000, y: 100, z: 400 }, position: { x: 0, y: 50, z: 0 } })
53
- a = applyFigureAuthoring(a, { kind: 'attach', part: 'box-1', face: 'bottom', to: { plane: 'mounting-plane' } })
53
+ a = applyFigureAuthoring(a, { kind: 'attach', part: 'box-1', to: { plane: 'mounting-plane' } })
54
54
  a = addFigureBox(a, { id: 'box-2', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 150, z: 0 } })
55
- a = applyFigureAuthoring(a, { kind: 'attach', part: 'box-2', face: 'bottom', to: { part: 'box-1', face: 'top' } })
55
+ a = applyFigureAuthoring(a, { kind: 'attach', part: 'box-2', to: { part: 'box-1', face: 'top' } })
56
56
  a = applyFigureAuthoring(a, { kind: 'make-repeat', part: 'box-2', axis: 'x', pitch: 300 })
57
57
  /* A part off to one side, free: its mirrored copy lands on the other side. */
58
58
  return addFigureBox(a, { id: 'side', size: { x: 100, y: 100, z: 100 }, position: { x: 900, y: 50, z: 0 } })
@@ -33,7 +33,7 @@ Object.assign(window.figures, {
33
33
  let a = figure('Fastened')
34
34
  a = addFigureBox(a, { id: 'frame', size: { x: 1600, y: 100, z: 150 }, position: { x: 0, y: 50, z: 0 } })
35
35
  a = addFigureBox(a, { id: 'leaf', size: { x: 1400, y: 1000, z: 40 }, position: { x: 0, y: 600, z: 0 } })
36
- return serializeFigureAsset(applyFigureAuthoring(a, { kind: 'attach', part: 'leaf', face: 'bottom', to: { part: 'frame', face: 'top' } }))
36
+ return serializeFigureAsset(applyFigureAuthoring(a, { kind: 'attach', part: 'leaf', to: { part: 'frame', face: 'top' } }))
37
37
  },
38
38
  /* A box with rounded corners: made from the box, still called what it is. */
39
39
  /* A housing and a stray part on the floor, the space the figure takes declared from both. */
@@ -41,7 +41,7 @@ Object.assign(window.figures, {
41
41
  let a = figure('Gate')
42
42
  a = addFigureBox(a, { id: 'housing', size: { x: 300, y: 1000, z: 300 }, position: { x: 0, y: 500, z: 0 } })
43
43
  a = addFigureBox(a, { id: 'stray', size: { x: 100, y: 100, z: 100 }, position: { x: 600, y: 50, z: 0 } })
44
- for (const part of ['housing', 'stray']) a = applyFigureAuthoring(a, { kind: 'attach', part, face: 'bottom', to: { plane: 'mounting-plane' } })
44
+ for (const part of ['housing', 'stray']) a = applyFigureAuthoring(a, { kind: 'attach', part, to: { plane: 'mounting-plane' } })
45
45
  return serializeFigureAsset(applyFigureAuthoring(a, { kind: 'declare-occupancy', proposal: proposeFigureOccupancy(a), placement: 'floor' }))
46
46
  },
47
47
  rounded: () => {
@@ -611,10 +611,10 @@ describe('the page around the figure: the shell, the dock, the properties', () =
611
611
  })
612
612
 
613
613
  describe('a placement the figure\'s parts refuse, chosen on the properties tab (wall placement, 09-25)', () => {
614
- /* A post standing on the floor's plane by its bottom: a wall takes the back face, so the wall is refused. */
614
+ /* A post standing on the floor's plane: a wall's plane faces another way, so the post is named and the wall refused. */
615
615
  const post = () => {
616
616
  const a = addFigureBox(createFigureAsset('Post'), { id: 'post', size: { x: 100, y: 1000, z: 100 }, position: { x: 0, y: 500, z: 0 } })
617
- return serializeFigureAsset(applyFigureAuthoring(a, { kind: 'attach', part: 'post', face: 'bottom', to: { plane: 'mounting-plane' } }))
617
+ return serializeFigureAsset(applyFigureAuthoring(a, { kind: 'attach', part: 'post', to: { plane: 'mounting-plane' } }))
618
618
  }
619
619
  const placement = () =>
620
620
  read(() => {
@@ -640,7 +640,7 @@ describe('a placement the figure\'s parts refuse, chosen on the properties tab (
640
640
  const said = await until(() => product.deep('figure-modeller-page >>> p[fit-error]')[0]?.textContent.trim(), 'the refusal said')
641
641
  /* Said from the kernel's details (ADR-0127): the page loads no translations, so the words are the key with its values. */
642
642
  assert.match(said, /^figure\.text\./, said)
643
- assert.ok(said.includes('post') && said.includes('back'), `names the part and the face the wall takes: ${said}`)
643
+ assert.ok(said.includes('post'), `names the part to fasten again: ${said}`)
644
644
  assert.equal(await revision(), start, 'nothing written')
645
645
  assert.equal(await placement(), 'floor', 'the list says the placement the figure has')
646
646
  })
@@ -45,7 +45,7 @@ export function doorSource() {
45
45
  a.palette = { ...a.palette, ...PALETTE }
46
46
  a = addFigureBox(a, { id: 'frame', size: { x: 1000, y: 100, z: 200 }, position: { x: 0, y: 50, z: 0 } })
47
47
  a = addFigureBox(a, { id: 'leaf', size: { x: 900, y: 1800, z: 40 }, position: { x: 0, y: 1000, z: 0 } })
48
- a = applyFigureAuthoring(a, { kind: 'attach', part: 'leaf', face: 'bottom', to: { part: 'frame', face: 'top' } })
48
+ a = applyFigureAuthoring(a, { kind: 'attach', part: 'leaf', to: { part: 'frame', face: 'top' } })
49
49
  const j = a.joints.find(k => k.body1.part === 'leaf')
50
50
  a = applyFigureAuthoring(a, { kind: 'set-joint', joint: { ...j, type: 'revolute', axis: 'Y', state: { id: 'open' } } })
51
51
  a = editFigurePart(a, { kind: 'set-driver', state: 'open', clip: 'swing', duration: 2, keys: [{ seconds: 0, value: 0 }, { seconds: 2, value: 80 }] })
@@ -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
  })