@code3d/core 0.0.1-alpha.4 → 0.0.1-alpha.7
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 +42 -5
- package/bld/chunks/{chunk-J77FMYFY.js → chunk-4WLVDYUJ.js} +992 -545
- package/bld/chunks/chunk-4WLVDYUJ.js.map +7 -0
- package/bld/chunks/{chunk-PAIFBNU4.js → chunk-C3AJ6WNB.js} +2 -2
- package/bld/chunks/{chunk-OQLJDJIF.js → chunk-TCMEEKWL.js} +2 -2
- package/bld/chunks/{chunk-ZMT6EFHL.js → chunk-VHSPHJ5F.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/cached.d.ts +5 -3
- package/bld/library/cached.d.ts.map +1 -1
- package/bld/library/index.d.ts +4 -4
- package/bld/library/index.d.ts.map +1 -1
- package/bld/library/index.js +17 -3
- package/bld/library/kernel-cache.d.ts +6 -3
- 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 +218 -82
- 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 +19 -5
- 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 +18 -10
- package/package.json +1 -1
- package/src/library/bound-solver.ts +103 -73
- package/src/library/cached.ts +29 -8
- package/src/library/index.ts +15 -4
- package/src/library/kernel-cache.ts +37 -5
- package/src/library/relation-solver.ts +194 -110
- package/src/library/runtime.ts +1046 -442
- package/src/library/sketch.ts +3 -3
- package/src/library/topology.ts +13 -2
- package/src/tooling/index.ts +15 -7
- package/bld/chunks/chunk-J77FMYFY.js.map +0 -7
- /package/bld/chunks/{chunk-PAIFBNU4.js.map → chunk-C3AJ6WNB.js.map} +0 -0
- /package/bld/chunks/{chunk-OQLJDJIF.js.map → chunk-TCMEEKWL.js.map} +0 -0
- /package/bld/chunks/{chunk-ZMT6EFHL.js.map → chunk-VHSPHJ5F.js.map} +0 -0
package/src/library/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export type {
|
|
1
|
+
export {cache} from './cached.js';
|
|
2
|
+
export type {CacheOptions} from './cached.js';
|
|
3
3
|
export {font, googleFont} from './font.js';
|
|
4
4
|
export type {Font} from './font.js';
|
|
5
5
|
export type {GoogleFontOptions} from './google-font.js';
|
|
@@ -16,6 +16,13 @@ export type {
|
|
|
16
16
|
} from './sketch.js';
|
|
17
17
|
|
|
18
18
|
export {
|
|
19
|
+
offset,
|
|
20
|
+
rotate,
|
|
21
|
+
pivot,
|
|
22
|
+
pivotVertex,
|
|
23
|
+
pivotPoint,
|
|
24
|
+
axisEdge,
|
|
25
|
+
axisLine,
|
|
19
26
|
arc,
|
|
20
27
|
bezier,
|
|
21
28
|
box,
|
|
@@ -45,8 +52,12 @@ export type {
|
|
|
45
52
|
Anchor,
|
|
46
53
|
Bound,
|
|
47
54
|
DirectionalBounds,
|
|
48
|
-
|
|
49
|
-
|
|
55
|
+
PivotChain,
|
|
56
|
+
PivotRotation,
|
|
57
|
+
AxisRotation,
|
|
58
|
+
AxisChain,
|
|
59
|
+
Transformation,
|
|
60
|
+
Relation,
|
|
50
61
|
CanonicalElements,
|
|
51
62
|
Constraint,
|
|
52
63
|
CurveElements,
|
|
@@ -40,6 +40,7 @@ export type KernelValueLifecycle<Value> = Readonly<{
|
|
|
40
40
|
}>;
|
|
41
41
|
|
|
42
42
|
type CacheEntry<Value> = Readonly<{
|
|
43
|
+
persistenceEligible: boolean;
|
|
43
44
|
estimatedBytes: number;
|
|
44
45
|
signature: string;
|
|
45
46
|
value: Value;
|
|
@@ -49,9 +50,11 @@ type CacheEntry<Value> = Readonly<{
|
|
|
49
50
|
|
|
50
51
|
export function createComputationCache({
|
|
51
52
|
maximumBytes = 2 * 1024 ** 3,
|
|
53
|
+
minimumPersistenceMilliseconds = 1,
|
|
52
54
|
nativeAllocatedBytes,
|
|
53
55
|
}: {
|
|
54
56
|
maximumBytes?: number;
|
|
57
|
+
minimumPersistenceMilliseconds?: number;
|
|
55
58
|
nativeAllocatedBytes: () => number;
|
|
56
59
|
}) {
|
|
57
60
|
const entries = new Map<string, CacheEntry<unknown>>();
|
|
@@ -71,6 +74,17 @@ export function createComputationCache({
|
|
|
71
74
|
const pendingPersistence = new Map<string, () => Uint8Array>();
|
|
72
75
|
let externalBytes = 0;
|
|
73
76
|
|
|
77
|
+
/** Adjust historical retention without invalidating the current model. */
|
|
78
|
+
function setKernelCacheBudget(bytes: number): void {
|
|
79
|
+
maximumBytes = bytes;
|
|
80
|
+
evictHistoricalEntries();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Apply to future computations without changing existing entries' admission. */
|
|
84
|
+
function setKernelCachePersistenceThreshold(milliseconds: number): void {
|
|
85
|
+
minimumPersistenceMilliseconds = milliseconds;
|
|
86
|
+
}
|
|
87
|
+
|
|
74
88
|
/** The host accounts for in-flight inputs and all auxiliary native heaps. */
|
|
75
89
|
function setKernelExternalBytes(bytes: number): void {
|
|
76
90
|
externalBytes = bytes;
|
|
@@ -128,8 +142,10 @@ export function createComputationCache({
|
|
|
128
142
|
): KernelArtifact<Value> {
|
|
129
143
|
const hit = findKernelOperation(key, lifecycle, codec);
|
|
130
144
|
if (hit) return hit;
|
|
145
|
+
const started = performance.now();
|
|
131
146
|
const value = compute();
|
|
132
|
-
|
|
147
|
+
const milliseconds = performance.now() - started;
|
|
148
|
+
acceptKernelOperation(key, lifecycle, value, milliseconds, codec);
|
|
133
149
|
return {id: key.id, value};
|
|
134
150
|
}
|
|
135
151
|
|
|
@@ -185,7 +201,9 @@ export function createComputationCache({
|
|
|
185
201
|
misses += 1;
|
|
186
202
|
return undefined;
|
|
187
203
|
}
|
|
188
|
-
|
|
204
|
+
// Existing disk records have already been admitted; reading one must
|
|
205
|
+
// not reinterpret decode time as its original computation cost.
|
|
206
|
+
cached = retainEntry(key, lifecycle, restored, true);
|
|
189
207
|
persistentHits += 1;
|
|
190
208
|
persisted.add(id);
|
|
191
209
|
}
|
|
@@ -198,7 +216,8 @@ export function createComputationCache({
|
|
|
198
216
|
throw new Error(`Kernel operation cache identity collision: ${id}`);
|
|
199
217
|
hits += 1;
|
|
200
218
|
const value = cached.instantiate(cached.value);
|
|
201
|
-
if (codec !== false)
|
|
219
|
+
if (cached.persistenceEligible && codec !== false)
|
|
220
|
+
persist(key, cached.value, codec, true);
|
|
202
221
|
touchEntry(id, cached as CacheEntry<unknown>);
|
|
203
222
|
return {id, value};
|
|
204
223
|
}
|
|
@@ -208,6 +227,7 @@ export function createComputationCache({
|
|
|
208
227
|
key: KernelOperationKey,
|
|
209
228
|
lifecycle: KernelValueLifecycle<Value>,
|
|
210
229
|
value: Value,
|
|
230
|
+
milliseconds: number,
|
|
211
231
|
codec?: CacheCodec<Value> | false,
|
|
212
232
|
): void {
|
|
213
233
|
const existing = entries.get(key.id);
|
|
@@ -224,8 +244,14 @@ export function createComputationCache({
|
|
|
224
244
|
lifecycle.release(value);
|
|
225
245
|
throw error;
|
|
226
246
|
}
|
|
227
|
-
const entry = retainEntry(
|
|
228
|
-
|
|
247
|
+
const entry = retainEntry(
|
|
248
|
+
key,
|
|
249
|
+
lifecycle,
|
|
250
|
+
retained,
|
|
251
|
+
milliseconds >= minimumPersistenceMilliseconds,
|
|
252
|
+
);
|
|
253
|
+
if (entry.persistenceEligible && codec !== false)
|
|
254
|
+
persist(key, retained, codec);
|
|
229
255
|
touchEntry(key.id, entry as CacheEntry<unknown>);
|
|
230
256
|
}
|
|
231
257
|
|
|
@@ -282,8 +308,10 @@ export function createComputationCache({
|
|
|
282
308
|
key: KernelOperationKey,
|
|
283
309
|
lifecycle: KernelValueLifecycle<Value>,
|
|
284
310
|
retained: Value,
|
|
311
|
+
persistenceEligible: boolean,
|
|
285
312
|
): CacheEntry<Value> {
|
|
286
313
|
const entry: CacheEntry<Value> = {
|
|
314
|
+
persistenceEligible,
|
|
287
315
|
estimatedBytes:
|
|
288
316
|
256 +
|
|
289
317
|
estimateRetainedBytes(key.signature) +
|
|
@@ -368,6 +396,8 @@ export function createComputationCache({
|
|
|
368
396
|
beginKernelOperationEvaluation,
|
|
369
397
|
clearKernelOperationCache,
|
|
370
398
|
kernelOperationCacheStats,
|
|
399
|
+
setKernelCacheBudget,
|
|
400
|
+
setKernelCachePersistenceThreshold,
|
|
371
401
|
setKernelArtifactStore,
|
|
372
402
|
findKernelOperation,
|
|
373
403
|
findKernelOperations,
|
|
@@ -388,6 +418,8 @@ export const {
|
|
|
388
418
|
beginKernelOperationEvaluation,
|
|
389
419
|
clearKernelOperationCache,
|
|
390
420
|
kernelOperationCacheStats,
|
|
421
|
+
setKernelCacheBudget,
|
|
422
|
+
setKernelCachePersistenceThreshold,
|
|
391
423
|
setKernelArtifactStore,
|
|
392
424
|
findKernelOperation,
|
|
393
425
|
findKernelOperations,
|
|
@@ -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);
|