@code3d/core 0.0.1-alpha.0 → 0.0.1-alpha.11

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 (154) hide show
  1. package/README.md +43 -807
  2. package/THIRD_PARTY.md +40 -0
  3. package/bld/THIRD_PARTY_NOTICES.txt +182 -0
  4. package/bld/chunks/chunk-7FIFPADJ.js +1 -0
  5. package/bld/chunks/chunk-7FIFPADJ.js.map +7 -0
  6. package/bld/chunks/chunk-CL3E2DE4.js +19 -0
  7. package/bld/chunks/chunk-CL3E2DE4.js.map +7 -0
  8. package/bld/chunks/chunk-HZRQUHM2.js +30 -0
  9. package/bld/chunks/chunk-HZRQUHM2.js.map +7 -0
  10. package/bld/chunks/chunk-JBVIAMJ6.js +26474 -0
  11. package/bld/chunks/chunk-JBVIAMJ6.js.map +7 -0
  12. package/bld/chunks/chunk-QK6ZET47.js +31 -0
  13. package/bld/chunks/chunk-QK6ZET47.js.map +7 -0
  14. package/bld/library/bound-solver.d.ts +29 -5
  15. package/bld/library/bound-solver.d.ts.map +1 -1
  16. package/bld/library/cached.d.ts +35 -0
  17. package/bld/library/cached.d.ts.map +1 -0
  18. package/bld/library/font.d.ts +39 -0
  19. package/bld/library/font.d.ts.map +1 -0
  20. package/bld/library/google-font.d.ts +14 -0
  21. package/bld/library/google-font.d.ts.map +1 -0
  22. package/bld/library/index.d.ts +10 -2
  23. package/bld/library/index.d.ts.map +1 -1
  24. package/bld/library/index.js +94 -3
  25. package/bld/library/index.js.map +7 -1
  26. package/bld/library/inspect.d.ts +100 -0
  27. package/bld/library/inspect.d.ts.map +1 -0
  28. package/bld/library/kernel-artifact-codec.d.ts.map +1 -1
  29. package/bld/library/kernel-cache.d.ts +28 -7
  30. package/bld/library/kernel-cache.d.ts.map +1 -1
  31. package/bld/library/loft.d.ts.map +1 -1
  32. package/bld/library/material.d.ts +1 -1
  33. package/bld/library/material.d.ts.map +1 -1
  34. package/bld/library/open-cascade.d.ts +4 -0
  35. package/bld/library/open-cascade.d.ts.map +1 -0
  36. package/bld/library/relation-solver.d.ts +19 -8
  37. package/bld/library/relation-solver.d.ts.map +1 -1
  38. package/bld/library/replicad.d.ts +3 -2
  39. package/bld/library/replicad.d.ts.map +1 -1
  40. package/bld/library/replicad.js +10 -24
  41. package/bld/library/replicad.js.map +7 -1
  42. package/bld/library/retained-memory.d.ts.map +1 -1
  43. package/bld/library/runtime.d.ts +518 -155
  44. package/bld/library/runtime.d.ts.map +1 -1
  45. package/bld/library/sketch-solver.d.ts +1 -1
  46. package/bld/library/sketch-solver.d.ts.map +1 -1
  47. package/bld/library/sketch.d.ts +21 -1
  48. package/bld/library/sketch.d.ts.map +1 -1
  49. package/bld/library/text.d.ts +53 -0
  50. package/bld/library/text.d.ts.map +1 -0
  51. package/bld/library/three.js +3 -3
  52. package/bld/library/three.js.map +7 -1
  53. package/bld/library/topology.d.ts +6 -2
  54. package/bld/library/topology.d.ts.map +1 -1
  55. package/bld/node/index.d.ts.map +1 -1
  56. package/bld/node/index.js +96 -14
  57. package/bld/node/index.js.map +7 -1
  58. package/bld/node/replicad.js +13 -3
  59. package/bld/node/replicad.js.map +7 -1
  60. package/bld/tooling/index.d.ts +18 -15
  61. package/bld/tooling/index.d.ts.map +1 -1
  62. package/bld/tooling/index.js +165 -19
  63. package/bld/tooling/index.js.map +7 -1
  64. package/docs/api.md +764 -0
  65. package/docs/custom-primitives.mdx +58 -0
  66. package/docs/local-coordinates.md +187 -0
  67. package/docs/origins-and-rotation.mdx +105 -0
  68. package/docs/relations.mdx +450 -0
  69. package/docs/runtime.md +297 -0
  70. package/docs/shells.mdx +70 -0
  71. package/docs/sketches.md +83 -0
  72. package/docs/text.md +23 -0
  73. package/docs/topology.md +152 -0
  74. package/docs/values.md +119 -0
  75. package/package.json +20 -7
  76. package/src/library/bound-solver.ts +103 -73
  77. package/src/library/cached.ts +153 -0
  78. package/src/library/font.ts +186 -0
  79. package/src/library/google-font.ts +97 -0
  80. package/src/library/index.ts +53 -5
  81. package/src/library/inspect.ts +178 -0
  82. package/src/library/kernel-artifact-codec.ts +129 -20
  83. package/src/library/kernel-cache.ts +175 -39
  84. package/src/library/loft.ts +9 -1
  85. package/src/library/material.ts +3 -4
  86. package/src/library/open-cascade.ts +13 -0
  87. package/src/library/relation-solver.ts +230 -113
  88. package/src/library/replicad.ts +5 -5
  89. package/src/library/retained-memory.ts +6 -1
  90. package/src/library/runtime.ts +3769 -1178
  91. package/src/library/shell.ts +3 -3
  92. package/src/library/sketch-solver.ts +3 -1
  93. package/src/library/sketch.ts +112 -13
  94. package/src/library/text.ts +477 -0
  95. package/src/library/topology.ts +61 -17
  96. package/src/library/tsconfig.json +2 -1
  97. package/src/node/index.ts +14 -3
  98. package/src/node/tsconfig.json +2 -1
  99. package/src/tooling/index.ts +64 -51
  100. package/src/tooling/tsconfig.json +2 -1
  101. package/bld/library/alignment-geometry.js +0 -322
  102. package/bld/library/alignment-geometry.js.map +0 -1
  103. package/bld/library/bound-solver.js +0 -252
  104. package/bld/library/bound-solver.js.map +0 -1
  105. package/bld/library/extrude.js +0 -28
  106. package/bld/library/extrude.js.map +0 -1
  107. package/bld/library/kernel-artifact-codec.js +0 -148
  108. package/bld/library/kernel-artifact-codec.js.map +0 -1
  109. package/bld/library/kernel-cache.js +0 -245
  110. package/bld/library/kernel-cache.js.map +0 -1
  111. package/bld/library/kernel-shapes.js +0 -108
  112. package/bld/library/kernel-shapes.js.map +0 -1
  113. package/bld/library/loft.js +0 -161
  114. package/bld/library/loft.js.map +0 -1
  115. package/bld/library/material.js +0 -176
  116. package/bld/library/material.js.map +0 -1
  117. package/bld/library/model-color.js +0 -36
  118. package/bld/library/model-color.js.map +0 -1
  119. package/bld/library/open-cascade-error.js +0 -22
  120. package/bld/library/open-cascade-error.js.map +0 -1
  121. package/bld/library/relation-solver.js +0 -258
  122. package/bld/library/relation-solver.js.map +0 -1
  123. package/bld/library/retained-memory.js +0 -27
  124. package/bld/library/retained-memory.js.map +0 -1
  125. package/bld/library/runtime.js +0 -2954
  126. package/bld/library/runtime.js.map +0 -1
  127. package/bld/library/shell.js +0 -221
  128. package/bld/library/shell.js.map +0 -1
  129. package/bld/library/sketch-curve-intersections.js +0 -117
  130. package/bld/library/sketch-curve-intersections.js.map +0 -1
  131. package/bld/library/sketch-curves.js +0 -89
  132. package/bld/library/sketch-curves.js.map +0 -1
  133. package/bld/library/sketch-drag-rules.js +0 -522
  134. package/bld/library/sketch-drag-rules.js.map +0 -1
  135. package/bld/library/sketch-face.js +0 -72
  136. package/bld/library/sketch-face.js.map +0 -1
  137. package/bld/library/sketch-incidence.js +0 -114
  138. package/bld/library/sketch-incidence.js.map +0 -1
  139. package/bld/library/sketch-precision.js +0 -55
  140. package/bld/library/sketch-precision.js.map +0 -1
  141. package/bld/library/sketch-regions.js +0 -193
  142. package/bld/library/sketch-regions.js.map +0 -1
  143. package/bld/library/sketch-solver.js +0 -578
  144. package/bld/library/sketch-solver.js.map +0 -1
  145. package/bld/library/sketch.js +0 -650
  146. package/bld/library/sketch.js.map +0 -1
  147. package/bld/library/spatial.js +0 -195
  148. package/bld/library/spatial.js.map +0 -1
  149. package/bld/library/topology-id.js +0 -67
  150. package/bld/library/topology-id.js.map +0 -1
  151. package/bld/library/topology-inspection.js +0 -267
  152. package/bld/library/topology-inspection.js.map +0 -1
  153. package/bld/library/topology.js +0 -455
  154. package/bld/library/topology.js.map +0 -1
