@things-factory/figure-ui 10.1.198 → 10.1.201

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/client/modeller/editor.ts +7 -3
  2. package/client/modeller/fasten-form.ts +18 -35
  3. package/client/modeller/fasten-geometry.ts +67 -115
  4. package/client/modeller/figure-fasten-dialog.ts +4 -12
  5. package/client/modeller/figure-links.ts +6 -7
  6. package/client/modeller/joint-drag.ts +9 -11
  7. package/client/modeller/links.ts +35 -99
  8. package/client/modeller/refusals.ts +2 -6
  9. package/dist-client/modeller/editor.js +7 -2
  10. package/dist-client/modeller/editor.js.map +1 -1
  11. package/dist-client/modeller/fasten-form.d.ts +5 -8
  12. package/dist-client/modeller/fasten-form.js +16 -28
  13. package/dist-client/modeller/fasten-form.js.map +1 -1
  14. package/dist-client/modeller/fasten-geometry.d.ts +20 -19
  15. package/dist-client/modeller/fasten-geometry.js +54 -113
  16. package/dist-client/modeller/fasten-geometry.js.map +1 -1
  17. package/dist-client/modeller/figure-fasten-dialog.js +4 -12
  18. package/dist-client/modeller/figure-fasten-dialog.js.map +1 -1
  19. package/dist-client/modeller/figure-links.d.ts +1 -1
  20. package/dist-client/modeller/figure-links.js +6 -7
  21. package/dist-client/modeller/figure-links.js.map +1 -1
  22. package/dist-client/modeller/joint-drag.js +9 -11
  23. package/dist-client/modeller/joint-drag.js.map +1 -1
  24. package/dist-client/modeller/links.d.ts +19 -26
  25. package/dist-client/modeller/links.js +36 -94
  26. package/dist-client/modeller/links.js.map +1 -1
  27. package/dist-client/modeller/refusals.d.ts +1 -1
  28. package/dist-client/modeller/refusals.js +2 -6
  29. package/dist-client/modeller/refusals.js.map +1 -1
  30. package/dist-client/tsconfig.tsbuildinfo +1 -1
  31. package/dist-server/tsconfig.tsbuildinfo +1 -1
  32. package/package.json +6 -6
  33. package/test/ai-proposal.test.ts +1 -1
  34. package/test/browser/acceptance-page.js +11 -11
  35. package/test/browser/detail-page.js +1 -1
  36. package/test/browser/detail-tabs-page.js +1 -1
  37. package/test/browser/editing-page.js +1 -1
  38. package/test/browser/fix-alignment.check.mjs +3 -3
  39. package/test/browser/links.check.mjs +20 -8
  40. package/test/browser/modeller-page.js +3 -3
  41. package/test/browser/part-ids.check.mjs +2 -2
  42. package/test/browser/parts-page.js +2 -2
  43. package/test/browser/product-page.check.mjs +3 -3
  44. package/test/browser/product-page.js +1 -1
  45. package/test/browser/row-preview.check.mjs +2 -2
  46. package/test/browser/screen-page.js +3 -3
  47. package/test/browser/section-page.js +3 -3
  48. package/test/browser/section.check.mjs +4 -4
  49. package/test/clip-ai-proposal.test.ts +1 -1
  50. package/test/clip-consent.test.ts +1 -1
  51. package/test/command-preview.test.ts +8 -8
  52. package/test/face-mapping.test.ts +2 -2
  53. package/test/fasten-drawing.test.ts +14 -2
  54. package/test/fasten-form.test.ts +11 -3
  55. package/test/fasten-geometry.test.ts +22 -31
  56. package/test/fix-alignment.test.ts +16 -4
  57. package/test/gizmo.test.ts +2 -2
  58. package/test/joint-drag.test.ts +13 -2
  59. package/test/joint-fan.test.ts +1 -1
  60. package/test/joint-frames.test.ts +74 -45
  61. package/test/joints.ts +1 -1
  62. package/test/links.test.ts +68 -68
  63. package/test/live.test.ts +2 -2
  64. package/test/preview-size.test.ts +1 -1
  65. package/test/refusals.test.ts +18 -20
  66. package/test/resize-with-placement.test.ts +6 -6
  67. package/test/revision.test.ts +4 -5
  68. package/test/section-panel.test.ts +1 -1
  69. package/translations/en.json +9 -11
  70. package/translations/es.json +9 -11
  71. package/translations/ja.json +9 -11
  72. package/translations/ko.json +9 -11
  73. package/translations/ms.json +9 -11
  74. package/translations/zh.json +9 -11
