@code3d/core 0.0.1-alpha.3 → 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.
- package/README.md +31 -3
- package/bld/chunks/{chunk-U5G2VU2I.js → chunk-3RIMOCPP.js} +2 -2
- package/bld/chunks/{chunk-P5VGKAEM.js → chunk-KYW4AWKO.js} +2 -2
- package/bld/chunks/{chunk-RPY2WVCB.js → chunk-T3RAUNSF.js} +941 -506
- package/bld/chunks/chunk-T3RAUNSF.js.map +7 -0
- package/bld/chunks/{chunk-BF55AIEQ.js → chunk-T6COR2SG.js} +3 -3
- package/bld/library/bound-solver.d.ts +29 -5
- package/bld/library/bound-solver.d.ts.map +1 -1
- package/bld/library/index.d.ts +2 -2
- package/bld/library/index.d.ts.map +1 -1
- package/bld/library/index.js +15 -1
- package/bld/library/kernel-cache.d.ts +2 -1
- package/bld/library/kernel-cache.d.ts.map +1 -1
- package/bld/library/relation-solver.d.ts +12 -7
- package/bld/library/relation-solver.d.ts.map +1 -1
- package/bld/library/replicad.js +2 -2
- package/bld/library/runtime.d.ts +214 -76
- package/bld/library/runtime.d.ts.map +1 -1
- package/bld/library/sketch.d.ts +2 -2
- package/bld/library/sketch.d.ts.map +1 -1
- package/bld/library/topology.d.ts +2 -0
- package/bld/library/topology.d.ts.map +1 -1
- package/bld/node/index.js +17 -3
- package/bld/node/replicad.js +4 -4
- package/bld/tooling/index.d.ts +3 -3
- package/bld/tooling/index.d.ts.map +1 -1
- package/bld/tooling/index.js +16 -10
- package/package.json +1 -1
- package/src/library/bound-solver.ts +103 -73
- package/src/library/index.ts +13 -2
- package/src/library/kernel-cache.ts +8 -0
- package/src/library/relation-solver.ts +194 -110
- package/src/library/runtime.ts +1029 -424
- package/src/library/sketch.ts +3 -3
- package/src/library/topology.ts +13 -2
- package/src/tooling/index.ts +14 -7
- package/bld/chunks/chunk-RPY2WVCB.js.map +0 -7
- /package/bld/chunks/{chunk-U5G2VU2I.js.map → chunk-3RIMOCPP.js.map} +0 -0
- /package/bld/chunks/{chunk-P5VGKAEM.js.map → chunk-KYW4AWKO.js.map} +0 -0
- /package/bld/chunks/{chunk-BF55AIEQ.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
|
|
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
|
|
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<{
|
|
49
|
+
export type Body = Readonly<{
|
|
50
|
+
name: string;
|
|
51
|
+
relations: readonly Relation[];
|
|
52
|
+
initial?: RigidTransform;
|
|
53
|
+
transformations?: readonly BodyAction[];
|
|
54
|
+
}>;
|
|
52
55
|
|
|
53
|
-
|
|
54
|
-
export function beforeRelation(
|
|
56
|
+
function applyExternalRotation(
|
|
55
57
|
pose: RigidTransform,
|
|
56
|
-
|
|
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.
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
|
95
|
+
export function afterTransformations(
|
|
87
96
|
pose: RigidTransform,
|
|
88
|
-
relation:
|
|
97
|
+
relation: {
|
|
98
|
+
actions: readonly BodyAction[];
|
|
99
|
+
},
|
|
89
100
|
poses: readonly RigidTransform[],
|
|
90
|
-
owner: number,
|
|
91
101
|
): RigidTransform {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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(
|
|
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 =
|
|
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] =
|
|
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
|
|
344
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
386
|
+
return [delta[1]];
|
|
381
387
|
}),
|
|
382
388
|
...active.flatMap(owner => {
|
|
383
389
|
if (flexible[owner]) return [];
|
|
384
|
-
const
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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 <
|
|
428
|
-
|
|
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
|
-
|
|
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);
|