@@ -1,8 +1,10 @@
1
1
  import {
2
2
  axisRotation,
3
+ externalRotationFrame,
3
4
  solveBodies as solveBoundBodies,
4
5
  type BodyRelation as BoundRelation,
5
- type BodyRotation,
6
+ type BodyAction,
7
+ solveTranslations,
6
8
  } from './bound-solver.js';
7
9
  import {
8
10
  alignmentResidual,
@@ -24,16 +26,14 @@ import {
24
26
  composeTransforms,
25
27
  identityRigidTransform,
26
28
  invertTransform,
27
- origin,
28
29
  rotateVector,
29
30
  rotation,
30
- translation,
31
31
  type Quaternion,
32
32
  type RigidTransform,
33
33
  type Vec3,
34
34
  } from './spatial.js';
35
35
 
36
- export {axisRotation, type BodyRotation};
36
+ export {axisRotation, type BodyAction};
37
37
  type AlignEndpoint = Readonly<{
38
38
  body: number;
39
39
  geometry: AlignmentGeometry;
@@ -44,76 +44,76 @@ type AlignRelation = Readonly<{
44
44
  id: string;
45
45
  source: AlignEndpoint;
46
46
  target: AlignEndpoint;
47
- offset: Vec3 | undefined;
48
- rotations: readonly BodyRotation[];
49
47
  }>;
50
- type Relation = (BoundRelation & {kind: 'on'}) | AlignRelation;
51
- export type Body = Readonly<{name: string; relations: readonly Relation[]}>;
48
+ type FrameRelation = Readonly<{
49
+ kind: 'frame';
50
+ id: string;
51
+ source: Omit<AlignEndpoint, 'geometry'>;
52
+ target: Omit<AlignEndpoint, 'geometry'>;
53
+ }>;
54
+ type Relation = (BoundRelation & {kind: 'on'}) | AlignRelation | FrameRelation;
55
+ export type Body = Readonly<{
56
+ name: string;
57
+ relations: readonly Relation[];
58
+ initial?: RigidTransform;
59
+ transformations?: readonly BodyAction[];
60
+ }>;
52
61
 
53
- /** Authored actions apply after satisfying the relation, about self or an external axis. */
54
- export function beforeRelation(
62
+ function applyExternalRotation(
55
63
  pose: RigidTransform,
56
- relation: Pick<Relation, 'kind' | 'rotations' | 'offset' | 'target'>,
64
+ action: Exclude<BodyAction, {offset: Vec3} | {local: RigidTransform}>,
65
+ reference: RigidTransform,
66
+ inverse = false,
67
+ ): RigidTransform {
68
+ const {center, quaternion} = externalRotationFrame(
69
+ action,
70
+ pose.quaternion,
71
+ reference.quaternion,
72
+ inverse,
73
+ );
74
+ const point = addVectors(reference.position, center);
75
+ return composeTransforms(
76
+ {quaternion, position: subtract(point, rotateVector(point, quaternion))},
77
+ pose,
78
+ );
79
+ }
80
+
81
+ /** Undo exactly the authored prefix, in reverse call order. */
82
+ export function beforeTransformations(
83
+ pose: RigidTransform,
84
+ relation: {
85
+ actions: readonly BodyAction[];
86
+ },
57
87
  poses: readonly RigidTransform[],
58
- owner: number,
59
88
  ): RigidTransform {
60
- for (const action of [...relation.rotations].reverse())
61
- pose =
62
- 'local' in action
63
- ? composeTransforms(pose, invertTransform(action.local))
64
- : composeTransforms(
65
- axisRotation(
66
- composeTransforms(poses[action.body], action.axis),
67
- -action.angle,
68
- ),
69
- pose,
70
- );
71
- if (relation.kind === 'align' && relation.offset) {
72
- const target =
73
- relation.target.body === owner ? pose : poses[relation.target.body];
74
- const frame = composeTransforms(target, relation.target.transform);
75
- pose = {
76
- ...pose,
77
- position: subtract(
78
- pose.position,
79
- rotateVector(relation.offset, frame.quaternion),
80
- ),
81
- };
89
+ for (const action of [...relation.actions].reverse()) {
90
+ if ('offset' in action) {
91
+ pose = {...pose, position: subtract(pose.position, action.offset)};
92
+ } else
93
+ pose =
94
+ 'local' in action
95
+ ? composeTransforms(pose, invertTransform(action.local))
96
+ : applyExternalRotation(pose, action, poses[action.body], true);
82
97
  }
83
98
  return pose;
84
99
  }
85
100
 
86
- function afterRelation(
101
+ export function afterTransformations(
87
102
  pose: RigidTransform,
88
- relation: Relation,
103
+ relation: {
104
+ actions: readonly BodyAction[];
105
+ },
89
106
  poses: readonly RigidTransform[],
90
- owner: number,
91
107
  ): RigidTransform {
92
- if (relation.kind === 'align' && relation.offset) {
93
- const target =
94
- relation.target.body === owner ? pose : poses[relation.target.body];
95
- pose = {
96
- ...pose,
97
- position: addVectors(
98
- pose.position,
99
- rotateVector(
100
- relation.offset,
101
- composeTransforms(target, relation.target.transform).quaternion,
102
- ),
103
- ),
104
- };
108
+ for (const action of relation.actions) {
109
+ if ('offset' in action) {
110
+ pose = {...pose, position: addVectors(pose.position, action.offset)};
111
+ } else
112
+ pose =
113
+ 'local' in action
114
+ ? composeTransforms(pose, action.local)
115
+ : applyExternalRotation(pose, action, poses[action.body]);
105
116
  }
106
- for (const action of relation.rotations)
107
- pose =
108
- 'local' in action
109
- ? composeTransforms(pose, action.local)
110
- : composeTransforms(
111
- axisRotation(
112
- composeTransforms(poses[action.body], action.axis),
113
- action.angle,
114
- ),
115
- pose,
116
- );
117
117
  return pose;
118
118
  }
119
119
 
@@ -271,10 +271,35 @@ export function solveBodies(
271
271
  if (relation.kind === 'align')
272
272
  validateAlignment(relation.source.geometry, relation.target.geometry);
273
273
  const active = bodies.flatMap((body, i) =>
274
- body.relations.length ? [i] : [],
274
+ body.relations.length || body.transformations?.length ? [i] : [],
275
275
  );
276
+ const unplace = (
277
+ pose: RigidTransform,
278
+ owner: number,
279
+ poses: readonly RigidTransform[],
280
+ ) =>
281
+ beforeTransformations(
282
+ pose,
283
+ {actions: bodies[owner].transformations ?? []},
284
+ poses,
285
+ );
286
+ const place = (
287
+ pose: RigidTransform,
288
+ owner: number,
289
+ poses: readonly RigidTransform[],
290
+ ) =>
291
+ afterTransformations(
292
+ pose,
293
+ {actions: bodies[owner].transformations ?? []},
294
+ poses,
295
+ );
296
+ const baselinePose = (
297
+ pose: RigidTransform,
298
+ poses: readonly RigidTransform[],
299
+ owner: number,
300
+ ) => unplace(pose, owner, poses);
276
301
  const flexible = bodies.map(body =>
277
- body.relations.some(r => r.kind === 'align'),
302
+ body.relations.some(r => r.kind !== 'on'),
278
303
  );
279
304
  // Stable ordering and deduplication keep repeated conditions from biasing seeds.
280
305
  const relations = bodies.flatMap((body, owner) =>
@@ -287,31 +312,28 @@ export function solveBodies(
287
312
  ]
288
313
  .sort(([a], [b]) => a.localeCompare(b))
289
314
  .map(([, entry]) => entry);
290
- let poses = bodies.map(() => identityRigidTransform);
291
- for (const owner of active) {
292
- const authored =
293
- unique.find(
294
- entry => entry.owner === owner && entry.relation.rotations.length,
295
- )?.relation ??
296
- unique.find(
297
- entry =>
298
- entry.owner === owner &&
299
- entry.relation.kind === 'align' &&
300
- entry.relation.offset,
301
- )?.relation;
302
- if (authored)
303
- poses[owner] = afterRelation(
304
- identityRigidTransform,
305
- authored,
306
- poses,
307
- owner,
308
- );
309
- }
315
+ let poses = bodies.map(body => body.initial ?? identityRigidTransform);
316
+ for (const owner of active) poses[owner] = place(poses[owner], owner, poses);
310
317
  for (const {owner, relation} of unique) {
311
- if (relation.kind !== 'align') continue;
318
+ if (relation.kind === 'on') continue;
312
319
  const sourceSelf = relation.source.body === owner;
313
320
  if (relation.target.body === relation.source.body) continue;
314
- const baseline = beforeRelation(poses[owner], relation, poses, owner);
321
+ const baseline = baselinePose(poses[owner], poses, owner);
322
+ if (relation.kind === 'frame') {
323
+ const source = composeTransforms(
324
+ sourceSelf ? baseline : poses[relation.source.body],
325
+ relation.source.transform,
326
+ );
327
+ const target = composeTransforms(
328
+ relation.target.body === owner ? baseline : poses[relation.target.body],
329
+ relation.target.transform,
330
+ );
331
+ const seed = sourceSelf
332
+ ? composeTransforms(target, invertTransform(source))
333
+ : composeTransforms(source, invertTransform(target));
334
+ poses[owner] = place(composeTransforms(seed, baseline), owner, poses);
335
+ continue;
336
+ }
315
337
  const source = transformGeometry(
316
338
  relation.source.geometry,
317
339
  relation.source.body === owner ? baseline : poses[relation.source.body],
@@ -323,12 +345,7 @@ export function solveBodies(
323
345
  const seed = sourceSelf
324
346
  ? alignmentSeed(source, target)
325
347
  : alignmentSeed(target, source);
326
- poses[owner] = afterRelation(
327
- composeTransforms(seed, baseline),
328
- relation,
329
- poses,
330
- owner,
331
- );
348
+ poses[owner] = place(composeTransforms(seed, baseline), owner, poses);
332
349
  }
333
350
  const geometryScale = Math.max(
334
351
  1,
@@ -340,12 +357,25 @@ export function solveBodies(
340
357
  : [],
341
358
  ),
342
359
  );
343
- const variables = active.flatMap(body =>
344
- Array.from({length: 6}, (_, axis) => ({body, axis})),
360
+ const fullVariables = active.flatMap(body =>
361
+ // Bound-only bodies without authored rotations have exactly identity
362
+ // orientation. Do not numerically perturb those fixed axes: native bound
363
+ // queries can introduce noise that prevents convergence at the identity.
364
+ Array.from(
365
+ {
366
+ length:
367
+ flexible[body] ||
368
+ bodies[body].transformations?.some(action => !('offset' in action))
369
+ ? 6
370
+ : 3,
371
+ },
372
+ (_, axis) => ({body, axis}),
373
+ ),
345
374
  );
375
+ let variables = fullVariables.filter(variable => variable.axis < 3);
346
376
  const residual = (candidate: readonly RigidTransform[]): number[] => [
347
377
  ...unique.flatMap(({owner, relation: r}) => {
348
- const baseline = beforeRelation(candidate[owner], r, candidate, owner);
378
+ const baseline = baselinePose(candidate[owner], candidate, owner);
349
379
  const source =
350
380
  r.source.body === owner ? baseline : candidate[r.source.body],
351
381
  target = r.target.body === owner ? baseline : candidate[r.target.body];
@@ -354,6 +384,18 @@ export function solveBodies(
354
384
  transformGeometry(r.source.geometry, source),
355
385
  transformGeometry(r.target.geometry, target),
356
386
  );
387
+ if (r.kind === 'frame') {
388
+ const a = composeTransforms(source, r.source.transform);
389
+ const b = composeTransforms(target, r.target.transform);
390
+ const relative = composeTransforms(invertTransform(b), a);
391
+ // q and -q denote the same orientation. The local rotation error has
392
+ // three independent components; the exact seed also handles half turns.
393
+ const sign = relative.quaternion[3] < 0 ? -1 : 1;
394
+ return [
395
+ ...subtract(a.position, b.position),
396
+ ...relative.quaternion.slice(0, 3).map(value => 2 * sign * value),
397
+ ];
398
+ }
357
399
  const frame = composeTransforms(target, r.target.transform);
358
400
  const relative = composeTransforms(
359
401
  invertTransform(rotation(frame.quaternion)),
@@ -369,29 +411,28 @@ export function solveBodies(
369
411
  source.position,
370
412
  rotateVector(center, frame.quaternion),
371
413
  );
372
- const b = composeTransforms(
373
- frame,
374
- translation(r.offset ?? origin),
375
- ).position;
414
+ const b = frame.position;
376
415
  const delta = rotateVector(
377
416
  subtract(a, b),
378
417
  invertTransform(frame).quaternion,
379
418
  );
380
- return r.offset ? [...delta] : [delta[1]];
419
+ return [delta[1]];
381
420
  }),
382
421
  ...active.flatMap(owner => {
383
422
  if (flexible[owner]) return [];
384
- const rotations = bodies[owner].relations.filter(r => r.rotations.length);
385
- return rotations.length
386
- ? rotations.flatMap(r =>
387
- beforeRelation(
388
- candidate[owner],
389
- r,
390
- candidate,
391
- owner,
392
- ).quaternion.slice(0, 3),
393
- )
394
- : candidate[owner].quaternion.slice(0, 3);
423
+ const initial = bodies[owner].initial ?? identityRigidTransform;
424
+ const unplaced = unplace(candidate[owner], owner, candidate);
425
+ const orientationResidual = (pose: RigidTransform) =>
426
+ composeTransforms(
427
+ invertTransform(rotation(initial.quaternion)),
428
+ rotation(pose.quaternion),
429
+ ).quaternion.slice(0, 3);
430
+ return [
431
+ ...(bodies[owner].relations.length
432
+ ? []
433
+ : subtract(unplaced.position, initial.position)),
434
+ ...orientationResidual(unplaced),
435
+ ];
395
436
  }),
396
437
  ];
397
438
  const perturb = (
@@ -420,12 +461,80 @@ export function solveBodies(
420
461
  };
421
462
  });
422
463
  };
464
+ // Restore bound-only bodies' free translations without breaking alignments to
465
+ // dependent bodies. Every active translation participates in the nullspace;
466
+ // only bound-only bodies contribute preferences for their incoming pose.
467
+ const settleBoundTranslations = (): readonly RigidTransform[] => {
468
+ if (!active.some(owner => !flexible[owner])) return poses;
469
+ const free = active.flatMap(body => [0, 1, 2].map(axis => ({body, axis})));
470
+ if (!free.length) return poses;
471
+ const shift = (steps: readonly number[]) => {
472
+ const result = poses.map(pose => ({
473
+ ...pose,
474
+ position: [...pose.position] as [number, number, number],
475
+ }));
476
+ free.forEach(({body, axis}, i) => {
477
+ result[body].position[axis] += steps[i];
478
+ });
479
+ return result;
480
+ };
481
+ const preferences = (candidate: readonly RigidTransform[]) =>
482
+ unique
483
+ .filter(({owner}) => !flexible[owner])
484
+ .flatMap(({owner}) =>
485
+ subtract(
486
+ baselinePose(candidate[owner], candidate, owner).position,
487
+ bodies[owner].initial?.position ?? [0, 0, 0],
488
+ ),
489
+ );
490
+ const h = 1e-5;
491
+ const initial = residual(poses),
492
+ preference = preferences(poses);
493
+ const columns = free.map((_, i) => {
494
+ const plus = shift(free.map((_, j) => (i === j ? h : 0)));
495
+ const minus = shift(free.map((_, j) => (i === j ? -h : 0)));
496
+ const a = residual(plus),
497
+ b = residual(minus),
498
+ p = preferences(plus),
499
+ q = preferences(minus);
500
+ return {
501
+ constraints: a.map((v, j) => (v - b[j]) / (2 * h)),
502
+ preferences: p.map((v, j) => (v - q[j]) / (2 * h)),
503
+ };
504
+ });
505
+ const step = solveTranslations(
506
+ initial.map((value, i) => ({
507
+ id: 'bound translation',
508
+ value: -value,
509
+ coefficients: columns.map(column => column.constraints[i]),
510
+ })),
511
+ preference.map((value, i) => ({
512
+ id: 'pre-action position',
513
+ value: -value,
514
+ coefficients: columns.map(column => column.preferences[i]),
515
+ })),
516
+ free.length,
517
+ );
518
+ const candidate = shift(step);
519
+ // A nonlinear geometric locus need not admit its linearized free step.
520
+ // Preserve the valid geometric solution when that direction is not free.
521
+ return residual(candidate).every(value => Math.abs(value) < 1e-8)
522
+ ? candidate
523
+ : poses;
524
+ };
423
525
  let values = residual(poses),
424
526
  damping = 1e-5;
425
527
  const norm = (values: readonly number[]) =>
426
528
  values.reduce((sum, x) => sum + x * x, 0);
427
- for (let iteration = 0; iteration < 160; iteration++) {
428
- if (values.every(v => Math.abs(v) < 1e-8)) return poses;
529
+ for (let iteration = 0; iteration < 168; iteration++) {
530
+ // First settle translation along the seeded orientations. This prevents
531
+ // unconstrained rotations from tilting a contact chain merely to move it.
532
+ // Relations that genuinely require rotation then use the full rigid solve.
533
+ if (iteration === 8) {
534
+ variables = fullVariables;
535
+ damping = 1e-5;
536
+ }
537
+ if (values.every(v => Math.abs(v) < 1e-8)) return settleBoundTranslations();
429
538
  const h = 1e-6;
430
539
  const columns = variables.map((_, i) => {
431
540
  const shifted = residual(
@@ -434,7 +543,15 @@ export function solveBodies(
434
543
  variables.map((_, j) => (i === j ? h : 0)),
435
544
  ),
436
545
  );
437
- return values.map((v, j) => (shifted[j] - v) / h);
546
+ const opposite = residual(
547
+ perturb(
548
+ poses,
549
+ variables.map((_, j) => (i === j ? -h : 0)),
550
+ ),
551
+ );
552
+ // A directional bound has a cusp at an axis-aligned pose. A forward
553
+ // difference invents a rotation gradient there; use both sides.
554
+ return values.map((_, j) => (shifted[j] - opposite[j]) / (2 * h));
438
555
  });
439
556
  const multiply = (a: readonly number[], b: readonly number[]) =>
440
557
  a.reduce((sum, x, i) => sum + x * b[i], 0);
@@ -1,5 +1,5 @@
1
1
  import * as replicadModule from 'replicad';
2
- import {modelFromReplicadSolid} from './runtime.js';
2
+ import {primitiveConstructor} from './runtime.js';
3
3
  import {castOwnedShape} from './kernel-shapes.js';
4
4
  import type {SolidModel} from './index.js';
5
5
  import type {Shape3D} from 'replicad';
@@ -29,12 +29,12 @@ export const replicad: Replicad = Object.freeze({
29
29
 
30
30
  /**
31
31
  * Defines a synchronous model constructor with the builder's parameters.
32
- * The builder runs on every call and transfers ownership of its returned solid
33
- * to code3d. Intermediate shapes remain the builder's responsibility.
32
+ * Reuses deterministic geometry by arguments; pass changing captured state as
33
+ * explicit parameters. Each invocation creates a fresh model. The builder
34
+ * transfers its returned solid to code3d and owns its intermediate shapes.
34
35
  */
35
36
  export function definePrimitive<
36
37
  Builder extends (...arguments_: never[]) => Shape3D,
37
38
  >(build: Builder): (...arguments_: Parameters<Builder>) => SolidModel {
38
- return (...arguments_: Parameters<Builder>) =>
39
- modelFromReplicadSolid(build(...arguments_));
39
+ return primitiveConstructor(build);
40
40
  }
@@ -12,7 +12,12 @@ export function estimateRetainedBytes(value: unknown): number {
12
12
  seen.add(value);
13
13
  if (ArrayBuffer.isView(value)) return 64 + visit(value.buffer);
14
14
  if (value instanceof ArrayBuffer) return 32 + value.byteLength;
15
- const values = Object.values(value);
15
+ const values =
16
+ value instanceof Map
17
+ ? [...value].flat()
18
+ : value instanceof Set
19
+ ? [...value]
20
+ : Object.values(value);
16
21
  return (
17
22
  32 +
18
23
  values.length * 8 +