@@ -1,5 +1,6 @@
1
+ import { figureJointAsFixed } from '@hatiolab/figure-model';
1
2
  import type { FigureFollow, FigureSizeRange } from '@hatiolab/figure-model';
2
- import type { FigureAsset, FigureAuthoringAction, FigureBindingPlace, FigureFace, FigureJoint } from '@hatiolab/figure-model';
3
+ import type { FigureAsset, FigureAuthoringAction, FigureBindingPlace, FigureFace, FigureFacing, FigureJoint } from '@hatiolab/figure-model';
3
4
  import { type FigureRevision } from './revision.js';
4
5
  import { type JointAxisVector } from './joint-axis.js';
5
6
  export type Axis = 'x' | 'y' | 'z';
@@ -27,7 +28,7 @@ export declare function heldBy(asset: FigureAsset, part: string): {
27
28
  } | {
28
29
  plane: true;
29
30
  } | null;
30
- /** 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). */
31
+ /** The word for where a joint's body0 end sits: its face's, or the part's origin for an end placed by it (ADR-0107 decision 4). */
31
32
  export declare const endPlaceKeyOf: (end: object) => string;
32
33
  /** Whether a dimension of this part can follow something — box-shaped, whose three sizes are X, Y and Z. */
33
34
  export declare function canFollow(asset: FigureAsset, part: string): boolean;
@@ -126,35 +127,30 @@ export type Alignment = 'centre' | 'min' | 'max' | 'mm' | 'share' | 'input';
126
127
  */
127
128
  /**
128
129
  * The mounting plane of this figure, by its placement (ADR-0093): the floor or the ceiling is Y = 0 and a part stands
129
- * on it by its bottom or hangs from it by its top; a wall is Z = 0 and a part is fastened to it by its back.
130
+ * on it or hangs below it; a wall is Z = 0 and a part stands out of it (ADR-0140 3.11).
130
131
  */
131
132
  export declare function mountingPlaneOf(asset: FigureAsset): {
132
133
  normal: Axis;
133
- faces: FigureFace[];
134
+ facings: FigureFacing[];
134
135
  };
