@code3d/core 0.0.1-alpha.4 → 0.0.1-alpha.6

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 (40) hide show
  1. package/README.md +31 -3
  2. package/bld/chunks/{chunk-OQLJDJIF.js → chunk-3RIMOCPP.js} +2 -2
  3. package/bld/chunks/{chunk-PAIFBNU4.js → chunk-KYW4AWKO.js} +2 -2
  4. package/bld/chunks/{chunk-J77FMYFY.js → chunk-T3RAUNSF.js} +941 -514
  5. package/bld/chunks/chunk-T3RAUNSF.js.map +7 -0
  6. package/bld/chunks/{chunk-ZMT6EFHL.js → chunk-T6COR2SG.js} +3 -3
  7. package/bld/library/bound-solver.d.ts +29 -5
  8. package/bld/library/bound-solver.d.ts.map +1 -1
  9. package/bld/library/index.d.ts +2 -2
  10. package/bld/library/index.d.ts.map +1 -1
  11. package/bld/library/index.js +15 -1
  12. package/bld/library/kernel-cache.d.ts +2 -1
  13. package/bld/library/kernel-cache.d.ts.map +1 -1
  14. package/bld/library/relation-solver.d.ts +12 -7
  15. package/bld/library/relation-solver.d.ts.map +1 -1
  16. package/bld/library/replicad.js +2 -2
  17. package/bld/library/runtime.d.ts +214 -78
  18. package/bld/library/runtime.d.ts.map +1 -1
  19. package/bld/library/sketch.d.ts +2 -2
  20. package/bld/library/sketch.d.ts.map +1 -1
  21. package/bld/library/topology.d.ts +2 -0
  22. package/bld/library/topology.d.ts.map +1 -1
  23. package/bld/node/index.js +17 -3
  24. package/bld/node/replicad.js +4 -4
  25. package/bld/tooling/index.d.ts +3 -3
  26. package/bld/tooling/index.d.ts.map +1 -1
  27. package/bld/tooling/index.js +16 -10
  28. package/package.json +1 -1
  29. package/src/library/bound-solver.ts +103 -73
  30. package/src/library/index.ts +13 -2
  31. package/src/library/kernel-cache.ts +8 -0
  32. package/src/library/relation-solver.ts +194 -110
  33. package/src/library/runtime.ts +1029 -436
  34. package/src/library/sketch.ts +3 -3
  35. package/src/library/topology.ts +13 -2
  36. package/src/tooling/index.ts +14 -7
  37. package/bld/chunks/chunk-J77FMYFY.js.map +0 -7
  38. /package/bld/chunks/{chunk-OQLJDJIF.js.map → chunk-3RIMOCPP.js.map} +0 -0
  39. /package/bld/chunks/{chunk-PAIFBNU4.js.map → chunk-KYW4AWKO.js.map} +0 -0
  40. /package/bld/chunks/{chunk-ZMT6EFHL.js.map → chunk-T6COR2SG.js.map} +0 -0
@@ -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,70 @@ 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
48
  type Relation = (BoundRelation & {kind: 'on'}) | AlignRelation;
51
- export type Body = Readonly<{name: string; relations: readonly Relation[]}>;
49
+ export type Body = Readonly<{
50
+ name: string;
51
+ relations: readonly Relation[];
52
+ initial?: RigidTransform;
53
+ transformations?: readonly BodyAction[];
54
+ }>;
52
55
 
