@things-factory/figure-ui 10.1.115 → 10.1.117

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 (68) hide show
  1. package/client/graphql/index.ts +6 -1
  2. package/client/modeller/editor.ts +5 -11
  3. package/client/modeller/fasten-geometry.ts +1 -1
  4. package/client/modeller/figure-links.ts +52 -17
  5. package/client/modeller/joint-axis.ts +36 -0
  6. package/client/modeller/joint-drag.ts +30 -10
  7. package/client/modeller/joint-fan.ts +2 -1
  8. package/client/modeller/links.ts +70 -20
  9. package/client/modeller/recipes.ts +6 -1
  10. package/client/pages/figure-list-page.ts +4 -4
  11. package/client/types.ts +1 -1
  12. package/client/viewparts/detail-motion.ts +11 -0
  13. package/client/viewparts/figure-asset-viewer.ts +177 -0
  14. package/client/viewparts/figure-detail-dock.ts +187 -0
  15. package/dist-client/graphql/index.d.ts +5 -0
  16. package/dist-client/graphql/index.js +5 -1
  17. package/dist-client/graphql/index.js.map +1 -1
  18. package/dist-client/modeller/clips.d.ts +1 -1
  19. package/dist-client/modeller/editor.d.ts +3 -2
  20. package/dist-client/modeller/editor.js +5 -11
  21. package/dist-client/modeller/editor.js.map +1 -1
  22. package/dist-client/modeller/fasten-geometry.js +1 -1
  23. package/dist-client/modeller/fasten-geometry.js.map +1 -1
  24. package/dist-client/modeller/figure-links.d.ts +1 -0
  25. package/dist-client/modeller/figure-links.js +65 -33
  26. package/dist-client/modeller/figure-links.js.map +1 -1
  27. package/dist-client/modeller/joint-axis.d.ts +13 -0
  28. package/dist-client/modeller/joint-axis.js +31 -0
  29. package/dist-client/modeller/joint-axis.js.map +1 -0
  30. package/dist-client/modeller/joint-drag.js +29 -9
  31. package/dist-client/modeller/joint-drag.js.map +1 -1
  32. package/dist-client/modeller/joint-fan.js +2 -1
  33. package/dist-client/modeller/joint-fan.js.map +1 -1
  34. package/dist-client/modeller/links.d.ts +34 -4
  35. package/dist-client/modeller/links.js +59 -18
  36. package/dist-client/modeller/links.js.map +1 -1
  37. package/dist-client/modeller/recipes.js +5 -1
  38. package/dist-client/modeller/recipes.js.map +1 -1
  39. package/dist-client/pages/figure-list-page.d.ts +2 -3
  40. package/dist-client/pages/figure-list-page.js +4 -4
  41. package/dist-client/pages/figure-list-page.js.map +1 -1
  42. package/dist-client/route.d.ts +1 -1
  43. package/dist-client/tsconfig.tsbuildinfo +1 -1
  44. package/dist-client/types.d.ts +1 -1
  45. package/dist-client/types.js.map +1 -1
  46. package/dist-client/viewparts/detail-motion.d.ts +6 -0
  47. package/dist-client/viewparts/detail-motion.js +9 -0
  48. package/dist-client/viewparts/detail-motion.js.map +1 -0
  49. package/dist-client/viewparts/figure-asset-viewer.d.ts +31 -0
  50. package/dist-client/viewparts/figure-asset-viewer.js +198 -0
  51. package/dist-client/viewparts/figure-asset-viewer.js.map +1 -0
  52. package/dist-client/viewparts/figure-detail-dock.d.ts +36 -0
  53. package/dist-client/viewparts/figure-detail-dock.js +224 -0
  54. package/dist-client/viewparts/figure-detail-dock.js.map +1 -0
  55. package/package.json +6 -6
  56. package/test/browser/detail-page.js +34 -0
  57. package/test/browser/serve-detail.mjs +10 -0
  58. package/test/command-preview.test.ts +2 -2
  59. package/test/detail-motion.test.ts +26 -0
  60. package/test/joint-frames.test.ts +293 -0
  61. package/test/links.test.ts +20 -1
  62. package/test/modeller-route.test.ts +4 -2
  63. package/test/verdict.test.ts +3 -1
  64. package/translations/en.json +20 -2
  65. package/translations/ja.json +20 -2
  66. package/translations/ko.json +20 -2
  67. package/translations/ms.json +20 -2
  68. package/translations/zh.json +20 -2
@@ -1,13 +1,19 @@
1
1
  import * as THREE from 'three';
2
- import { compileFigureAsset } from '@hatiolab/figure-model';
2
+ import { compileFigureAsset, figureMimicTermsOf } from '@hatiolab/figure-model';
3
+ import { jointAxisUnit, jointAxisWord } from './joint-axis.js';
3
4
  const AXES = ['x', 'y', 'z'];
4
5
  /** The axis each face faces along, and which way (as figure-model's `FIGURE_FACES`). */
5
6
  const FACE_AXIS = { left: 'x', right: 'x', bottom: 'y', top: 'y', back: 'z', front: 'z' };
6
7
  const FACE_SIGN = { left: -1, right: 1, bottom: -1, top: 1, back: -1, front: 1 };
7
8
  const matrixOf = (r) => new THREE.Matrix4().set(r[0][0], r[0][1], r[0][2], 0, r[1][0], r[1][1], r[1][2], 0, r[2][0], r[2][1], r[2][2], 0, 0, 0, 0, 1);
8
9
  const eulerMatrix = (deg) => new THREE.Matrix4().makeRotationFromEuler(new THREE.Euler(...deg.map(THREE.MathUtils.degToRad), 'XYZ'));
9
- const unit = (axis) => new THREE.Vector3(/x/i.test(axis) ? 1 : 0, /y/i.test(axis) ? 1 : 0, /z/i.test(axis) ? 1 : 0);
10
+ const unit = (axis) => new THREE.Vector3(...(jointAxisUnit(axis) ?? [0, 0, 0]));
10
11
  const round = (v, step) => Number((Math.round(v / step) * step).toFixed(Math.max(0, -Math.floor(Math.log10(step)))));
12
+ /** The joint frame's words of ADR-0107, read without the kernel's types (an older kernel has none). */
13
+ const framesOf = (j) => (j.origin ?? {});
14
+ /** `origin.rotate`, URDF's rpy: Rz·Ry·Rx about the fixed axes. */
15
+ const rotateMatrix = (j) => eulerMatrixOrder(AXES.map(a => framesOf(j).rotate?.[a] ?? 0), 'ZYX');
16
+ const eulerMatrixOrder = (deg, order) => new THREE.Matrix4().makeRotationFromEuler(new THREE.Euler(...deg.map(THREE.MathUtils.degToRad), order));
11
17
  /** The part's own quarter turn in the joint frame, as the record says it. */
12
18
  const turnMatrix = (j) => eulerMatrix(AXES.map(a => j.origin?.turn?.[a] ?? 0));
13
19
  /** The joint frame's rotation in the figure: the part's rotation now, less its own turn and less the joint's. */