135
- export declare function alignAxesOf(asset: FigureAsset, part: string, target: {
136
+ export declare function alignAxesOf(asset: FigureAsset, target: {
136
137
  part: string;
137
138
  face: string;
138
- } | 'plane', twisted?: boolean): {
139
+ } | 'plane'): {
139
140
  axis: Axis;
140
141
  edges: boolean;
141
142
  }[];
142
- /**
143
- * The faces of its own a part may be fastened by: its outer faces -- on the mounting plane only the ones the plane takes
144
- * (a bottom or a top on the floor, a back on a wall) -- and, to another part, its centre, which a roller spins about.
145
- */
146
- export declare function ownFacesOf(asset: FigureAsset, part: string, toPlane: boolean): Array<FigureFace | 'centre'>;
147
143
  /**
148
144
  * The axes across the face the fastening form asks about in rows: only where the face drawing (`view`, its axes) cannot
149
145
  * set the place -- no drawing at all, an axis it does not draw, a place that follows a position field, or one the
150
146
  * figure's size may scale.
151
147
  */
152
- export declare function alignRowsOf(asset: FigureAsset, part: string, aim: {
148
+ export declare function alignRowsOf(asset: FigureAsset, aim: {
153
149
  part: string;
154
150
  face: string;
155
151
  } | 'plane', view: {
156
152
  axes: readonly Axis[];
157
- } | undefined, twisted: boolean, alignInput?: Partial<Record<Axis, string>>): {
153
+ } | undefined, alignInput?: Partial<Record<Axis, string>>): {
158
154
  axis: Axis;
159
155
  edges: boolean;
160
156
  }[];
@@ -174,21 +170,19 @@ export declare function shownWhenOf(asset: FigureAsset, part: string): {
174
170
  /** Whether `part` hangs from `from`, through any number of fastenings. */
175
171
  export declare function hangsFrom(asset: FigureAsset, part: string, from: string): boolean;
176
172
  /**
177
- * The fastening a part has, as the fastening form holds it -- so the form opens on what is there (the face, including
178
- * \`centre\`, the target and its face, the gap, the twist and where across the face) and a change is a change to that.
173
+ * The fastening a part has, as the fastening form holds it -- so the form opens on what is there (the target and its
174
+ * face, how it meets it, the gap and where across the face) and a change is a change to that.
179
175
  * Opening it on the defaults showed a roller fastened by its centre as fastened by its bottom, and fastening again
180
176
  * from there broke its turning in place (walk 1, 2026-09-26). Null for a part fastened to nothing.
181
177
  */
182
178
  export declare function fasteningOf(revision: FigureRevision, part: string): {
183
- face: string;
184
179
  toPlane: boolean;
185
180
  target: string;
186
181
  targetFace: string;
187
- facing: 'meet' | 'flush';
182
+ facing: FigureFacing;
188
183
  gap: number;
189
184
  /** The record's gap when it is worked out (a length that follows something): kept as it is until a number is typed. */
190
185
  gapKept?: unknown;
191
- twist: number;
192
186
  align: Partial<Record<Axis, Alignment>>;
193
187
  alignMm: Partial<Record<Axis, number>>;
194
188
  /** Placed by a part's origin (ADR-0107 decision 4): no face to fasten by, and the form does not write it. */
@@ -206,23 +200,22 @@ export declare function fastenGapOf(form: {
206
200
  gap?: unknown;
207
201
  };
208
202
  /**
209
- * The alignment that keeps the part where it stands when it is fastened: on each axis across the face, its distance
210
- * from the target's centre (the plane's is the origin). Fastening used to centre it -- a door set 100 mm in front of
211
- * its frame slid into it (2026-09-26). Left centred where it already is, and where the target stands turned.
203
+ * The alignment that keeps the part where it stands when it is fastened, as the form holds it: the kernel's place
204
+ * (`figureFastenKeptPlaceOf`, ADR-0140 3.7), a distance on each axis it is off the target's centre.
212
205
  */
213
206
  export declare function placeKeptOf(revision: FigureRevision, part: string, target: {
214
207
  part: string;
215
208
  face: string;
216
- } | 'plane', twisted?: boolean): {
209
+ } | 'plane'): {
217
210
  align: Partial<Record<Axis, Alignment>>;
218
211
  alignMm: Partial<Record<Axis, number>>;
219
212
  alignInput: Partial<Record<Axis, string>>;
220
213
  };
221
214
  /** The `align` of an attach command from the form's choices: only the axes across that face, and centred ones left out. */
222
- export declare function alignOf(asset: FigureAsset, part: string, target: {
215
+ export declare function alignOf(asset: FigureAsset, target: {
223
216
  part: string;
224
217
  face: string;
225
- } | 'plane', how: Partial<Record<Axis, Alignment>>, mm: Partial<Record<Axis, number>>, twisted?: boolean, inputs?: Partial<Record<Axis, string>>): Partial<Record<Axis, "min" | "max" | {
218
+ } | 'plane', how: Partial<Record<Axis, Alignment>>, mm: Partial<Record<Axis, number>>, inputs?: Partial<Record<Axis, string>>): Partial<Record<Axis, "min" | "max" | {
226
219
  mm: number | {
227
220
  from: {
228
221
  of: "instance";
@@ -395,5 +388,5 @@ export declare function rangeDiffers(a: FigureSizeRange, b: FigureSizeRange): bo
395
388
  * architect's ruling 2026-09-24); the modeller proposes it, the author declares it.
396
389
  */
397
390
  export declare function turnsAboutAJoint(asset: FigureAsset): boolean;
398
- /** The joint made still: its fastening, its place and its row kept, its motion gone. */
399
- export declare function fixedJointOf(joint: FigureJoint): FigureJoint;
391
+ /** The joint made still: its fastening, its place and its row kept, its motion gone (the kernel's `figureJointAsFixed`, ADR-0140 D6). */
392
+ export declare const fixedJointOf: typeof figureJointAsFixed;
@@ -1,4 +1,4 @@
1
- import { FIGURE_FACES, figureShapeFaces, figurePartsExtentOf, figureFollowOf, proposeFigureProportion, defaultInstanceSizeRange, foldInstanceSizeRange, figureAttachmentsOf, figureBindingsOf, figureInstanceSizeLimitsOf, figureFollowableAxesOf, figureShapeProviderOf, figureMimicTermsOf } from '@hatiolab/figure-model';
1
+ import { figureShapeFaces, figurePartsExtentOf, figureFollowOf, proposeFigureProportion, defaultInstanceSizeRange, foldInstanceSizeRange, figureAttachmentsOf, figureBindingsOf, figureInstanceSizeLimitsOf, figureFollowableAxesOf, figureShapeProviderOf, figureMimicTermsOf, figureFastenKeptPlaceOf, figureFastenAxesOf, figureJointAsFixed, figureJointWithMotion } from '@hatiolab/figure-model';
2
2
  import { startingShapeKindOf } from './starting-shape.js';
3
3
  import { boxOf } from './align.js';
4
4
  import { graphValuesOf } from './revision.js';
@@ -32,7 +32,7 @@ export function heldBy(asset, part) {
32
32
  const to = fastenedTo(asset, part);
33
33
  return to ? { part: to } : null;
34
34
  }
35
- /** 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). */
35
+ /** The word for where a joint's body0 end sits: its face's, or the part's origin for an end placed by it (ADR-0107 decision 4). */
36
36
  export const endPlaceKeyOf = (end) => ('face' in end ? `figure.label.face-${end.face}` : 'figure.label.joint-origin');
37
37
  /** Whether a dimension of this part can follow something — box-shaped, whose three sizes are X, Y and Z. */
38
38
  export function canFollow(asset, part) {
@@ -234,10 +234,9 @@ export function rowAxesOf(asset, part) {
234
234
  if (joints.some(o => 'part' in o.body0 && o.body0.part === part))
235
235
  return [];
236
236
  /* Placed by an origin: no face to lay the row across, and the kernel refuses it. */
237
- const by = j.body1;
238
- if (!('face' in j.body0) || !('face' in by))
237
+ if (!('face' in j.body0) || 'at' in j.body1)
239
238
  return [];
240
- return AXES.filter(axis => axis !== FIGURE_FACES[by.face]);
239
+ return figureFastenAxesOf(asset, { part: j.body0.part, face: j.body0.face });
241
240
  }
242
241
  /**
243
242
  * Why a part cannot be made into a row here, in the terms `rowAxesOf` refuses it: said, not hidden (ADR-0097 §5.5).
@@ -254,7 +253,7 @@ export function rowBlockOf(asset, part) {
254
253
  return 'on-plane';
255
254
  if (joints.some(o => 'part' in o.body0 && o.body0.part === part))
256
255
  return 'holds-parts';
257
- if (!('face' in j.body0) || !('face' in j.body1))
256
+ if (!('face' in j.body0) || 'at' in j.body1)
258
257
  return 'at-origin';
259
258
  return undefined;
260
259
  }
@@ -267,43 +266,26 @@ export const rowOf = (asset, part) => asset.joints?.find(o => o.body1.part === p
267
266
  */
268
267
  /**
269
268
  * The mounting plane of this figure, by its placement (ADR-0093): the floor or the ceiling is Y = 0 and a part stands
270
- * on it by its bottom or hangs from it by its top; a wall is Z = 0 and a part is fastened to it by its back.
269
+ * on it or hangs below it; a wall is Z = 0 and a part stands out of it (ADR-0140 3.11).
271
270
  */
272
271
  export function mountingPlaneOf(asset) {
273
- return asset.placement === 'wall' ? { normal: 'z', faces: ['back'] } : { normal: 'y', faces: ['bottom', 'top'] };
272
+ return asset.placement === 'wall' ? { normal: 'z', facings: ['meet'] } : { normal: 'y', facings: ['meet', 'flush'] };
274
273
  }
275
- export function alignAxesOf(asset, part, target, twisted = false) {
274
+ export function alignAxesOf(asset, target) {
276
275
  /* The plane has no edges, but a figure that declares its size has sides on it (figure-model 0.1.64). */
277
- if (target === 'plane') {
278
- const { normal } = mountingPlaneOf(asset);
279
- return AXES.filter(axis => axis !== normal).map(axis => ({ axis, edges: !twisted && hasInstanceSize(asset, axis) }));
280
- }
281
- const along = faceAxisOf(asset, target.part, target.face);
282
- if (!along)
283
- return [];
284
- const theirs = new Set(facesOf(asset, target.part)), mine = new Set(facesOf(asset, part));
285
- /* A part twisted about the face's normal has no side level with the target's edges. */
286
- return AXES.filter(axis => axis !== along).map(axis => ({ axis, edges: !twisted && OUTER[axis].every(f => theirs.has(f) && mine.has(f)) }));
287
- }
288
- /**
289
- * The faces of its own a part may be fastened by: its outer faces -- on the mounting plane only the ones the plane takes
290
- * (a bottom or a top on the floor, a back on a wall) -- and, to another part, its centre, which a roller spins about.
291
- */
292
- export function ownFacesOf(asset, part, toPlane) {
293
- const outer = Object.keys(FIGURE_FACES);
294
- const plane = mountingPlaneOf(asset);
295
- return [
296
- ...facesOf(asset, part).filter(face => outer.includes(face) && (!toPlane || plane.faces.includes(face))),
297
- ...(toPlane ? [] : ['centre'])
298
- ];
276
+ if (target === 'plane')
277
+ return figureFastenAxesOf(asset, 'plane').map(axis => ({ axis, edges: hasInstanceSize(asset, axis) }));
278
+ /* The part is lined up by its real extent in the pose it has (ADR-0140 3.4): the target's two sides are what it needs. */
279
+ const theirs = new Set(facesOf(asset, target.part));
280
+ return figureFastenAxesOf(asset, target).map(axis => ({ axis, edges: OUTER[axis].every(f => theirs.has(f)) }));
299
281
  }
300
282
  /**
301
283
  * The axes across the face the fastening form asks about in rows: only where the face drawing (`view`, its axes) cannot
302
284
  * set the place -- no drawing at all, an axis it does not draw, a place that follows a position field, or one the
303
285
  * figure's size may scale.
304
286
  */
305
- export function alignRowsOf(asset, part, aim, view, twisted, alignInput = {}) {
306
- return alignAxesOf(asset, part, aim, twisted).filter(({ axis }) => !view || !view.axes.includes(axis) || !!alignInput[axis] || hasInstanceSize(asset, axis));
287
+ export function alignRowsOf(asset, aim, view, alignInput = {}) {
288
+ return alignAxesOf(asset, aim).filter(({ axis }) => !view || !view.axes.includes(axis) || !!alignInput[axis] || hasInstanceSize(asset, axis));
307
289
  }
308
290
  /** The controls a part's showing can follow: the figure's state inputs, with their range and label. */
309
291
  export function visibilityControlsOf(asset) {
@@ -335,8 +317,8 @@ export function hangsFrom(asset, part, from) {
335
317
  return false;
336
318
  }
337
319
  /**
338
- * The fastening a part has, as the fastening form holds it -- so the form opens on what is there (the face, including
339
- * \`centre\`, the target and its face, the gap, the twist and where across the face) and a change is a change to that.
320
+ * The fastening a part has, as the fastening form holds it -- so the form opens on what is there (the target and its
321
+ * face, how it meets it, the gap and where across the face) and a change is a change to that.
340
322
  * Opening it on the defaults showed a roller fastened by its centre as fastened by its bottom, and fastening again
341
323
  * from there broke its turning in place (walk 1, 2026-09-26). Null for a part fastened to nothing.
342
324
  */
@@ -347,10 +329,9 @@ export function fasteningOf(revision, part) {
347
329
  const toPlane = 'plane' in record.body0;
348
330
  const target = 'part' in record.body0 ? record.body0.part : '';
349
331
  const targetFace = 'face' in record.body0 ? record.body0.face : 'top';
350
- const atOrigin = !('plane' in record.body0) && (!('face' in record.body0) || !('face' in record.body1));
351
- const twist = record.origin?.twist ?? 0;
332
+ const atOrigin = !('plane' in record.body0) && (!('face' in record.body0) || 'at' in record.body1);
352
333
  /* Where across the face: as the record says it, and where it says nothing, where the part stands now. */
353
- const kept = placeKeptOf(revision, part, toPlane ? 'plane' : { part: target, face: targetFace }, !!twist);
334
+ const kept = placeKeptOf(revision, part, toPlane ? 'plane' : { part: target, face: targetFace });
354
335
  for (const [axis, how] of Object.entries(record.origin?.align ?? {})) {
355
336
  if (how === 'min' || how === 'max' || how === 'centre') {
356
337
  kept.align[axis] = how;
@@ -369,14 +350,12 @@ export function fasteningOf(revision, part) {
369
350
  }
370
351
  const gap = record.origin?.gap;
371
352
  return {
372
- face: 'face' in record.body1 ? record.body1.face : 'centre',
373
353
  toPlane,
374
354
  target,
375
355
  targetFace,
376
356
  facing: record.origin?.facing ?? 'meet',
377
357
  gap: typeof gap === 'number' ? gap : 0,
378
358
  ...(gap !== undefined && typeof gap !== 'number' ? { gapKept: gap } : {}),
379
- twist,
380
359
  ...kept,
381
360
  ...(atOrigin ? { atOrigin: true } : {})
382
361
  };
@@ -392,41 +371,17 @@ export function fastenGapOf(form) {
392
371
  return form.gap ? { gap: form.gap } : {};
393
372
  }
394
373
  /**
395
- * The alignment that keeps the part where it stands when it is fastened: on each axis across the face, its distance
396
- * from the target's centre (the plane's is the origin). Fastening used to centre it -- a door set 100 mm in front of
397
- * its frame slid into it (2026-09-26). Left centred where it already is, and where the target stands turned.
398
- */
399
- export function placeKeptOf(revision, part, target, twisted = false) {
400
- const kept = { align: {}, alignMm: {}, alignInput: {} };
401
- const asset = revision.asset;
402
- /* Where it stands as drawn: the pose the command that fastens it sees. */
403
- const geometry = revision.geometry;
404
- const poseOf = (id) => geometry.find(g => g.id === id)?.pose;
405
- const mine = poseOf(part);
406
- if (!mine)
407
- return kept;
408
- let centre = [0, 0, 0];
409
- if (target !== 'plane') {
410
- const theirs = poseOf(target.part);
411
- const upright = theirs?.r.every((row, i) => row.every((v, j) => Math.abs(v - (i === j ? 1 : 0)) < 1e-9));
412
- if (!theirs || !upright)
413
- return kept;
414
- centre = theirs.t;
415
- }
416
- for (const { axis } of alignAxesOf(asset, part, target, twisted)) {
417
- const k = AXES.indexOf(axis);
418
- const off = Math.round((mine.t[k] - centre[k]) * 10) / 10;
419
- if (Math.abs(off) < 0.5)
420
- continue;
421
- kept.align[axis] = 'mm';
422
- kept.alignMm[axis] = off;
423
- }
424
- return kept;
374
+ * The alignment that keeps the part where it stands when it is fastened, as the form holds it: the kernel's place
375
+ * (`figureFastenKeptPlaceOf`, ADR-0140 3.7), a distance on each axis it is off the target's centre.
376
+ */
377
+ export function placeKeptOf(revision, part, target) {
378
+ const alignMm = figureFastenKeptPlaceOf(revision.asset, part, target, { ...revision.inputs });
379
+ return { align: Object.fromEntries(Object.keys(alignMm).map(axis => [axis, 'mm'])), alignMm, alignInput: {} };
425
380
  }
426
381
  /** The `align` of an attach command from the form's choices: only the axes across that face, and centred ones left out. */
427
- export function alignOf(asset, part, target, how, mm, twisted = false, inputs = {}) {
382
+ export function alignOf(asset, target, how, mm, inputs = {}) {
428
383
  const align = {};
429
- for (const { axis, edges } of alignAxesOf(asset, part, target, twisted)) {
384
+ for (const { axis, edges } of alignAxesOf(asset, target)) {
430
385
  const one = how[axis] ?? 'centre';
431
386
  const size = asset.designInputs[`size.${axis}`];
432
387
  if (one === 'share' && mm[axis] && hasInstanceSize(asset, axis) && size)
@@ -539,27 +494,23 @@ export function leadersFor(asset, part, kind) {
539
494
  * control or limits of its own — its range is the leader's times the multiplier.
540
495
  */
541
496
  export function motionJoint(joint, m, part) {
497
+ /* Where it is fastened -- its row and its frame's own words too -- is the kernel's to keep (`figureJointWithMotion`, ADR-0140 D6). */
542
498
  const moving = {
543
- id: joint.id,
544
- body0: joint.body0,
545
- body1: joint.body1,
546
- ...(joint.origin ? { origin: joint.origin } : {}),
547
- /* A row keeps being a row when it is given a motion: dropped here, the row went with it (designer 2026-09-27). */
548
- ...(joint.repeat ? { repeat: joint.repeat } : {}),
549
499
  type: m.kind === 'slide' ? 'prismatic' : 'revolute',
550
500
  axis: (m.axisVector ?? m.axis.toUpperCase())
551
501
  };
502
+ const given = (motion) => figureJointWithMotion(joint, { ...moving, ...motion });
552
503
  if (m.follow)
553
- return { ...moving, mimic: { joint: m.follow, multiplier: m.multiplier, ...(m.offset ? { offset: m.offset } : {}) } };
504
+ return given({ mimic: { joint: m.follow, multiplier: m.multiplier, ...(m.offset ? { offset: m.offset } : {}) } });
554
505
  if (m.followSum)
555
- return { ...moving, mimic: { sum: m.followSum, ...(m.offset ? { offset: m.offset } : {}) } };
506
+ return given({ mimic: { sum: m.followSum, ...(m.offset ? { offset: m.offset } : {}) } });
556
507
  if (m.kind === 'slide' && m.travelSum && !m.travel)
557
- return { ...moving, travel: m.travelSum, state: { id: m.id || `${part}-${m.kind}` } };
508
+ return given({ travel: m.travelSum, state: { id: m.id || `${part}-${m.kind}` } });
558
509
  if (m.kind === 'slide' && m.travel) {
559
510
  const [of, axis] = m.travel.split(':');
560
- return { ...moving, travel: { source: { of: 'part', part: of, axis: axis }, ...(m.travelPlus ? { plus: m.travelPlus } : {}) }, state: { id: m.id || `${part}-${m.kind}` } };
511
+ return given({ travel: { source: { of: 'part', part: of, axis: axis }, ...(m.travelPlus ? { plus: m.travelPlus } : {}) }, state: { id: m.id || `${part}-${m.kind}` } });
561
512
  }
562
- return { ...moving, ...(m.kind === 'continuous' ? {} : { lowerLimit: m.min, upperLimit: m.max }), state: { id: m.id || `${part}-${m.kind}` } };
513
+ return given({ ...(m.kind === 'continuous' ? {} : { lowerLimit: m.min, upperLimit: m.max }), state: { id: m.id || `${part}-${m.kind}` } });
563
514
  }
564
515
  /**
565
516
  * Why the motion form cannot be saved as it stands, as the kernel would refuse it: a follower's multiplier that is not
@@ -727,15 +678,6 @@ export function rangeDiffers(a, b) {
727
678
  export function turnsAboutAJoint(asset) {
728
679
  return modelOf(asset).nodes.some(n => n.op === 'axis-turn@1');
729
680
  }
730
- /** The joint made still: its fastening, its place and its row kept, its motion gone. */
731
- export function fixedJointOf(joint) {
732
- return {
733
- id: joint.id,
734
- type: 'fixed',
735
- body0: joint.body0,
736
- body1: joint.body1,
737
- ...(joint.origin ? { origin: joint.origin } : {}),
738
- ...(joint.repeat ? { repeat: joint.repeat } : {})
739
- };
740
- }
681
+ /** The joint made still: its fastening, its place and its row kept, its motion gone (the kernel's `figureJointAsFixed`, ADR-0140 D6). */
682
+ export const fixedJointOf = figureJointAsFixed;
741
683
  //# sourceMappingURL=links.js.map