53
- /** Authored actions apply after satisfying the relation, about self or an external axis. */
54
- export function beforeRelation(
56
+ function applyExternalRotation(
55
57
  pose: RigidTransform,
56
- relation: Pick<Relation, 'kind' | 'rotations' | 'offset' | 'target'>,
58
+ action: Exclude<BodyAction, {offset: Vec3} | {local: RigidTransform}>,
59
+ reference: RigidTransform,
60
+ inverse = false,
61
+ ): RigidTransform {
62
+ const {center, quaternion} = externalRotationFrame(
63
+ action,
64
+ pose.quaternion,
65
+ reference.quaternion,
66
+ inverse,
67
+ );
68
+ const point = addVectors(reference.position, center);
69
+ return composeTransforms(
70
+ {quaternion, position: subtract(point, rotateVector(point, quaternion))},
71
+ pose,
72
+ );
73
+ }
74
+
75
+ /** Undo exactly the authored prefix, in reverse call order. */
76
+ export function beforeTransformations(
77
+ pose: RigidTransform,
78
+ relation: {
79
+ actions: readonly BodyAction[];
80
+ },
57
81
  poses: readonly RigidTransform[],
58
- owner: number,
59
82
  ): 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
- };
83
+ for (const action of [...relation.actions].reverse()) {
84
+ if ('offset' in action) {
85
+ pose = {...pose, position: subtract(pose.position, action.offset)};
86
+ } else
87
+ pose =
88
+ 'local' in action
89
+ ? composeTransforms(pose, invertTransform(action.local))
90
+ : applyExternalRotation(pose, action, poses[action.body], true);
82
91
  }
83
92
  return pose;
84
93
  }
85
94
 
86
- function afterRelation(
95
+ export function afterTransformations(
87
96
  pose: RigidTransform,
88
- relation: Relation,
97
+ relation: {
98
+ actions: readonly BodyAction[];
99
+ },
89
100
  poses: readonly RigidTransform[],
90
- owner: number,
91
101
  ): 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
- };
102
+ for (const action of relation.actions) {
103
+ if ('offset' in action) {
104
+ pose = {...pose, position: addVectors(pose.position, action.offset)};
105
+ } else
106
+ pose =
107
+ 'local' in action
108
+ ? composeTransforms(pose, action.local)
109
+ : applyExternalRotation(pose, action, poses[action.body]);
105
110
  }
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
111
  return pose;
118
112
  }
119
113
 