@@ -31,7 +37,7 @@ export function jointDragOf(asset, id, part, result, values) {
31
37
  if (!turns)
32
38
  return { kind: 'refused', reason: j.type === 'prismatic' ? 'turned-slide' : 'fixed-turn', joint: j.id };
33
39
  if (j.mimic)
34
- return { kind: 'refused', reason: 'mimic', joint: j.id, leader: j.mimic.joint };
40
+ return { kind: 'refused', reason: 'mimic', joint: j.id, leader: figureMimicTermsOf(j.mimic).sum.map(t => t.joint).join(', ') };
35
41
  /* The turn about the joint's axis (the twist of the change): the only turn the joint can make. */
36
42
  const before = new THREE.Quaternion().setFromRotationMatrix(matrixOf(part.pose.r));
37
43
  const after = new THREE.Quaternion().setFromRotationMatrix(eulerMatrix(result.rotation));
@@ -41,7 +47,7 @@ export function jointDragOf(asset, id, part, result, values) {
41
47
  /* Turned about another axis: the joint cannot make that turn, and nothing is written for it. */
42
48
  const whole = 2 * Math.acos(Math.min(1, Math.abs(change.w)));
43
49
  if (Math.abs(twist) < whole * 0.5)
44
- return { kind: 'refused', reason: 'off-axis', joint: j.id, axis: j.axis };
50
+ return { kind: 'refused', reason: 'off-axis', joint: j.id, axis: jointAxisWord(j.axis) };
45
51
  let value = now + THREE.MathUtils.radToDeg(twist);
46
52
  if (endless)
47
53
  value = ((((value + 180) % 360) + 360) % 360) - 180;
@@ -55,7 +61,7 @@ export function jointDragOf(asset, id, part, result, values) {
55
61
  const frame = jointFrameRotation(j, part, turns ? now : 0);
56
62
  if (j.type === 'prismatic') {
57
63
  if (j.mimic)
58
- return { kind: 'refused', reason: 'mimic', joint: j.id, leader: j.mimic.joint };
64
+ return { kind: 'refused', reason: 'mimic', joint: j.id, leader: figureMimicTermsOf(j.mimic).sum.map(t => t.joint).join(', ') };
59
65
  /* How far one unit of the value moves the part along its axis, measured on the figure itself (a ratio travels a length). */
60
66
  const along = unit(j.axis).applyMatrix4(frame).normalize();
61
67
  const at = (v) => {
@@ -69,12 +75,26 @@ export function jointDragOf(asset, id, part, result, values) {
69
75
  return { kind: 'value', input: stateId, unit: j.travel ? 'ratio' : 'mm', value: round(value, j.travel ? 0.001 : 1) };
70
76
  }
71
77
  }
72
- /* Moving the joint's origin: the change in the frame the joint sits in. */
73
- const local = moved.clone().applyMatrix4(frame.clone().invert());
78
+ /* Moving the joint's origin: the change in the frame it is placed in, before `origin.rotate` turns the joint frame. */
79
+ const local = moved.clone().applyMatrix4(frame.clone().multiply(rotateMatrix(j).invert()).invert());
80
+ const origin = { ...(j.origin ?? {}) };
81
+ if (!('plane' in j.body0) && !('face' in j.body0)) {
82
+ /* At the parent's origin there is no face: the frame moves by its offset, along the parent's axes. */
83
+ const offset = { ...(framesOf(j).offset ?? {}) };
84
+ for (const axis of AXES) {
85
+ const d = local[axis];
86
+ if (Math.abs(d) < 0.5)
87
+ continue;
88
+ const was = offset[axis] ?? 0;
89
+ if (typeof was !== 'number')
90
+ return { kind: 'refused', reason: 'origin-follows', joint: j.id };
91
+ offset[axis] = round(was + d, 1);
92
+ }
93
+ return { kind: 'joint', joint: { ...j, origin: { ...origin, offset } } };
94
+ }
74
95
  const normal = 'plane' in j.body0 ? 'y' : FACE_AXIS[j.body0.face];
75
96
  /* Which way a larger gap moves the part: out of the face it meets (on the plane: up when it stands, down when it hangs). */
76
- const outward = 'plane' in j.body0 ? (j.body1.face === 'bottom' ? 1 : -1) : ((j.origin?.facing ?? 'meet') === 'meet' ? 1 : -1) * FACE_SIGN[j.body0.face];
77
- const origin = { ...(j.origin ?? {}) };
97
+ const outward = 'plane' in j.body0 ? ('face' in j.body1 && j.body1.face === 'bottom' ? 1 : -1) : ((j.origin?.facing ?? 'meet') === 'meet' ? 1 : -1) * FACE_SIGN[j.body0.face];
78
98
  const along = local[normal];
79
99
  if (Math.abs(along) >= 0.5) {
80
100
  const gap = origin.gap ?? 0;
@@ -1 +1 @@
1
- {"version":3,"file":"joint-drag.js","sourceRoot":"","sources":["../../client/modeller/joint-drag.ts"],"names":[],"mappings":"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","sourcesContent":["import * as THREE from 'three'\nimport { compileFigureAsset } from '@hatiolab/figure-model'\nimport type { FigureAsset, FigureJoint } from '@hatiolab/figure-model'\n\nimport type { GizmoResult } from './gizmo.js'\nimport type { FigureRenderPart } from './renderer.js'\n\n/*\n * A gizmo drag on a part that hangs from a joint edits the joint, not the part (ADR-0093; r3 §3: the modeller changes\n * the inputs of the relation, and never breaks it because a computed place was moved):\n *\n * - turning a part on a revolute or continuous joint sets the joint's value -- the turn about the joint's axis;\n * - sliding a part on a prismatic joint along its axis sets its value;\n * - moving a part any other way moves the joint's origin: the gap along the face it sits on, the offset across it.\n *\n * What would break a relation is refused and said: an origin that follows another part's size, a joint that mimics\n * another, a turn on a joint that does not turn.\n */\n\nexport type JointDrag =\n | { kind: 'value'; input: string; unit: 'deg' | 'mm' | 'ratio'; value: number }\n | { kind: 'joint'; joint: FigureJoint }\n | { kind: 'refused'; reason: 'mimic' | 'fixed-turn' | 'origin-follows' | 'turned-slide' | 'off-axis'; joint: string; leader?: string; axis?: string }\n\ntype Axis = 'x' | 'y' | 'z'\nconst AXES: Axis[] = ['x', 'y', 'z']\n/** The axis each face faces along, and which way (as figure-model's `FIGURE_FACES`). */\nconst FACE_AXIS: Record<string, Axis> = { left: 'x', right: 'x', bottom: 'y', top: 'y', back: 'z', front: 'z' }\nconst FACE_SIGN: Record<string, number> = { left: -1, right: 1, bottom: -1, top: 1, back: -1, front: 1 }\n\nconst matrixOf = (r: number[][]) => new THREE.Matrix4().set(r[0]![0]!, r[0]![1]!, r[0]![2]!, 0, r[1]![0]!, r[1]![1]!, r[1]![2]!, 0, r[2]![0]!, r[2]![1]!, r[2]![2]!, 0, 0, 0, 0, 1)\nconst eulerMatrix = (deg: number[]) => new THREE.Matrix4().makeRotationFromEuler(new THREE.Euler(...(deg.map(THREE.MathUtils.degToRad) as [number, number, number]), 'XYZ'))\nconst unit = (axis: string) => new THREE.Vector3(/x/i.test(axis) ? 1 : 0, /y/i.test(axis) ? 1 : 0, /z/i.test(axis) ? 1 : 0)\nconst round = (v: number, step: number) => Number((Math.round(v / step) * step).toFixed(Math.max(0, -Math.floor(Math.log10(step)))))\n\n/** The part's own quarter turn in the joint frame, as the record says it. */\nconst turnMatrix = (j: FigureJoint) => eulerMatrix(AXES.map(a => j.origin?.turn?.[a] ?? 0))\n\n/** The joint frame's rotation in the figure: the part's rotation now, less its own turn and less the joint's. */\nfunction jointFrameRotation(j: FigureJoint, part: FigureRenderPart, angle: number): THREE.Matrix4 {\n const now = matrixOf(part.pose.r as number[][])\n const own = turnMatrix(j)\n const turn = j.type === 'revolute' ? new THREE.Matrix4().makeRotationAxis(unit(j.axis!), THREE.MathUtils.degToRad(angle)) : new THREE.Matrix4()\n return now.multiply(own.invert()).multiply(turn.invert())\n}\n\nexport function jointDragOf(asset: FigureAsset, id: string, part: FigureRenderPart, result: GizmoResult, values: Record<string, number>): JointDrag | null {\n const j = asset.joints?.find(joint => joint.body1.part === id)\n /* A size is the part's own, not the joint's: a scale drag goes to the part's measures (the editor drops the shift). */\n if (!j || result.scale || (!result.rotation && !result.position)) return null\n const stateId = j.state?.id ?? j.id\n const now = values[stateId] ?? asset.stateDefaults[stateId] ?? 0\n const turns = j.type === 'revolute'\n /* A revolute joint without limits turns without end (USD). */\n const endless = turns && j.lowerLimit === undefined && j.upperLimit === undefined\n\n if (result.rotation) {\n if (!turns) return { kind: 'refused', reason: j.type === 'prismatic' ? 'turned-slide' : 'fixed-turn', joint: j.id }\n if (j.mimic) return { kind: 'refused', reason: 'mimic', joint: j.id, leader: j.mimic.joint }\n /* The turn about the joint's axis (the twist of the change): the only turn the joint can make. */\n const before = new THREE.Quaternion().setFromRotationMatrix(matrixOf(part.pose.r as number[][]))\n const after = new THREE.Quaternion().setFromRotationMatrix(eulerMatrix(result.rotation))\n const change = after.multiply(before.invert())\n const axis = unit(j.axis!).applyMatrix4(jointFrameRotation(j, part, now)).normalize()\n const twist = 2 * Math.atan2(change.x * axis.x + change.y * axis.y + change.z * axis.z, change.w)\n /* Turned about another axis: the joint cannot make that turn, and nothing is written for it. */\n const whole = 2 * Math.acos(Math.min(1, Math.abs(change.w)))\n if (Math.abs(twist) < whole * 0.5) return { kind: 'refused', reason: 'off-axis', joint: j.id, axis: j.axis! }\n let value = now + THREE.MathUtils.radToDeg(twist)\n if (endless) value = ((((value + 180) % 360) + 360) % 360) - 180\n else value = Math.min(Math.max(value, j.lowerLimit!), j.upperLimit!)\n return { kind: 'value', input: stateId, unit: 'deg', value: round(value, 0.1) }\n }\n\n const moved = new THREE.Vector3(...result.position!).sub(new THREE.Vector3(...(part.pose.t as [number, number, number])))\n if (moved.length() < 0.5) return null\n const frame = jointFrameRotation(j, part, turns ? now : 0)\n\n if (j.type === 'prismatic') {\n if (j.mimic) return { kind: 'refused', reason: 'mimic', joint: j.id, leader: j.mimic.joint }\n /* How far one unit of the value moves the part along its axis, measured on the figure itself (a ratio travels a length). */\n const along = unit(j.axis!).applyMatrix4(frame).normalize()\n const at = (v: number) => {\n const g = compileFigureAsset(asset).evaluate({ ...values, [stateId]: v }).geometry.find(g => g.id === id && 'pose' in g) as { pose: { t: number[] } } | undefined\n return g ? new THREE.Vector3(...(g.pose.t as [number, number, number])).dot(along) : 0\n }\n const step = j.travel ? 0.01 : 1\n const rate = (at(now + step) - at(now)) / step\n if (Math.abs(rate) > 1e-9 && Math.abs(moved.dot(along)) >= moved.length() * 0.7) {\n const value = Math.min(Math.max(now + moved.dot(along) / rate, j.lowerLimit!), j.upperLimit!)\n return { kind: 'value', input: stateId, unit: j.travel ? 'ratio' : 'mm', value: round(value, j.travel ? 0.001 : 1) }\n }\n }\n\n /* Moving the joint's origin: the change in the frame the joint sits in. */\n const local = moved.clone().applyMatrix4(frame.clone().invert())\n const normal: Axis = 'plane' in j.body0 ? 'y' : FACE_AXIS[j.body0.face]!\n /* Which way a larger gap moves the part: out of the face it meets (on the plane: up when it stands, down when it hangs). */\n const outward = 'plane' in j.body0 ? (j.body1.face === 'bottom' ? 1 : -1) : ((j.origin?.facing ?? 'meet') === 'meet' ? 1 : -1) * FACE_SIGN[j.body0.face]!\n const origin: NonNullable<FigureJoint['origin']> = { ...(j.origin ?? {}) }\n const along = local[normal]\n if (Math.abs(along) >= 0.5) {\n const gap = origin.gap ?? 0\n if (typeof gap !== 'number') return { kind: 'refused', reason: 'origin-follows', joint: j.id }\n origin.gap = round(gap + outward * along, 1)\n }\n for (const axis of AXES.filter(a => a !== normal)) {\n const d = local[axis]\n if (Math.abs(d) < 0.5) continue\n const how = origin.align?.[axis] ?? 'centre'\n const was = how === 'centre' ? 0 : typeof how === 'object' && typeof how.mm === 'number' ? how.mm : null\n if (was === null) return { kind: 'refused', reason: 'origin-follows', joint: j.id }\n origin.align = { ...(origin.align ?? {}), [axis]: { mm: round(was + d, 1) } }\n }\n return { kind: 'joint', joint: { ...j, origin } }\n}\n"]}
1
+ {"version":3,"file":"joint-drag.js","sourceRoot":"","sources":["../../client/modeller/joint-drag.ts"],"names":[],"mappings":"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","sourcesContent":["import * as THREE from 'three'\nimport { compileFigureAsset, figureMimicTermsOf } from '@hatiolab/figure-model'\nimport type { FigureAsset, FigureJoint } from '@hatiolab/figure-model'\n\nimport type { GizmoResult } from './gizmo.js'\nimport { jointAxisUnit, jointAxisWord } from './joint-axis.js'\nimport type { FigureRenderPart } from './renderer.js'\n\n/*\n * A gizmo drag on a part that hangs from a joint edits the joint, not the part (ADR-0093; r3 §3: the modeller changes\n * the inputs of the relation, and never breaks it because a computed place was moved):\n *\n * - turning a part on a revolute or continuous joint sets the joint's value -- the turn about the joint's axis;\n * - sliding a part on a prismatic joint along its axis sets its value;\n * - moving a part any other way moves the joint's origin: the gap along the face it sits on, the offset across it;\n * at a parent's origin (ADR-0107 decision 4), its `origin.offset` in the parent's frame.\n *\n * What would break a relation is refused and said: an origin that follows another part's size, a joint that mimics\n * another, a turn on a joint that does not turn.\n */\n\nexport type JointDrag =\n | { kind: 'value'; input: string; unit: 'deg' | 'mm' | 'ratio'; value: number }\n | { kind: 'joint'; joint: FigureJoint }\n | { kind: 'refused'; reason: 'mimic' | 'fixed-turn' | 'origin-follows' | 'turned-slide' | 'off-axis'; joint: string; leader?: string; axis?: string }\n\ntype Axis = 'x' | 'y' | 'z'\nconst AXES: Axis[] = ['x', 'y', 'z']\n/** The axis each face faces along, and which way (as figure-model's `FIGURE_FACES`). */\nconst FACE_AXIS: Record<string, Axis> = { left: 'x', right: 'x', bottom: 'y', top: 'y', back: 'z', front: 'z' }\nconst FACE_SIGN: Record<string, number> = { left: -1, right: 1, bottom: -1, top: 1, back: -1, front: 1 }\n\nconst matrixOf = (r: number[][]) => new THREE.Matrix4().set(r[0]![0]!, r[0]![1]!, r[0]![2]!, 0, r[1]![0]!, r[1]![1]!, r[1]![2]!, 0, r[2]![0]!, r[2]![1]!, r[2]![2]!, 0, 0, 0, 0, 1)\nconst eulerMatrix = (deg: number[]) => new THREE.Matrix4().makeRotationFromEuler(new THREE.Euler(...(deg.map(THREE.MathUtils.degToRad) as [number, number, number]), 'XYZ'))\nconst unit = (axis: unknown) => new THREE.Vector3(...(jointAxisUnit(axis) ?? [0, 0, 0]))\nconst round = (v: number, step: number) => Number((Math.round(v / step) * step).toFixed(Math.max(0, -Math.floor(Math.log10(step)))))\n\n/** The joint frame's words of ADR-0107, read without the kernel's types (an older kernel has none). */\nconst framesOf = (j: FigureJoint) => (j.origin ?? {}) as { offset?: Partial<Record<Axis, unknown>>; rotate?: Partial<Record<Axis, number>> }\n/** `origin.rotate`, URDF's rpy: Rz·Ry·Rx about the fixed axes. */\nconst rotateMatrix = (j: FigureJoint) => eulerMatrixOrder(AXES.map(a => framesOf(j).rotate?.[a] ?? 0), 'ZYX')\nconst eulerMatrixOrder = (deg: number[], order: THREE.EulerOrder) => new THREE.Matrix4().makeRotationFromEuler(new THREE.Euler(...(deg.map(THREE.MathUtils.degToRad) as [number, number, number]), order))\n\n/** The part's own quarter turn in the joint frame, as the record says it. */\nconst turnMatrix = (j: FigureJoint) => eulerMatrix(AXES.map(a => j.origin?.turn?.[a] ?? 0))\n\n/** The joint frame's rotation in the figure: the part's rotation now, less its own turn and less the joint's. */\nfunction jointFrameRotation(j: FigureJoint, part: FigureRenderPart, angle: number): THREE.Matrix4 {\n const now = matrixOf(part.pose.r as number[][])\n const own = turnMatrix(j)\n const turn = j.type === 'revolute' ? new THREE.Matrix4().makeRotationAxis(unit(j.axis!), THREE.MathUtils.degToRad(angle)) : new THREE.Matrix4()\n return now.multiply(own.invert()).multiply(turn.invert())\n}\n\nexport function jointDragOf(asset: FigureAsset, id: string, part: FigureRenderPart, result: GizmoResult, values: Record<string, number>): JointDrag | null {\n const j = asset.joints?.find(joint => joint.body1.part === id)\n /* A size is the part's own, not the joint's: a scale drag goes to the part's measures (the editor drops the shift). */\n if (!j || result.scale || (!result.rotation && !result.position)) return null\n const stateId = j.state?.id ?? j.id\n const now = values[stateId] ?? asset.stateDefaults[stateId] ?? 0\n const turns = j.type === 'revolute'\n /* A revolute joint without limits turns without end (USD). */\n const endless = turns && j.lowerLimit === undefined && j.upperLimit === undefined\n\n if (result.rotation) {\n if (!turns) return { kind: 'refused', reason: j.type === 'prismatic' ? 'turned-slide' : 'fixed-turn', joint: j.id }\n if (j.mimic) return { kind: 'refused', reason: 'mimic', joint: j.id, leader: figureMimicTermsOf(j.mimic)!.sum.map(t => t.joint).join(', ') }\n /* The turn about the joint's axis (the twist of the change): the only turn the joint can make. */\n const before = new THREE.Quaternion().setFromRotationMatrix(matrixOf(part.pose.r as number[][]))\n const after = new THREE.Quaternion().setFromRotationMatrix(eulerMatrix(result.rotation))\n const change = after.multiply(before.invert())\n const axis = unit(j.axis!).applyMatrix4(jointFrameRotation(j, part, now)).normalize()\n const twist = 2 * Math.atan2(change.x * axis.x + change.y * axis.y + change.z * axis.z, change.w)\n /* Turned about another axis: the joint cannot make that turn, and nothing is written for it. */\n const whole = 2 * Math.acos(Math.min(1, Math.abs(change.w)))\n if (Math.abs(twist) < whole * 0.5) return { kind: 'refused', reason: 'off-axis', joint: j.id, axis: jointAxisWord(j.axis) }\n let value = now + THREE.MathUtils.radToDeg(twist)\n if (endless) value = ((((value + 180) % 360) + 360) % 360) - 180\n else value = Math.min(Math.max(value, j.lowerLimit!), j.upperLimit!)\n return { kind: 'value', input: stateId, unit: 'deg', value: round(value, 0.1) }\n }\n\n const moved = new THREE.Vector3(...result.position!).sub(new THREE.Vector3(...(part.pose.t as [number, number, number])))\n if (moved.length() < 0.5) return null\n const frame = jointFrameRotation(j, part, turns ? now : 0)\n\n if (j.type === 'prismatic') {\n if (j.mimic) return { kind: 'refused', reason: 'mimic', joint: j.id, leader: figureMimicTermsOf(j.mimic)!.sum.map(t => t.joint).join(', ') }\n /* How far one unit of the value moves the part along its axis, measured on the figure itself (a ratio travels a length). */\n const along = unit(j.axis!).applyMatrix4(frame).normalize()\n const at = (v: number) => {\n const g = compileFigureAsset(asset).evaluate({ ...values, [stateId]: v }).geometry.find(g => g.id === id && 'pose' in g) as { pose: { t: number[] } } | undefined\n return g ? new THREE.Vector3(...(g.pose.t as [number, number, number])).dot(along) : 0\n }\n const step = j.travel ? 0.01 : 1\n const rate = (at(now + step) - at(now)) / step\n if (Math.abs(rate) > 1e-9 && Math.abs(moved.dot(along)) >= moved.length() * 0.7) {\n const value = Math.min(Math.max(now + moved.dot(along) / rate, j.lowerLimit!), j.upperLimit!)\n return { kind: 'value', input: stateId, unit: j.travel ? 'ratio' : 'mm', value: round(value, j.travel ? 0.001 : 1) }\n }\n }\n\n /* Moving the joint's origin: the change in the frame it is placed in, before `origin.rotate` turns the joint frame. */\n const local = moved.clone().applyMatrix4(frame.clone().multiply(rotateMatrix(j).invert()).invert())\n const origin: NonNullable<FigureJoint['origin']> = { ...(j.origin ?? {}) }\n if (!('plane' in j.body0) && !('face' in j.body0)) {\n /* At the parent's origin there is no face: the frame moves by its offset, along the parent's axes. */\n const offset = { ...(framesOf(j).offset ?? {}) }\n for (const axis of AXES) {\n const d = local[axis]\n if (Math.abs(d) < 0.5) continue\n const was = offset[axis] ?? 0\n if (typeof was !== 'number') return { kind: 'refused', reason: 'origin-follows', joint: j.id }\n offset[axis] = round(was + d, 1)\n }\n return { kind: 'joint', joint: { ...j, origin: { ...origin, offset } } as FigureJoint }\n }\n const normal: Axis = 'plane' in j.body0 ? 'y' : FACE_AXIS[j.body0.face]!\n /* Which way a larger gap moves the part: out of the face it meets (on the plane: up when it stands, down when it hangs). */\n const outward = 'plane' in j.body0 ? ('face' in j.body1 && j.body1.face === 'bottom' ? 1 : -1) : ((j.origin?.facing ?? 'meet') === 'meet' ? 1 : -1) * FACE_SIGN[j.body0.face]!\n const along = local[normal]\n if (Math.abs(along) >= 0.5) {\n const gap = origin.gap ?? 0\n if (typeof gap !== 'number') return { kind: 'refused', reason: 'origin-follows', joint: j.id }\n origin.gap = round(gap + outward * along, 1)\n }\n for (const axis of AXES.filter(a => a !== normal)) {\n const d = local[axis]\n if (Math.abs(d) < 0.5) continue\n const how = origin.align?.[axis] ?? 'centre'\n const was = how === 'centre' ? 0 : typeof how === 'object' && typeof how.mm === 'number' ? how.mm : null\n if (was === null) return { kind: 'refused', reason: 'origin-follows', joint: j.id }\n origin.align = { ...(origin.align ?? {}), [axis]: { mm: round(was + d, 1) } }\n }\n return { kind: 'joint', joint: { ...j, origin } }\n}\n"]}
@@ -1,5 +1,6 @@
1
1
  import { compileFigureAsset, compileFigureGraph, figureWorldBoxesOf, figureWritersOf } from '@hatiolab/figure-model';
2
2
  import { sectionShape } from './drawn-shapes.js';
3
+ import { jointAxisUnit } from './joint-axis.js';
3
4
  /*
4
5
  * Where the selected part turns, and how far -- a translucent fan on the editing board. An axis name says nothing about which way a part swings; on a six-axis arm the author went to the preview and
5
6
  * pushed each slider to its end to find out. The fan lies in the plane the part turns in, at the pivot, open from the
@@ -80,7 +81,7 @@ function turnOf(asset, part, values, box) {
80
81
  /* In the child's own frame: the joint is where its pose puts the seat's origin, the axis turned back by its pose. */
81
82
  const back = (v) => [0, 1, 2].map(i => pose.r[0][i] * v[0] + pose.r[1][i] * v[1] + pose.r[2][i] * v[2]);
82
83
  const pivot = back(pose.t.map(v => -v));
83
- const axis = norm(back(AXIS_NAMES.map(a => (a.toUpperCase() === record.axis ? 1 : 0))));
84
+ const axis = norm(back(jointAxisUnit(record.axis) ?? [0, 0, 0]));
84
85
  if (!axis)
85
86
  return null;
86
87
  return { carrier, pivot, axis, toward: [0, 0, 0], control, turnedPart: child };
@@ -1 +1 @@
1
- {"version":3,"file":"joint-fan.js","sourceRoot":"","sources":["../../client/modeller/joint-fan.ts"],"names":[],"mappings":"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","sourcesContent":["import { compileFigureAsset, compileFigureGraph, figureWorldBoxesOf, figureWritersOf } from '@hatiolab/figure-model'\nimport type { FigureAsset } from '@hatiolab/figure-model'\n\nimport type { FigureRenderPart } from './renderer.js'\nimport { sectionShape } from './drawn-shapes.js'\n\n/*\n * Where the selected part turns, and how far -- a translucent fan on the editing board. An axis name says nothing about which way a part swings; on a six-axis arm the author went to the preview and\n * pushed each slider to its end to find out. The fan lies in the plane the part turns in, at the pivot, open from the\n * control's lowest value to its highest; the edge at the value the figure holds now is where the part is.\n *\n * It is found by evaluating, not by reading the graph's shape: the frame that carries the part is sampled across the\n * control's range, and a point at a fixed distance from the pivot traces the fan. So the direction is right whatever\n * the slope of the control, and a joint inside other joints is drawn where its parents put it.\n *\n * Two ways a part turns are read:\n * - a joint record (ADR-0093): at the joint's origin, about its axis;\n * - a joint written as values (`<joint>.motion` · `<joint>.chain`): the part is composed on the joint's\n * chain, the pivot is the joint's origin (`<joint>.frameOnParent`).\n * A slide has no fan. Nothing is drawn when the turn cannot be found -- a fan in some guessed plane would point the\n * wrong way.\n */\n\n/** The fan's name on the board. It is not a part: not in the list, not saved, not picked. */\nexport const JOINT_FAN = '__joint-fan'\n\n/** How thick the fan is: thin enough not to hide the part, thick enough to be seen edge-on. */\nconst THICKNESS = 6\n/** About one segment every six degrees, so a full turn reads as a circle. */\nconst STEP_DEG = 6\n\ntype Pose = { r: number[][]; t: number[] }\ninterface Node {\n id: string\n op: string\n args: string[]\n outputs: Record<string, string>\n params?: Record<string, string>\n}\n\nconst apply = (p: Pose, v: number[]) => [0, 1, 2].map(i => p.r[i]![0]! * v[0]! + p.r[i]![1]! * v[1]! + p.r[i]![2]! * v[2]! + p.t[i]!)\nconst sub = (a: number[], b: number[]) => a.map((v, i) => v - b[i]!)\nconst dot = (a: number[], b: number[]) => a.reduce((s, v, i) => s + v * b[i]!, 0)\nconst cross = (a: number[], b: number[]) => [a[1]! * b[2]! - a[2]! * b[1]!, a[2]! * b[0]! - a[0]! * b[2]!, a[0]! * b[1]! - a[1]! * b[0]!]\nconst norm = (a: number[]) => {\n const l = Math.hypot(...a)\n return l > 1e-9 ? a.map(v => v / l) : null\n}\n\n/** How far outside a part's box a pivot may lie and still be on the part: the kernel's contact skin. */\nconst SEAM = 1\n/** An origin this deep in a part's box (a share of its half-measure, on every axis) is housed by the part. */\nconst HOUSED = 0.5\nconst AXIS_NAMES = ['x', 'y', 'z'] as const\ntype WorldBox = { centre: Record<'x' | 'y' | 'z', number>; extent: Record<'x' | 'y' | 'z', number> }\n\ninterface Turn {\n /** The node whose value carries the moving frame into the figure's frame. */\n carrier: string\n /** The pivot, in the moving frame. */\n pivot: number[]\n /** The axis, in the moving frame. */\n axis: number[]\n /** Where the part sits, in the moving frame -- which way the fan's reference edge points. */\n toward: number[]\n /** The part the joint turns -- the fan is sized from it, so a joint shows one fan whichever part is selected. */\n turnedPart: string\n /** The state input that turns it. */\n control: { id: string; min: number; max: number }\n}\n\nfunction turnOf(asset: FigureAsset, part: string, values: Record<string, unknown>, box: WorldBox | undefined): Turn | null {\n const m = asset.document.model as unknown as { nodes: Node[]; constants: { id: string; value: number }[]; inputs: { id: string; role?: string; min?: number; max?: number }[] }\n const node = (id: string) => m.nodes.find(n => n.id === id)\n const constant = (ref: string) => m.constants.find(c => c.id === ref)?.value\n const valueOf = (n: Node | undefined) => (n ? values[Object.values(n.outputs)[0]!] : undefined) as Pose | undefined\n const controlOf = (ref: string | undefined, seen = new Set<string>()): Turn['control'] | null => {\n if (!ref || seen.has(ref)) return null\n seen.add(ref)\n const input = m.inputs.find(i => i.id === ref && i.role === 'state')\n if (input) return { id: input.id, min: input.min ?? 0, max: input.max ?? 0 }\n const writer = m.nodes.find(n => Object.values(n.outputs).includes(ref))\n for (const arg of writer?.args ?? []) {\n const found = controlOf(arg, seen)\n if (found) return found\n }\n return null\n }\n const axisOf = (motion: Node) => {\n const axis = motion.args.slice(0, 3).map(constant)\n return axis.every(v => typeof v === 'number') ? norm(axis as number[]) : null\n }\n\n /*\n A joint record (ADR-0093): the fan stands at the joint -- the point on the parent's face the child is fastened at --\n about the joint's axis. A part that does not turn itself shows the joint that sits on it, when there is one: the\n elbow block shows the elbow (user, 2026-09-24: 「elbow-housing itself is a axis」).\n */\n const turning = (asset.joints ?? []).filter(j => j.type === 'revolute')\n const record = turning.find(j => j.body1.part === part) ?? (() => {\n const on = turning.filter(j => 'part' in j.body0 && j.body0.part === part)\n return on.length === 1 && !(asset.joints ?? []).some(j => j.body1.part === part && j.type !== 'fixed') ? on[0] : undefined\n })()\n if (record) {\n const child = record.body1.part\n const carrier = node(`${child}.world`) ? `${child}.world` : `${child}.chain`\n const pose = valueOf(node(`${child}.pose`))\n const motion = node(`${child}.motion`)\n const control = controlOf(motion?.args[3])\n if (!node(carrier) || !pose || !control) return null\n /* In the child's own frame: the joint is where its pose puts the seat's origin, the axis turned back by its pose. */\n const back = (v: number[]) => [0, 1, 2].map(i => pose.r[0]![i]! * v[0]! + pose.r[1]![i]! * v[1]! + pose.r[2]![i]! * v[2]!)\n const pivot = back(pose.t.map(v => -v))\n const axis = norm(back(AXIS_NAMES.map(a => (a.toUpperCase() === record.axis ? 1 : 0))))\n if (!axis) return null\n return { carrier, pivot, axis, toward: [0, 0, 0], control, turnedPart: child }\n }\n\n /*\n A joint written as values (`<joint>.motion`, its origin `<joint>.frameOnParent`) carries its own part and\n everything fastened below it, through its chain (the first joint of a chain has only its attachment). A part shows\n the joint it is to that part of the mechanism (user, 2026-09-24: 「elbow-housing itself is a axis」 · 「please check\n all that logic and concept」), tested in this order on the joints as they stand now:\n 1. housing -- a joint's origin lies deep inside the part, which is centred on it: the shoulder housing shows the\n shoulder and the elbow housing the elbow, though another joint carries them;\n 2. turned -- the joint that carries the part, when its axis passes through the part: through its middle for a\n body that rolls on it (the wrist roll, the tool flange, the gripper body), through its root for a link (the\n upper arm, the forearm);\n 3. otherwise none: the pedestal (the base's axis runs through it but turns it not), the fingers.\n An axis passing through a part is not enough by itself: the wrist roll's axis runs the forearm's length and the\n tool roll's through the wrist pitch, and neither turns them (an earlier rule said so, and was wrong).\n */\n if (!box) return null\n const carrierOf = (joint: string) => (node(`${joint}.chain`) ? `${joint}.chain` : `${joint}.attach`)\n const mine = node(`${part}.moved`)?.args.find(arg => /\\.(chain|attach)\\.value$/.test(arg))?.replace(/\\.value$/, '')\n const centre = AXIS_NAMES.map(a => box.centre[a])\n const half = AXIS_NAMES.map(a => box.extent[a] / 2)\n const joints = m.nodes\n .filter(n => n.op === 'axis-turn@1' && n.id.endsWith('.motion') && node(n.id.replace(/\\.motion$/, '.frameOnParent')))\n .map(motion => {\n const joint = motion.id.replace(/\\.motion$/, '')\n const carrier = carrierOf(joint)\n const frame = valueOf(node(carrier))\n const origin = valueOf(node(`${joint}.frameOnParent`))\n const local = axisOf(motion)\n if (!frame || !origin || !local) return null\n const at = apply(frame, origin.t)\n const along = norm([0, 1, 2].map(i => dot(frame.r[i]!, local)))\n if (!along) return null\n const off = sub(centre, at)\n return {\n joint,\n motion,\n carrier,\n origin,\n /* How deep the origin lies: 0 at the centre, 1 on the box's face, on the axis where it is shallowest. */\n depth: Math.max(...[0, 1, 2].map(i => Math.abs(off[i]!) / (half[i]! + SEAM))),\n fromCentre: Math.hypot(...off),\n through: lineMeetsBox(at, along, centre, half)\n }\n })\n .filter((c): c is NonNullable<typeof c> => !!c)\n const housing = joints.filter(c => c.depth <= HOUSED).sort((a, b) => a.fromCentre - b.fromCentre)[0]\n const carrying = joints.find(c => c.carrier === mine)\n const chosen = housing ?? (carrying && carrying.through ? carrying : undefined)\n if (!chosen) return null\n const axis = axisOf(chosen.motion)\n const control = controlOf(chosen.motion.args[3])\n if (!axis || !control) return null\n /* The reference edge points at what the joint turns: of the parts it carries, the one farthest off its axis. */\n const flatOff = (t: number[]) => {\n const rel = sub(t, chosen.origin.t)\n return Math.hypot(...sub(rel, axis.map(v => v * dot(rel, axis))))\n }\n const turned = m.nodes\n .filter(n => n.id.endsWith('.moved') && n.args.includes(`${chosen.carrier}.value`))\n .map(n => ({ id: n.id.replace(/\\.moved$/, ''), t: valueOf(node(n.id.replace(/\\.moved$/, '.pose')))?.t }))\n .filter((one): one is { id: string; t: number[] } => !!one.t)\n .sort((a, b) => flatOff(b.t) - flatOff(a.t))[0]\n return { carrier: chosen.carrier, pivot: chosen.origin.t, axis, toward: turned?.t ?? chosen.origin.t, control, turnedPart: turned?.id ?? part }\n}\n\n/**\n * The fan for the part, as a body to stand on the board, or null when the part does not turn. `values` are the\n * inputs the figure is drawn with now (a pose in hand included), so the fan follows the arm as it is posed.\n */\nexport function jointFanOf(asset: FigureAsset, part: string, values: Record<string, number> = {}): FigureRenderPart | null {\n if (!part) return null\n let graph: ReturnType<typeof compileFigureGraph>\n try {\n graph = compileFigureGraph(asset.document.model as never)\n } catch {\n return null\n }\n const inputs = { ...asset.designInputs, ...asset.stateDefaults, ...values }\n const at = (over: Record<string, number>) => graph.evaluate({ ...inputs, ...over }).values as Record<string, unknown>\n let now: Record<string, unknown>\n try {\n now = at({})\n } catch {\n return null\n }\n let box: WorldBox | undefined\n try {\n box = figureWorldBoxesOf(\n compileFigureAsset({ ...asset, designInputs: { ...asset.designInputs, ...pick(values, asset.designInputs) }, stateDefaults: { ...asset.stateDefaults, ...pick(values, asset.stateDefaults) } })\n .evaluate()\n .geometry.filter(g => g.id === part),\n figureWritersOf(asset)\n )[0]\n } catch {\n box = undefined\n }\n const turn = turnOf(asset, part, now, box)\n if (!turn) return null\n const carrierRef = Object.values(asset.document.model.nodes.find(n => n.id === turn.carrier)!.outputs)[0]!\n const carrierNow = now[carrierRef] as Pose | undefined\n if (!carrierNow) return null\n\n /* The reference edge points from the pivot toward the part, in the plane it turns in; any perpendicular if it sits on the axis. */\n const rel = sub(turn.toward, turn.pivot)\n const inPlane = sub(rel, turn.axis.map(v => v * dot(rel, turn.axis)))\n const d = norm(inPlane) ?? norm(cross(turn.axis, Math.abs(turn.axis[0]!) < 0.9 ? [1, 0, 0] : [0, 1, 0]))!\n /*\n Sized from what the joint turns, not from the selected part: selecting the elbow housing or the forearm\n shows the elbow's one fan, not a smaller one for the smaller part (user, 2026-09-24).\n */\n const radius = Math.max(60, Math.round(Math.max(largestMeasureOf(asset, turn.turnedPart, inputs), Math.hypot(...inPlane)) * 0.6))\n const edge = turn.pivot.map((v, i) => v + d[i]! * radius)\n\n const { min, max } = turn.control\n if (!(max > min)) return null\n const steps = Math.max(2, Math.ceil((max - min) / STEP_DEG))\n const pivot = apply(carrierNow, turn.pivot)\n const traced: number[][] = []\n try {\n for (let i = 0; i <= steps; i++) {\n const s = min + ((max - min) * i) / steps\n traced.push(apply(at({ [turn.control.id]: s })[carrierRef] as Pose, edge))\n }\n } catch {\n return null\n }\n\n /* The fan's own frame: x toward the first traced point, y along the axis as it stands now, z completing them. */\n const axisNow = norm([0, 1, 2].map(i => dot(carrierNow.r[i]!, turn.axis)))\n const first = norm(sub(traced[0]!, pivot))\n if (!axisNow || !first) return null\n const zAxis = cross(first, axisNow)\n const r = [0, 1, 2].map(i => [first[i]!, axisNow[i]!, zAxis[i]!])\n const local = (p: number[]) => {\n const v = sub(p, pivot)\n return { x: dot(v, first), z: dot(v, zAxis) }\n }\n const full = max - min >= 359.999\n const profile = [...(full ? [] : [{ x: 0, z: 0 }]), ...traced.map(local)].map(p => ({ x: Math.round(p.x * 1000) / 1000, z: Math.round(p.z * 1000) / 1000 }))\n\n return {\n id: JOINT_FAN,\n shape: sectionShape(profile, THICKNESS),\n pose: { t: pivot, r },\n material: { token: '__joint-fan', transparent: true },\n /** Which control turns it: the joint the fan shows. */\n turns: turn.control.id\n } as unknown as FigureRenderPart\n}\n\n/** The values in hand that belong to a record's keys (design inputs, or state defaults). */\nfunction pick(values: Record<string, number>, of: Record<string, number>): Record<string, number> {\n return Object.fromEntries(Object.entries(values).filter(([id]) => id in of))\n}\n\n/** A part's largest own measure (a box side, a diameter, a length) at the inputs given; 0 when it cannot be read. */\nfunction largestMeasureOf(asset: FigureAsset, part: string, inputs: Record<string, number>): number {\n try {\n const body = compileFigureAsset({ ...asset, designInputs: pick(inputs, asset.designInputs), stateDefaults: pick(inputs, asset.stateDefaults) })\n .evaluate()\n .geometry.find(g => g.id === part) as { shape?: { dimensions?: Record<string, number> } } | undefined\n const dims = Object.entries(body?.shape?.dimensions ?? {}).map(([k, v]) => (k.startsWith('radius') ? v * 2 : v))\n return dims.length ? Math.max(...dims) : 0\n } catch {\n return 0\n }\n}\n\n/** Whether the line through `at` along `along` passes through the box (centre, half-measures), within the skin. */\nfunction lineMeetsBox(at: number[], along: number[], centre: number[], half: number[]): boolean {\n let lo = -Infinity,\n hi = Infinity\n for (let i = 0; i < 3; i++) {\n const min = centre[i]! - half[i]! - SEAM,\n max = centre[i]! + half[i]! + SEAM\n if (Math.abs(along[i]!) < 1e-12) {\n if (at[i]! < min || at[i]! > max) return false\n continue\n }\n const t1 = (min - at[i]!) / along[i]!,\n t2 = (max - at[i]!) / along[i]!\n lo = Math.max(lo, Math.min(t1, t2))\n hi = Math.min(hi, Math.max(t1, t2))\n if (lo > hi) return false\n }\n return true\n}\n\n"]}
1
+ {"version":3,"file":"joint-fan.js","sourceRoot":"","sources":["../../client/modeller/joint-fan.ts"],"names":[],"mappings":"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","sourcesContent":["import { compileFigureAsset, compileFigureGraph, figureWorldBoxesOf, figureWritersOf } from '@hatiolab/figure-model'\nimport type { FigureAsset } from '@hatiolab/figure-model'\n\nimport type { FigureRenderPart } from './renderer.js'\nimport { sectionShape } from './drawn-shapes.js'\nimport { jointAxisUnit } from './joint-axis.js'\n\n/*\n * Where the selected part turns, and how far -- a translucent fan on the editing board. An axis name says nothing about which way a part swings; on a six-axis arm the author went to the preview and\n * pushed each slider to its end to find out. The fan lies in the plane the part turns in, at the pivot, open from the\n * control's lowest value to its highest; the edge at the value the figure holds now is where the part is.\n *\n * It is found by evaluating, not by reading the graph's shape: the frame that carries the part is sampled across the\n * control's range, and a point at a fixed distance from the pivot traces the fan. So the direction is right whatever\n * the slope of the control, and a joint inside other joints is drawn where its parents put it.\n *\n * Two ways a part turns are read:\n * - a joint record (ADR-0093): at the joint's origin, about its axis;\n * - a joint written as values (`<joint>.motion` · `<joint>.chain`): the part is composed on the joint's\n * chain, the pivot is the joint's origin (`<joint>.frameOnParent`).\n * A slide has no fan. Nothing is drawn when the turn cannot be found -- a fan in some guessed plane would point the\n * wrong way.\n */\n\n/** The fan's name on the board. It is not a part: not in the list, not saved, not picked. */\nexport const JOINT_FAN = '__joint-fan'\n\n/** How thick the fan is: thin enough not to hide the part, thick enough to be seen edge-on. */\nconst THICKNESS = 6\n/** About one segment every six degrees, so a full turn reads as a circle. */\nconst STEP_DEG = 6\n\ntype Pose = { r: number[][]; t: number[] }\ninterface Node {\n id: string\n op: string\n args: string[]\n outputs: Record<string, string>\n params?: Record<string, string>\n}\n\nconst apply = (p: Pose, v: number[]) => [0, 1, 2].map(i => p.r[i]![0]! * v[0]! + p.r[i]![1]! * v[1]! + p.r[i]![2]! * v[2]! + p.t[i]!)\nconst sub = (a: number[], b: number[]) => a.map((v, i) => v - b[i]!)\nconst dot = (a: number[], b: number[]) => a.reduce((s, v, i) => s + v * b[i]!, 0)\nconst cross = (a: number[], b: number[]) => [a[1]! * b[2]! - a[2]! * b[1]!, a[2]! * b[0]! - a[0]! * b[2]!, a[0]! * b[1]! - a[1]! * b[0]!]\nconst norm = (a: number[]) => {\n const l = Math.hypot(...a)\n return l > 1e-9 ? a.map(v => v / l) : null\n}\n\n/** How far outside a part's box a pivot may lie and still be on the part: the kernel's contact skin. */\nconst SEAM = 1\n/** An origin this deep in a part's box (a share of its half-measure, on every axis) is housed by the part. */\nconst HOUSED = 0.5\nconst AXIS_NAMES = ['x', 'y', 'z'] as const\ntype WorldBox = { centre: Record<'x' | 'y' | 'z', number>; extent: Record<'x' | 'y' | 'z', number> }\n\ninterface Turn {\n /** The node whose value carries the moving frame into the figure's frame. */\n carrier: string\n /** The pivot, in the moving frame. */\n pivot: number[]\n /** The axis, in the moving frame. */\n axis: number[]\n /** Where the part sits, in the moving frame -- which way the fan's reference edge points. */\n toward: number[]\n /** The part the joint turns -- the fan is sized from it, so a joint shows one fan whichever part is selected. */\n turnedPart: string\n /** The state input that turns it. */\n control: { id: string; min: number; max: number }\n}\n\nfunction turnOf(asset: FigureAsset, part: string, values: Record<string, unknown>, box: WorldBox | undefined): Turn | null {\n const m = asset.document.model as unknown as { nodes: Node[]; constants: { id: string; value: number }[]; inputs: { id: string; role?: string; min?: number; max?: number }[] }\n const node = (id: string) => m.nodes.find(n => n.id === id)\n const constant = (ref: string) => m.constants.find(c => c.id === ref)?.value\n const valueOf = (n: Node | undefined) => (n ? values[Object.values(n.outputs)[0]!] : undefined) as Pose | undefined\n const controlOf = (ref: string | undefined, seen = new Set<string>()): Turn['control'] | null => {\n if (!ref || seen.has(ref)) return null\n seen.add(ref)\n const input = m.inputs.find(i => i.id === ref && i.role === 'state')\n if (input) return { id: input.id, min: input.min ?? 0, max: input.max ?? 0 }\n const writer = m.nodes.find(n => Object.values(n.outputs).includes(ref))\n for (const arg of writer?.args ?? []) {\n const found = controlOf(arg, seen)\n if (found) return found\n }\n return null\n }\n const axisOf = (motion: Node) => {\n const axis = motion.args.slice(0, 3).map(constant)\n return axis.every(v => typeof v === 'number') ? norm(axis as number[]) : null\n }\n\n /*\n A joint record (ADR-0093): the fan stands at the joint -- the point on the parent's face the child is fastened at --\n about the joint's axis. A part that does not turn itself shows the joint that sits on it, when there is one: the\n elbow block shows the elbow (user, 2026-09-24: 「elbow-housing itself is a axis」).\n */\n const turning = (asset.joints ?? []).filter(j => j.type === 'revolute')\n const record = turning.find(j => j.body1.part === part) ?? (() => {\n const on = turning.filter(j => 'part' in j.body0 && j.body0.part === part)\n return on.length === 1 && !(asset.joints ?? []).some(j => j.body1.part === part && j.type !== 'fixed') ? on[0] : undefined\n })()\n if (record) {\n const child = record.body1.part\n const carrier = node(`${child}.world`) ? `${child}.world` : `${child}.chain`\n const pose = valueOf(node(`${child}.pose`))\n const motion = node(`${child}.motion`)\n const control = controlOf(motion?.args[3])\n if (!node(carrier) || !pose || !control) return null\n /* In the child's own frame: the joint is where its pose puts the seat's origin, the axis turned back by its pose. */\n const back = (v: number[]) => [0, 1, 2].map(i => pose.r[0]![i]! * v[0]! + pose.r[1]![i]! * v[1]! + pose.r[2]![i]! * v[2]!)\n const pivot = back(pose.t.map(v => -v))\n const axis = norm(back(jointAxisUnit(record.axis) ?? [0, 0, 0]))\n if (!axis) return null\n return { carrier, pivot, axis, toward: [0, 0, 0], control, turnedPart: child }\n }\n\n /*\n A joint written as values (`<joint>.motion`, its origin `<joint>.frameOnParent`) carries its own part and\n everything fastened below it, through its chain (the first joint of a chain has only its attachment). A part shows\n the joint it is to that part of the mechanism (user, 2026-09-24: 「elbow-housing itself is a axis」 · 「please check\n all that logic and concept」), tested in this order on the joints as they stand now:\n 1. housing -- a joint's origin lies deep inside the part, which is centred on it: the shoulder housing shows the\n shoulder and the elbow housing the elbow, though another joint carries them;\n 2. turned -- the joint that carries the part, when its axis passes through the part: through its middle for a\n body that rolls on it (the wrist roll, the tool flange, the gripper body), through its root for a link (the\n upper arm, the forearm);\n 3. otherwise none: the pedestal (the base's axis runs through it but turns it not), the fingers.\n An axis passing through a part is not enough by itself: the wrist roll's axis runs the forearm's length and the\n tool roll's through the wrist pitch, and neither turns them (an earlier rule said so, and was wrong).\n */\n if (!box) return null\n const carrierOf = (joint: string) => (node(`${joint}.chain`) ? `${joint}.chain` : `${joint}.attach`)\n const mine = node(`${part}.moved`)?.args.find(arg => /\\.(chain|attach)\\.value$/.test(arg))?.replace(/\\.value$/, '')\n const centre = AXIS_NAMES.map(a => box.centre[a])\n const half = AXIS_NAMES.map(a => box.extent[a] / 2)\n const joints = m.nodes\n .filter(n => n.op === 'axis-turn@1' && n.id.endsWith('.motion') && node(n.id.replace(/\\.motion$/, '.frameOnParent')))\n .map(motion => {\n const joint = motion.id.replace(/\\.motion$/, '')\n const carrier = carrierOf(joint)\n const frame = valueOf(node(carrier))\n const origin = valueOf(node(`${joint}.frameOnParent`))\n const local = axisOf(motion)\n if (!frame || !origin || !local) return null\n const at = apply(frame, origin.t)\n const along = norm([0, 1, 2].map(i => dot(frame.r[i]!, local)))\n if (!along) return null\n const off = sub(centre, at)\n return {\n joint,\n motion,\n carrier,\n origin,\n /* How deep the origin lies: 0 at the centre, 1 on the box's face, on the axis where it is shallowest. */\n depth: Math.max(...[0, 1, 2].map(i => Math.abs(off[i]!) / (half[i]! + SEAM))),\n fromCentre: Math.hypot(...off),\n through: lineMeetsBox(at, along, centre, half)\n }\n })\n .filter((c): c is NonNullable<typeof c> => !!c)\n const housing = joints.filter(c => c.depth <= HOUSED).sort((a, b) => a.fromCentre - b.fromCentre)[0]\n const carrying = joints.find(c => c.carrier === mine)\n const chosen = housing ?? (carrying && carrying.through ? carrying : undefined)\n if (!chosen) return null\n const axis = axisOf(chosen.motion)\n const control = controlOf(chosen.motion.args[3])\n if (!axis || !control) return null\n /* The reference edge points at what the joint turns: of the parts it carries, the one farthest off its axis. */\n const flatOff = (t: number[]) => {\n const rel = sub(t, chosen.origin.t)\n return Math.hypot(...sub(rel, axis.map(v => v * dot(rel, axis))))\n }\n const turned = m.nodes\n .filter(n => n.id.endsWith('.moved') && n.args.includes(`${chosen.carrier}.value`))\n .map(n => ({ id: n.id.replace(/\\.moved$/, ''), t: valueOf(node(n.id.replace(/\\.moved$/, '.pose')))?.t }))\n .filter((one): one is { id: string; t: number[] } => !!one.t)\n .sort((a, b) => flatOff(b.t) - flatOff(a.t))[0]\n return { carrier: chosen.carrier, pivot: chosen.origin.t, axis, toward: turned?.t ?? chosen.origin.t, control, turnedPart: turned?.id ?? part }\n}\n\n/**\n * The fan for the part, as a body to stand on the board, or null when the part does not turn. `values` are the\n * inputs the figure is drawn with now (a pose in hand included), so the fan follows the arm as it is posed.\n */\nexport function jointFanOf(asset: FigureAsset, part: string, values: Record<string, number> = {}): FigureRenderPart | null {\n if (!part) return null\n let graph: ReturnType<typeof compileFigureGraph>\n try {\n graph = compileFigureGraph(asset.document.model as never)\n } catch {\n return null\n }\n const inputs = { ...asset.designInputs, ...asset.stateDefaults, ...values }\n const at = (over: Record<string, number>) => graph.evaluate({ ...inputs, ...over }).values as Record<string, unknown>\n let now: Record<string, unknown>\n try {\n now = at({})\n } catch {\n return null\n }\n let box: WorldBox | undefined\n try {\n box = figureWorldBoxesOf(\n compileFigureAsset({ ...asset, designInputs: { ...asset.designInputs, ...pick(values, asset.designInputs) }, stateDefaults: { ...asset.stateDefaults, ...pick(values, asset.stateDefaults) } })\n .evaluate()\n .geometry.filter(g => g.id === part),\n figureWritersOf(asset)\n )[0]\n } catch {\n box = undefined\n }\n const turn = turnOf(asset, part, now, box)\n if (!turn) return null\n const carrierRef = Object.values(asset.document.model.nodes.find(n => n.id === turn.carrier)!.outputs)[0]!\n const carrierNow = now[carrierRef] as Pose | undefined\n if (!carrierNow) return null\n\n /* The reference edge points from the pivot toward the part, in the plane it turns in; any perpendicular if it sits on the axis. */\n const rel = sub(turn.toward, turn.pivot)\n const inPlane = sub(rel, turn.axis.map(v => v * dot(rel, turn.axis)))\n const d = norm(inPlane) ?? norm(cross(turn.axis, Math.abs(turn.axis[0]!) < 0.9 ? [1, 0, 0] : [0, 1, 0]))!\n /*\n Sized from what the joint turns, not from the selected part: selecting the elbow housing or the forearm\n shows the elbow's one fan, not a smaller one for the smaller part (user, 2026-09-24).\n */\n const radius = Math.max(60, Math.round(Math.max(largestMeasureOf(asset, turn.turnedPart, inputs), Math.hypot(...inPlane)) * 0.6))\n const edge = turn.pivot.map((v, i) => v + d[i]! * radius)\n\n const { min, max } = turn.control\n if (!(max > min)) return null\n const steps = Math.max(2, Math.ceil((max - min) / STEP_DEG))\n const pivot = apply(carrierNow, turn.pivot)\n const traced: number[][] = []\n try {\n for (let i = 0; i <= steps; i++) {\n const s = min + ((max - min) * i) / steps\n traced.push(apply(at({ [turn.control.id]: s })[carrierRef] as Pose, edge))\n }\n } catch {\n return null\n }\n\n /* The fan's own frame: x toward the first traced point, y along the axis as it stands now, z completing them. */\n const axisNow = norm([0, 1, 2].map(i => dot(carrierNow.r[i]!, turn.axis)))\n const first = norm(sub(traced[0]!, pivot))\n if (!axisNow || !first) return null\n const zAxis = cross(first, axisNow)\n const r = [0, 1, 2].map(i => [first[i]!, axisNow[i]!, zAxis[i]!])\n const local = (p: number[]) => {\n const v = sub(p, pivot)\n return { x: dot(v, first), z: dot(v, zAxis) }\n }\n const full = max - min >= 359.999\n const profile = [...(full ? [] : [{ x: 0, z: 0 }]), ...traced.map(local)].map(p => ({ x: Math.round(p.x * 1000) / 1000, z: Math.round(p.z * 1000) / 1000 }))\n\n return {\n id: JOINT_FAN,\n shape: sectionShape(profile, THICKNESS),\n pose: { t: pivot, r },\n material: { token: '__joint-fan', transparent: true },\n /** Which control turns it: the joint the fan shows. */\n turns: turn.control.id\n } as unknown as FigureRenderPart\n}\n\n/** The values in hand that belong to a record's keys (design inputs, or state defaults). */\nfunction pick(values: Record<string, number>, of: Record<string, number>): Record<string, number> {\n return Object.fromEntries(Object.entries(values).filter(([id]) => id in of))\n}\n\n/** A part's largest own measure (a box side, a diameter, a length) at the inputs given; 0 when it cannot be read. */\nfunction largestMeasureOf(asset: FigureAsset, part: string, inputs: Record<string, number>): number {\n try {\n const body = compileFigureAsset({ ...asset, designInputs: pick(inputs, asset.designInputs), stateDefaults: pick(inputs, asset.stateDefaults) })\n .evaluate()\n .geometry.find(g => g.id === part) as { shape?: { dimensions?: Record<string, number> } } | undefined\n const dims = Object.entries(body?.shape?.dimensions ?? {}).map(([k, v]) => (k.startsWith('radius') ? v * 2 : v))\n return dims.length ? Math.max(...dims) : 0\n } catch {\n return 0\n }\n}\n\n/** Whether the line through `at` along `along` passes through the box (centre, half-measures), within the skin. */\nfunction lineMeetsBox(at: number[], along: number[], centre: number[], half: number[]): boolean {\n let lo = -Infinity,\n hi = Infinity\n for (let i = 0; i < 3; i++) {\n const min = centre[i]! - half[i]! - SEAM,\n max = centre[i]! + half[i]! + SEAM\n if (Math.abs(along[i]!) < 1e-12) {\n if (at[i]! < min || at[i]! > max) return false\n continue\n }\n const t1 = (min - at[i]!) / along[i]!,\n t2 = (max - at[i]!) / along[i]!\n lo = Math.max(lo, Math.min(t1, t2))\n hi = Math.min(hi, Math.max(t1, t2))\n if (lo > hi) return false\n }\n return true\n}\n\n"]}
@@ -1,5 +1,6 @@
1
1
  import type { FigureFollow, FigureSizeRange } from '@hatiolab/figure-model';
2
2
  import type { FigureAsset, FigureBindingPlace, FigureFace, FigureJoint } from '@hatiolab/figure-model';
3
+ import { type JointAxisVector } from './joint-axis.js';
3
4
  export type Axis = 'x' | 'y' | 'z';
4
5
  export declare const AXES: readonly Axis[];
5
6
  /** What one dimension of a part is worked out from (figure-model's `FigureFollow`). */
@@ -25,6 +26,8 @@ export declare function heldBy(asset: FigureAsset, part: string): {
25
26
  } | {
26
27
  plane: true;
27
28
  } | null;
29
+ /** The word for where a joint end sits: its face's, or the part's origin for an end placed by it (ADR-0107 decision 4). */
30
+ export declare const endPlaceKeyOf: (end: object) => string;
28
31
  /** Whether a dimension of this part can follow something — box-shaped, whose three sizes are X, Y and Z. */
29
32
  export declare function canFollow(asset: FigureAsset, part: string): boolean;
30
33
  /**
@@ -94,7 +97,7 @@ export declare function rowAxesOf(asset: FigureAsset, part: string): Axis[];
94
97
  * Why a part cannot be made into a row here, in the terms `rowAxesOf` refuses it: said, not hidden (ADR-0097 §5.5).
95
98
  * Nothing when it can, or when it already is a row (that row is shown where it stands).
96
99
  */
97
- export declare function rowBlockOf(asset: FigureAsset, part: string): 'unfastened' | 'on-plane' | 'holds-parts' | undefined;
100
+ export declare function rowBlockOf(asset: FigureAsset, part: string): 'unfastened' | 'on-plane' | 'holds-parts' | 'at-origin' | undefined;
98
101
  /** The row a part is made into, as its fastening's record keeps it. */
99
102
  export declare const rowOf: (asset: FigureAsset, part: string) => {
100
103
  axis: import("@hatiolab/figure-model").FigureJointAxis;
@@ -160,6 +163,8 @@ export declare function fasteningOf(asset: FigureAsset, part: string): {
160
163
  twist: number;
161
164
  align: Partial<Record<Axis, Alignment>>;
162
165
  alignMm: Partial<Record<Axis, number>>;
166
+ /** Placed by a part's origin (ADR-0107 decision 4): no face to fasten by, and the form does not write it. */
167
+ atOrigin?: true;
163
168
  } | null;
164
169
  /**
165
170
  * The alignment that keeps the part where it stands when it is fastened: on each axis across the face, its distance
@@ -205,6 +210,8 @@ export declare function positionIsShare(asset: FigureAsset, part: string, axis:
205
210
  export interface Motion {
206
211
  kind: 'slide' | 'turn' | 'continuous';
207
212
  axis: Axis | '?';
213
+ /** The record's axis when it is a vector, as written (the graph holds it turned into the parent's frame). */
214
+ vector?: JointAxisVector;
208
215
  control?: {
209
216
  id: string;
210
217
  unit?: string;
@@ -212,10 +219,13 @@ export interface Motion {
212
219
  max?: number;
213
220
  };
214
221
  /** A joint that moves with another (URDF's mimic): the leader's value times `multiplier`, plus `offset`. */
222
+ /** What it follows, as a sum of leaders each with its multiplier, plus an offset (one leader is a sum of one). */
215
223
  follows?: {
216
- joint: string;
217
- multiplier: number;
218
- offset?: number;
224
+ sum: Array<{
225
+ joint: string;
226
+ multiplier: number;
227
+ }>;
228
+ offset: number;
219
229
  };
220
230
  /** A slide whose distance is a part's length (plus a difference): the control is its share. */
221
231
  travel?: {
@@ -229,6 +239,8 @@ export declare function motionOf(asset: FigureAsset, part: string): Motion | und
229
239
  export interface MotionForm {
230
240
  kind: 'slide' | 'turn' | 'continuous';
231
241
  axis: Axis;
242
+ /** An axis that is a vector (ADR-0107): said, not chosen here, and written back unchanged. */
243
+ axisVector?: JointAxisVector;
232
244
  id: string;
233
245
  min: number;
234
246
  max: number;
@@ -242,6 +254,18 @@ export interface MotionForm {
242
254
  */
243
255
  travel?: string;
244
256
  travelPlus?: number;
257
+ /**
258
+ * Kept as they are and not edited here: a follower of several leaders (`{ sum, offset }`, the offset still in
259
+ * `offset`) and a stroke that is a sum of lengths. The form says them and writes them back unchanged, so saving
260
+ * a range or an axis does not turn a follower into a joint of its own or drop its stroke.
261
+ */
262
+ followSum?: Array<{
263
+ joint: string;
264
+ multiplier: number;
265
+ }>;
266
+ travelSum?: Extract<NonNullable<FigureJoint['travel']>, {
267
+ sum: unknown;
268
+ }>;
245
269
  }
246
270
  /**
247
271
  * The motion form filled from a joint as it is, so a motion can be changed where it is -- its range, the length it
@@ -260,6 +284,12 @@ export declare function leadersFor(asset: FigureAsset, part: string, kind: Motio
260
284
  * control or limits of its own — its range is the leader's times the multiplier.
261
285
  */
262
286
  export declare function motionJoint(joint: FigureJoint, m: MotionForm, part: string): FigureJoint;
287
+ /**
288
+ * Why the motion form cannot be saved as it stands, as the kernel would refuse it: a follower's multiplier that is not
289
+ * a number or is 0, or a range whose most is not above its least. A range need not hold 0 -- the control starts at the
290
+ * nearest end (a lift that rests at 200 mm).
291
+ */
292
+ export declare function motionFormRefusalOf(m: MotionForm, follow: string): 'multiplier' | 'range' | undefined;
263
293
  /** The select's value for what a dimension follows now. */
264
294
  export declare function choiceOf(follow: Follow): string;
265
295
  /**
@@ -1,5 +1,6 @@
1
- import { FIGURE_FACES, figureShapeFaces, figurePartsExtentOf, compileFigureAsset, compileFigureGraph, figureFollowOf, proposeFigureProportion, defaultInstanceSizeRange, foldInstanceSizeRange, figureAttachmentsOf, figureBindingsOf, figureInstanceSizeLimitsOf, figureFollowableAxesOf, figureShapeProviderOf } from '@hatiolab/figure-model';
1
+ import { FIGURE_FACES, figureShapeFaces, figurePartsExtentOf, compileFigureAsset, compileFigureGraph, figureFollowOf, proposeFigureProportion, defaultInstanceSizeRange, foldInstanceSizeRange, figureAttachmentsOf, figureBindingsOf, figureInstanceSizeLimitsOf, figureFollowableAxesOf, figureShapeProviderOf, figureMimicTermsOf } from '@hatiolab/figure-model';
2
2
  import { startingShapeKindOf } from './starting-shape.js';
3
+ import { jointAxisToken, jointAxisVectorOf } from './joint-axis.js';
3
4
  export const AXES = ['x', 'y', 'z'];
4
5
  const modelOf = (asset) => asset.document.model;
5
6
  /**
@@ -29,6 +30,8 @@ export function heldBy(asset, part) {
29
30
  const to = fastenedTo(asset, part);
30
31
  return to ? { part: to } : null;
31
32
  }
33
+ /** The word for where a joint end sits: its face's, or the part's origin for an end placed by it (ADR-0107 decision 4). */
34
+ export const endPlaceKeyOf = (end) => ('face' in end ? `figure.label.face-${end.face}` : 'figure.label.joint-origin');
32
35
  /** Whether a dimension of this part can follow something — box-shaped, whose three sizes are X, Y and Z. */
33
36
  export function canFollow(asset, part) {
34
37
  return followableAxes(asset, part).length > 0;
@@ -206,7 +209,11 @@ export function rowAxesOf(asset, part) {
206
209
  return [];
207
210
  if (joints.some(o => 'part' in o.body0 && o.body0.part === part))
208
211
  return [];
209
- return AXES.filter(axis => axis !== FIGURE_FACES[j.body1.face]);
212
+ /* Placed by an origin: no face to lay the row across, and the kernel refuses it. */
213
+ const by = j.body1;
214
+ if (!('face' in j.body0) || !('face' in by))
215
+ return [];
216
+ return AXES.filter(axis => axis !== FIGURE_FACES[by.face]);
210
217
  }
211
218
  /**
212
219
  * Why a part cannot be made into a row here, in the terms `rowAxesOf` refuses it: said, not hidden (ADR-0097 §5.5).
@@ -223,6 +230,8 @@ export function rowBlockOf(asset, part) {
223
230
  return 'on-plane';
224
231
  if (joints.some(o => 'part' in o.body0 && o.body0.part === part))
225
232
  return 'holds-parts';
233
+ if (!('face' in j.body0) || !('face' in j.body1))
234
+ return 'at-origin';
226
235
  return undefined;
227
236
  }
228
237
  /** The row a part is made into, as its fastening's record keeps it. */
@@ -293,7 +302,8 @@ export function fasteningOf(asset, part) {
293
302
  return null;
294
303
  const toPlane = 'plane' in record.body0;
295
304
  const target = 'part' in record.body0 ? record.body0.part : '';
296
- const targetFace = 'part' in record.body0 ? record.body0.face : 'top';
305
+ const targetFace = 'face' in record.body0 ? record.body0.face : 'top';
306
+ const atOrigin = !('plane' in record.body0) && (!('face' in record.body0) || !('face' in record.body1));
297
307
  const twist = record.origin?.twist ?? 0;
298
308
  /* Where across the face: as the record says it, and where it says nothing, where the part stands now. */
299
309
  const kept = placeKeptOf(asset, part, toPlane ? 'plane' : { part: target, face: targetFace }, !!twist);
@@ -315,14 +325,15 @@ export function fasteningOf(asset, part) {
315
325
  }
316
326
  const gap = record.origin?.gap;
317
327
  return {
318
- face: record.body1.face,
328
+ face: 'face' in record.body1 ? record.body1.face : 'centre',
319
329
  toPlane,
320
330
  target,
321
331
  targetFace,
322
332
  facing: record.origin?.facing ?? 'meet',
323
333
  gap: typeof gap === 'number' ? gap : 0,
324
334
  twist,
325
- ...kept
335
+ ...kept,
336
+ ...(atOrigin ? { atOrigin: true } : {})
326
337
  };
327
338
  }
328
339
  /**
@@ -406,7 +417,9 @@ export function motionOf(asset, part) {
406
417
  if (!node)
407
418
  return undefined;
408
419
  const record = asset.joints?.find(j => j.body1.part === part);
409
- const endless = record?.type === 'revolute' && record.lowerLimit === undefined && record.upperLimit === undefined;
420
+ /* A follower has no limits of its own: its range is its leaders', not a turn without end. */
421
+ const endless = record?.type === 'revolute' && record.lowerLimit === undefined && record.upperLimit === undefined && !record.mimic;
422
+ const vector = jointAxisVectorOf(record?.axis);
410
423
  const kind = endless ? 'continuous' : node.op === 'axis-turn@1' ? 'turn' : 'slide';
411
424
  const unit = node.args.slice(0, 3).map(ref => m.constants.find(c => c.id === ref)?.value);
412
425
  const axis = AXES[unit.findIndex(v => v !== undefined && v !== 0)] ?? '?';
@@ -428,8 +441,9 @@ export function motionOf(asset, part) {
428
441
  }
429
442
  return undefined;
430
443
  };
431
- const travel = record?.travel?.source && 'part' in record.travel.source ? { part: record.travel.source.part, axis: record.travel.source.axis, ...(record.travel.plus ? { plus: record.travel.plus } : {}) } : undefined;
432
- return { kind, axis, control: control(quantity), ...(record?.mimic ? { follows: record.mimic } : {}), ...(travel ? { travel } : {}) };
444
+ const one = record?.travel && 'source' in record.travel ? record.travel : undefined;
445
+ const travel = one?.source && 'part' in one.source ? { part: one.source.part, axis: one.source.axis, ...(one.plus ? { plus: one.plus } : {}) } : undefined;
446
+ return { kind, axis, ...(vector ? { vector } : {}), control: control(quantity), ...(record?.mimic ? { follows: figureMimicTermsOf(record.mimic) } : {}), ...(travel ? { travel } : {}) };
433
447
  }
434
448
  /**
435
449
  * The motion form filled from a joint as it is, so a motion can be changed where it is -- its range, the length it
@@ -438,18 +452,27 @@ export function motionOf(asset, part) {
438
452
  */
439
453
  export function motionFormOf(joint) {
440
454
  const kind = joint.type === 'prismatic' ? 'slide' : joint.lowerLimit === undefined && joint.upperLimit === undefined && !joint.mimic ? 'continuous' : 'turn';
441
- const travel = joint.travel?.source && 'part' in joint.travel.source ? `${joint.travel.source.part}:${joint.travel.source.axis}` : '';
455
+ const one = joint.travel && 'source' in joint.travel ? joint.travel : undefined;
456
+ const travel = one?.source && 'part' in one.source ? `${one.source.part}:${one.source.axis}` : '';
457
+ const travelSum = joint.travel && 'sum' in joint.travel ? joint.travel : undefined;
458
+ const terms = figureMimicTermsOf(joint.mimic);
459
+ const single = terms?.sum.length === 1 ? terms.sum[0] : undefined;
460
+ const axisVector = jointAxisVectorOf(joint.axis);
442
461
  return {
443
462
  kind,
444
- axis: (joint.axis ?? 'X').toLowerCase(),
445
- id: joint.state?.id ?? '',
446
- min: travel ? 0 : (joint.lowerLimit ?? (kind === 'slide' ? 0 : -90)),
447
- max: travel ? 500 : (joint.upperLimit ?? (kind === 'slide' ? 500 : 90)),
448
- follow: joint.mimic?.joint ?? '',
449
- multiplier: joint.mimic?.multiplier ?? 1,
450
- offset: joint.mimic?.offset ?? 0,
463
+ axis: jointAxisToken(joint.axis) ?? 'x',
464
+ ...(axisVector ? { axisVector } : {}),
465
+ /* A moving joint with no state record is driven by the input named after it (figure-model `stateIdOf`): keep that name, so an edit does not rename the control a board or a controller writes to. */
466
+ id: joint.state?.id ?? (joint.type !== 'fixed' && !joint.mimic ? joint.id : ''),
467
+ min: travel || travelSum ? 0 : (joint.lowerLimit ?? (kind === 'slide' ? 0 : -90)),
468
+ max: travel || travelSum ? 500 : (joint.upperLimit ?? (kind === 'slide' ? 500 : 90)),
469
+ follow: single?.joint ?? '',
470
+ multiplier: single?.multiplier ?? 1,
471
+ offset: terms?.offset ?? 0,
451
472
  travel,
452
- travelPlus: joint.travel?.plus ?? 0
473
+ travelPlus: one?.plus ?? 0,
474
+ ...(terms && !single ? { followSum: terms.sum } : {}),
475
+ ...(travelSum ? { travelSum } : {})
453
476
  };
454
477
  }
455
478
  /**
@@ -473,16 +496,34 @@ export function motionJoint(joint, m, part) {
473
496
  /* A row keeps being a row when it is given a motion: dropped here, the row went with it (designer 2026-09-27). */
474
497
  ...(joint.repeat ? { repeat: joint.repeat } : {}),
475
498
  type: m.kind === 'slide' ? 'prismatic' : 'revolute',
476
- axis: m.axis.toUpperCase()
499
+ axis: (m.axisVector ?? m.axis.toUpperCase())
477
500
  };
478
501
  if (m.follow)
479
502
  return { ...moving, mimic: { joint: m.follow, multiplier: m.multiplier, ...(m.offset ? { offset: m.offset } : {}) } };
503
+ if (m.followSum)
504
+ return { ...moving, mimic: { sum: m.followSum, ...(m.offset ? { offset: m.offset } : {}) } };
505
+ if (m.kind === 'slide' && m.travelSum && !m.travel)
506
+ return { ...moving, travel: m.travelSum, state: { id: m.id || `${part}-${m.kind}` } };
480
507
  if (m.kind === 'slide' && m.travel) {
481
508
  const [of, axis] = m.travel.split(':');
482
509
  return { ...moving, travel: { source: { of: 'part', part: of, axis: axis }, ...(m.travelPlus ? { plus: m.travelPlus } : {}) }, state: { id: m.id || `${part}-${m.kind}` } };
483
510
  }
484
511
  return { ...moving, ...(m.kind === 'continuous' ? {} : { lowerLimit: m.min, upperLimit: m.max }), state: { id: m.id || `${part}-${m.kind}` } };
485
512
  }
513
+ /**
514
+ * Why the motion form cannot be saved as it stands, as the kernel would refuse it: a follower's multiplier that is not
515
+ * a number or is 0, or a range whose most is not above its least. A range need not hold 0 -- the control starts at the
516
+ * nearest end (a lift that rests at 200 mm).
517
+ */
518
+ export function motionFormRefusalOf(m, follow) {
519
+ if (m.followSum)
520
+ return undefined;
521
+ if (follow)
522
+ return Number.isFinite(m.multiplier) && m.multiplier !== 0 ? undefined : 'multiplier';
523
+ if (m.kind === 'continuous' || (m.kind === 'slide' && (m.travel || m.travelSum)))
524
+ return undefined;
525
+ return Number.isFinite(m.min) && Number.isFinite(m.max) && m.max > m.min ? undefined : 'range';
526
+ }
486
527
  /** The select's value for what a dimension follows now. */
487
528
  export function choiceOf(follow) {
488
529
  switch (follow.of) {