@@ -271,8 +265,33 @@ export function solveBodies(
271
265
  if (relation.kind === 'align')
272
266
  validateAlignment(relation.source.geometry, relation.target.geometry);
273
267
  const active = bodies.flatMap((body, i) =>
274
- body.relations.length ? [i] : [],
268
+ body.relations.length || body.transformations?.length ? [i] : [],
275
269
  );
270
+ const unplace = (
271
+ pose: RigidTransform,
272
+ owner: number,
273
+ poses: readonly RigidTransform[],
274
+ ) =>
275
+ beforeTransformations(
276
+ pose,
277
+ {actions: bodies[owner].transformations ?? []},
278
+ poses,
279
+ );
280
+ const place = (
281
+ pose: RigidTransform,
282
+ owner: number,
283
+ poses: readonly RigidTransform[],
284
+ ) =>
285
+ afterTransformations(
286
+ pose,
287
+ {actions: bodies[owner].transformations ?? []},
288
+ poses,
289
+ );
290
+ const baselinePose = (
291
+ pose: RigidTransform,
292
+ poses: readonly RigidTransform[],
293
+ owner: number,
294
+ ) => unplace(pose, owner, poses);
276
295
  const flexible = bodies.map(body =>
277
296
  body.relations.some(r => r.kind === 'align'),
278
297
  );
@@ -287,31 +306,13 @@ export function solveBodies(
287
306
  ]
288
307
  .sort(([a], [b]) => a.localeCompare(b))
289
308
  .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
- }
309
+ let poses = bodies.map(body => body.initial ?? identityRigidTransform);
310
+ for (const owner of active) poses[owner] = place(poses[owner], owner, poses);
310
311
  for (const {owner, relation} of unique) {
311
312
  if (relation.kind !== 'align') continue;
312
313
  const sourceSelf = relation.source.body === owner;
313
314
  if (relation.target.body === relation.source.body) continue;
314
- const baseline = beforeRelation(poses[owner], relation, poses, owner);
315
+ const baseline = baselinePose(poses[owner], poses, owner);
315
316
  const source = transformGeometry(
316
317
  relation.source.geometry,
317
318
  relation.source.body === owner ? baseline : poses[relation.source.body],
@@ -323,12 +324,7 @@ export function solveBodies(
323
324
  const seed = sourceSelf
324
325
  ? alignmentSeed(source, target)
325
326
  : alignmentSeed(target, source);
326
- poses[owner] = afterRelation(
327
- composeTransforms(seed, baseline),
328
- relation,
329
- poses,
330
- owner,
331
- );
327
+ poses[owner] = place(composeTransforms(seed, baseline), owner, poses);
332
328
  }
333
329
  const geometryScale = Math.max(
334
330
  1,
@@ -340,12 +336,25 @@ export function solveBodies(
340
336
  : [],
341
337
  ),
342
338
  );
343
- const variables = active.flatMap(body =>
344
- Array.from({length: 6}, (_, axis) => ({body, axis})),
339
+ const fullVariables = active.flatMap(body =>
340
+ // Bound-only bodies without authored rotations have exactly identity
341
+ // orientation. Do not numerically perturb those fixed axes: native bound
342
+ // queries can introduce noise that prevents convergence at the identity.
343
+ Array.from(
344
+ {
345
+ length:
346
+ flexible[body] ||
347
+ bodies[body].transformations?.some(action => !('offset' in action))
348
+ ? 6
349
+ : 3,
350
+ },
351
+ (_, axis) => ({body, axis}),
352
+ ),
345
353
  );
354
+ let variables = fullVariables.filter(variable => variable.axis < 3);
346
355
  const residual = (candidate: readonly RigidTransform[]): number[] => [
347
356
  ...unique.flatMap(({owner, relation: r}) => {
348
- const baseline = beforeRelation(candidate[owner], r, candidate, owner);
357
+ const baseline = baselinePose(candidate[owner], candidate, owner);
349
358
  const source =
350
359
  r.source.body === owner ? baseline : candidate[r.source.body],
351
360
  target = r.target.body === owner ? baseline : candidate[r.target.body];
@@ -369,29 +378,28 @@ export function solveBodies(
369
378
  source.position,
370
379
  rotateVector(center, frame.quaternion),
371
380
  );
372
- const b = composeTransforms(
373
- frame,
374
- translation(r.offset ?? origin),
375
- ).position;
381
+ const b = frame.position;
376
382
  const delta = rotateVector(
377
383
  subtract(a, b),
378
384
  invertTransform(frame).quaternion,
379
385
  );
380
- return r.offset ? [...delta] : [delta[1]];
386
+ return [delta[1]];
381
387
  }),
382
388
  ...active.flatMap(owner => {
383
389
  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);
390
+ const initial = bodies[owner].initial ?? identityRigidTransform;
391
+ const unplaced = unplace(candidate[owner], owner, candidate);
392
+ const orientationResidual = (pose: RigidTransform) =>
393
+ composeTransforms(
394
+ invertTransform(rotation(initial.quaternion)),
395
+ rotation(pose.quaternion),
396
+ ).quaternion.slice(0, 3);
397
+ return [
398
+ ...(bodies[owner].relations.length
399
+ ? []
400
+ : subtract(unplaced.position, initial.position)),
401
+ ...orientationResidual(unplaced),
402
+ ];
395
403
  }),
396
404
  ];
397
405
  const perturb = (
@@ -420,12 +428,80 @@ export function solveBodies(
420
428
  };
421
429
  });
422
430
  };
431
+ // Restore bound-only bodies' free translations without breaking alignments to
432
+ // dependent bodies. Every active translation participates in the nullspace;
433
+ // only bound-only bodies contribute preferences for their incoming pose.
434
+ const settleBoundTranslations = (): readonly RigidTransform[] => {
435
+ if (!active.some(owner => !flexible[owner])) return poses;
436
+ const free = active.flatMap(body => [0, 1, 2].map(axis => ({body, axis})));
437
+ if (!free.length) return poses;
438
+ const shift = (steps: readonly number[]) => {
439
+ const result = poses.map(pose => ({
440
+ ...pose,
441
+ position: [...pose.position] as [number, number, number],
442
+ }));
443
+ free.forEach(({body, axis}, i) => {
444
+ result[body].position[axis] += steps[i];
445
+ });
446
+ return result;
447
+ };
448
+ const preferences = (candidate: readonly RigidTransform[]) =>
449
+ unique
450
+ .filter(({owner}) => !flexible[owner])
451
+ .flatMap(({owner}) =>
452
+ subtract(
453
+ baselinePose(candidate[owner], candidate, owner).position,
454
+ bodies[owner].initial?.position ?? [0, 0, 0],
455
+ ),
456
+ );
457
+ const h = 1e-5;
458
+ const initial = residual(poses),
459
+ preference = preferences(poses);
460
+ const columns = free.map((_, i) => {
461
+ const plus = shift(free.map((_, j) => (i === j ? h : 0)));
462
+ const minus = shift(free.map((_, j) => (i === j ? -h : 0)));
463
+ const a = residual(plus),
464
+ b = residual(minus),
465
+ p = preferences(plus),
466
+ q = preferences(minus);
467
+ return {
468
+ constraints: a.map((v, j) => (v - b[j]) / (2 * h)),
469
+ preferences: p.map((v, j) => (v - q[j]) / (2 * h)),
470
+ };
471
+ });
472
+ const step = solveTranslations(
473
+ initial.map((value, i) => ({
474
+ id: 'bound translation',
475
+ value: -value,
476
+ coefficients: columns.map(column => column.constraints[i]),
477
+ })),
478
+ preference.map((value, i) => ({
479
+ id: 'pre-action position',
480
+ value: -value,
481
+ coefficients: columns.map(column => column.preferences[i]),
482
+ })),
483
+ free.length,
484
+ );
485
+ const candidate = shift(step);
486
+ // A nonlinear geometric locus need not admit its linearized free step.
487
+ // Preserve the valid geometric solution when that direction is not free.
488
+ return residual(candidate).every(value => Math.abs(value) < 1e-8)
489
+ ? candidate
490
+ : poses;
491
+ };
423
492
  let values = residual(poses),
424
493
  damping = 1e-5;
425
494
  const norm = (values: readonly number[]) =>
426
495
  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;
496
+ for (let iteration = 0; iteration < 168; iteration++) {
497
+ // First settle translation along the seeded orientations. This prevents
498
+ // unconstrained rotations from tilting a contact chain merely to move it.
499
+ // Relations that genuinely require rotation then use the full rigid solve.
500
+ if (iteration === 8) {
501
+ variables = fullVariables;
502
+ damping = 1e-5;
503
+ }
504
+ if (values.every(v => Math.abs(v) < 1e-8)) return settleBoundTranslations();
429
505
  const h = 1e-6;
430
506
  const columns = variables.map((_, i) => {
431
507
  const shifted = residual(
@@ -434,7 +510,15 @@ export function solveBodies(
434
510
  variables.map((_, j) => (i === j ? h : 0)),
435
511
  ),
436
512
  );
437
- return values.map((v, j) => (shifted[j] - v) / h);
513
+ const opposite = residual(
514
+ perturb(
515
+ poses,
516
+ variables.map((_, j) => (i === j ? -h : 0)),
517
+ ),
518
+ );
519
+ // A directional bound has a cusp at an axis-aligned pose. A forward
520
+ // difference invents a rotation gradient there; use both sides.
521
+ return values.map((_, j) => (shifted[j] - opposite[j]) / (2 * h));
438
522
  });
439
523
  const multiply = (a: readonly number[], b: readonly number[]) =>
440
524
  a.reduce((sum, x, i) => sum + x * b[i], 0);