@code3d/core 0.0.1-alpha.0
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/LICENSE +22 -0
- package/README.md +824 -0
- package/THIRD_PARTY.md +28 -0
- package/bld/library/alignment-geometry.d.ts +50 -0
- package/bld/library/alignment-geometry.d.ts.map +1 -0
- package/bld/library/alignment-geometry.js +322 -0
- package/bld/library/alignment-geometry.js.map +1 -0
- package/bld/library/bound-solver.d.ts +33 -0
- package/bld/library/bound-solver.d.ts.map +1 -0
- package/bld/library/bound-solver.js +252 -0
- package/bld/library/bound-solver.js.map +1 -0
- package/bld/library/extrude.d.ts +8 -0
- package/bld/library/extrude.d.ts.map +1 -0
- package/bld/library/extrude.js +28 -0
- package/bld/library/extrude.js.map +1 -0
- package/bld/library/index.d.ts +7 -0
- package/bld/library/index.d.ts.map +1 -0
- package/bld/library/index.js +3 -0
- package/bld/library/index.js.map +1 -0
- package/bld/library/kernel-artifact-codec.d.ts +3 -0
- package/bld/library/kernel-artifact-codec.d.ts.map +1 -0
- package/bld/library/kernel-artifact-codec.js +148 -0
- package/bld/library/kernel-artifact-codec.js.map +1 -0
- package/bld/library/kernel-cache.d.ts +63 -0
- package/bld/library/kernel-cache.d.ts.map +1 -0
- package/bld/library/kernel-cache.js +245 -0
- package/bld/library/kernel-cache.js.map +1 -0
- package/bld/library/kernel-shapes.d.ts +20 -0
- package/bld/library/kernel-shapes.d.ts.map +1 -0
- package/bld/library/kernel-shapes.js +108 -0
- package/bld/library/kernel-shapes.js.map +1 -0
- package/bld/library/loft.d.ts +7 -0
- package/bld/library/loft.d.ts.map +1 -0
- package/bld/library/loft.js +161 -0
- package/bld/library/loft.js.map +1 -0
- package/bld/library/material.d.ts +12 -0
- package/bld/library/material.d.ts.map +1 -0
- package/bld/library/material.js +176 -0
- package/bld/library/material.js.map +1 -0
- package/bld/library/model-color.d.ts +8 -0
- package/bld/library/model-color.d.ts.map +1 -0
- package/bld/library/model-color.js +36 -0
- package/bld/library/model-color.js.map +1 -0
- package/bld/library/open-cascade-error.d.ts +2 -0
- package/bld/library/open-cascade-error.d.ts.map +1 -0
- package/bld/library/open-cascade-error.js +22 -0
- package/bld/library/open-cascade-error.js.map +1 -0
- package/bld/library/relation-solver.d.ts +29 -0
- package/bld/library/relation-solver.d.ts.map +1 -0
- package/bld/library/relation-solver.js +258 -0
- package/bld/library/relation-solver.js.map +1 -0
- package/bld/library/replicad.d.ts +17 -0
- package/bld/library/replicad.d.ts.map +1 -0
- package/bld/library/replicad.js +24 -0
- package/bld/library/replicad.js.map +1 -0
- package/bld/library/retained-memory.d.ts +7 -0
- package/bld/library/retained-memory.d.ts.map +1 -0
- package/bld/library/retained-memory.js +27 -0
- package/bld/library/retained-memory.js.map +1 -0
- package/bld/library/runtime.d.ts +775 -0
- package/bld/library/runtime.d.ts.map +1 -0
- package/bld/library/runtime.js +2954 -0
- package/bld/library/runtime.js.map +1 -0
- package/bld/library/shell.d.ts +7 -0
- package/bld/library/shell.d.ts.map +1 -0
- package/bld/library/shell.js +221 -0
- package/bld/library/shell.js.map +1 -0
- package/bld/library/sketch-curve-intersections.d.ts +13 -0
- package/bld/library/sketch-curve-intersections.d.ts.map +1 -0
- package/bld/library/sketch-curve-intersections.js +117 -0
- package/bld/library/sketch-curve-intersections.js.map +1 -0
- package/bld/library/sketch-curves.d.ts +31 -0
- package/bld/library/sketch-curves.d.ts.map +1 -0
- package/bld/library/sketch-curves.js +89 -0
- package/bld/library/sketch-curves.js.map +1 -0
- package/bld/library/sketch-drag-rules.d.ts +35 -0
- package/bld/library/sketch-drag-rules.d.ts.map +1 -0
- package/bld/library/sketch-drag-rules.js +522 -0
- package/bld/library/sketch-drag-rules.js.map +1 -0
- package/bld/library/sketch-face.d.ts +4 -0
- package/bld/library/sketch-face.d.ts.map +1 -0
- package/bld/library/sketch-face.js +72 -0
- package/bld/library/sketch-face.js.map +1 -0
- package/bld/library/sketch-incidence.d.ts +26 -0
- package/bld/library/sketch-incidence.d.ts.map +1 -0
- package/bld/library/sketch-incidence.js +114 -0
- package/bld/library/sketch-incidence.js.map +1 -0
- package/bld/library/sketch-precision.d.ts +19 -0
- package/bld/library/sketch-precision.d.ts.map +1 -0
- package/bld/library/sketch-precision.js +55 -0
- package/bld/library/sketch-precision.js.map +1 -0
- package/bld/library/sketch-regions.d.ts +12 -0
- package/bld/library/sketch-regions.d.ts.map +1 -0
- package/bld/library/sketch-regions.js +193 -0
- package/bld/library/sketch-regions.js.map +1 -0
- package/bld/library/sketch-solver.d.ts +97 -0
- package/bld/library/sketch-solver.d.ts.map +1 -0
- package/bld/library/sketch-solver.js +578 -0
- package/bld/library/sketch-solver.js.map +1 -0
- package/bld/library/sketch.d.ts +142 -0
- package/bld/library/sketch.d.ts.map +1 -0
- package/bld/library/sketch.js +650 -0
- package/bld/library/sketch.js.map +1 -0
- package/bld/library/spatial.d.ts +28 -0
- package/bld/library/spatial.d.ts.map +1 -0
- package/bld/library/spatial.js +195 -0
- package/bld/library/spatial.js.map +1 -0
- package/bld/library/three.d.ts +3 -0
- package/bld/library/three.d.ts.map +1 -0
- package/bld/library/three.js +3 -0
- package/bld/library/three.js.map +1 -0
- package/bld/library/topology-id.d.ts +24 -0
- package/bld/library/topology-id.d.ts.map +1 -0
- package/bld/library/topology-id.js +67 -0
- package/bld/library/topology-id.js.map +1 -0
- package/bld/library/topology-inspection.d.ts +64 -0
- package/bld/library/topology-inspection.d.ts.map +1 -0
- package/bld/library/topology-inspection.js +267 -0
- package/bld/library/topology-inspection.js.map +1 -0
- package/bld/library/topology.d.ts +94 -0
- package/bld/library/topology.d.ts.map +1 -0
- package/bld/library/topology.js +455 -0
- package/bld/library/topology.js.map +1 -0
- package/bld/node/index.d.ts +2 -0
- package/bld/node/index.d.ts.map +1 -0
- package/bld/node/index.js +14 -0
- package/bld/node/index.js.map +1 -0
- package/bld/node/replicad.d.ts +3 -0
- package/bld/node/replicad.d.ts.map +1 -0
- package/bld/node/replicad.js +3 -0
- package/bld/node/replicad.js.map +1 -0
- package/bld/tooling/index.d.ts +26 -0
- package/bld/tooling/index.d.ts.map +1 -0
- package/bld/tooling/index.js +19 -0
- package/bld/tooling/index.js.map +1 -0
- package/package.json +53 -0
- package/src/library/alignment-geometry.ts +431 -0
- package/src/library/bound-solver.ts +410 -0
- package/src/library/extrude.ts +38 -0
- package/src/library/index.ts +88 -0
- package/src/library/kernel-artifact-codec.ts +166 -0
- package/src/library/kernel-cache.ts +337 -0
- package/src/library/kernel-shapes.ts +135 -0
- package/src/library/loft.ts +196 -0
- package/src/library/material.ts +228 -0
- package/src/library/model-color.ts +47 -0
- package/src/library/open-cascade-error.ts +26 -0
- package/src/library/relation-solver.ts +479 -0
- package/src/library/replicad.ts +40 -0
- package/src/library/retained-memory.ts +23 -0
- package/src/library/runtime.ts +5146 -0
- package/src/library/shell.ts +281 -0
- package/src/library/sketch-curve-intersections.ts +154 -0
- package/src/library/sketch-curves.ts +161 -0
- package/src/library/sketch-drag-rules.ts +767 -0
- package/src/library/sketch-face.ts +89 -0
- package/src/library/sketch-incidence.ts +204 -0
- package/src/library/sketch-precision.ts +81 -0
- package/src/library/sketch-regions.ts +271 -0
- package/src/library/sketch-solver.ts +973 -0
- package/src/library/sketch.ts +977 -0
- package/src/library/spatial.ts +254 -0
- package/src/library/three.ts +2 -0
- package/src/library/topology-id.ts +81 -0
- package/src/library/topology-inspection.ts +368 -0
- package/src/library/topology.ts +776 -0
- package/src/library/tsconfig.json +11 -0
- package/src/node/index.ts +20 -0
- package/src/node/replicad.ts +3 -0
- package/src/node/tsconfig.json +16 -0
- package/src/tooling/index.ts +160 -0
- package/src/tooling/tsconfig.json +15 -0
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
import {
|
|
2
|
+
addVectors,
|
|
3
|
+
composeTransforms,
|
|
4
|
+
identityRigidTransform,
|
|
5
|
+
invertTransform,
|
|
6
|
+
negateVector,
|
|
7
|
+
origin,
|
|
8
|
+
rotateVector,
|
|
9
|
+
rotation,
|
|
10
|
+
transformsAreEquivalent,
|
|
11
|
+
type Quaternion,
|
|
12
|
+
type RigidTransform,
|
|
13
|
+
type Vec3,
|
|
14
|
+
} from './spatial.js';
|
|
15
|
+
|
|
16
|
+
export type Bounds = readonly [minimum: Vec3, maximum: Vec3];
|
|
17
|
+
export type BodyRotation =
|
|
18
|
+
| Readonly<{local: RigidTransform}>
|
|
19
|
+
| Readonly<{axis: RigidTransform; body: number; angle: number}>;
|
|
20
|
+
|
|
21
|
+
export type BodyRelation = Readonly<{
|
|
22
|
+
id: string;
|
|
23
|
+
source: Readonly<{
|
|
24
|
+
body: number;
|
|
25
|
+
key: string;
|
|
26
|
+
bounds: (orientation: Quaternion) => Bounds;
|
|
27
|
+
}>;
|
|
28
|
+
target: Readonly<{body: number; transform: RigidTransform; facing: 1 | -1}>;
|
|
29
|
+
offset: Vec3 | undefined;
|
|
30
|
+
rotations: readonly BodyRotation[];
|
|
31
|
+
}>;
|
|
32
|
+
|
|
33
|
+
export type Body = Readonly<{name: string; relations: readonly BodyRelation[]}>;
|
|
34
|
+
type AffinePoint = {constant: Vec3; columns: Vec3[]};
|
|
35
|
+
type AffinePose = {position: AffinePoint; quaternion: Quaternion};
|
|
36
|
+
type Equation = {id: string; coefficients: number[]; value: number};
|
|
37
|
+
const axes: readonly Vec3[] = [
|
|
38
|
+
[1, 0, 0],
|
|
39
|
+
[0, 1, 0],
|
|
40
|
+
[0, 0, 1],
|
|
41
|
+
];
|
|
42
|
+
const dot = (a: Vec3, b: Vec3): number =>
|
|
43
|
+
a.reduce((sum, x, i) => sum + x * b[i], 0);
|
|
44
|
+
const scale = (v: Vec3, s: number): Vec3 => [v[0] * s, v[1] * s, v[2] * s];
|
|
45
|
+
|
|
46
|
+
/** A rotation about a positioned, directed line. Angles are in degrees. */
|
|
47
|
+
export function axisRotation(
|
|
48
|
+
axis: RigidTransform,
|
|
49
|
+
angle: number,
|
|
50
|
+
): RigidTransform {
|
|
51
|
+
const direction = rotateVector([0, 1, 0], axis.quaternion);
|
|
52
|
+
const half = (angle * Math.PI) / 360;
|
|
53
|
+
const q: Quaternion = [...scale(direction, Math.sin(half)), Math.cos(half)];
|
|
54
|
+
return {
|
|
55
|
+
quaternion: q,
|
|
56
|
+
position: addVectors(
|
|
57
|
+
axis.position,
|
|
58
|
+
negateVector(rotateVector(axis.position, q)),
|
|
59
|
+
),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Explicit rotations determine orientation; contacts solve translations only. */
|
|
64
|
+
export function solveBodies(
|
|
65
|
+
bodies: readonly Body[],
|
|
66
|
+
): readonly RigidTransform[] {
|
|
67
|
+
const seeds = new Map<number, RigidTransform>();
|
|
68
|
+
const visiting = new Set<number>();
|
|
69
|
+
const seed = (index: number): RigidTransform => {
|
|
70
|
+
const known = seeds.get(index);
|
|
71
|
+
if (known) return known;
|
|
72
|
+
if (visiting.has(index))
|
|
73
|
+
throw new Error(
|
|
74
|
+
'Cyclic external rotation axes cannot determine an authored orientation.',
|
|
75
|
+
);
|
|
76
|
+
visiting.add(index);
|
|
77
|
+
const candidates = bodies[index].relations
|
|
78
|
+
.filter(r => r.rotations.length)
|
|
79
|
+
.map(relation => {
|
|
80
|
+
let pose = identityRigidTransform;
|
|
81
|
+
for (const action of relation.rotations) {
|
|
82
|
+
pose =
|
|
83
|
+
'local' in action
|
|
84
|
+
? composeTransforms(pose, action.local)
|
|
85
|
+
: composeTransforms(
|
|
86
|
+
axisRotation(
|
|
87
|
+
composeTransforms(seed(action.body), action.axis),
|
|
88
|
+
action.angle,
|
|
89
|
+
),
|
|
90
|
+
pose,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
return pose;
|
|
94
|
+
});
|
|
95
|
+
const pose = candidates[0] ?? identityRigidTransform;
|
|
96
|
+
if (
|
|
97
|
+
candidates.some(
|
|
98
|
+
candidate =>
|
|
99
|
+
!transformsAreEquivalent(
|
|
100
|
+
rotation(candidate.quaternion),
|
|
101
|
+
rotation(pose.quaternion),
|
|
102
|
+
),
|
|
103
|
+
)
|
|
104
|
+
) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
`Conflicting explicit rotations for ${bodies[index].name}.`,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
visiting.delete(index);
|
|
110
|
+
seeds.set(index, pose);
|
|
111
|
+
return pose;
|
|
112
|
+
};
|
|
113
|
+
bodies.forEach((_, index) => seed(index));
|
|
114
|
+
const variables = bodies.flatMap((body, index) =>
|
|
115
|
+
body.relations.length ? [index] : [],
|
|
116
|
+
);
|
|
117
|
+
const dimension = variables.length * 3;
|
|
118
|
+
const positioned = new Map<number, AffinePose>();
|
|
119
|
+
const positionBody = (index: number): AffinePose => {
|
|
120
|
+
const known = positioned.get(index);
|
|
121
|
+
if (known) return known;
|
|
122
|
+
let pose: AffinePose = {
|
|
123
|
+
quaternion: identityRigidTransform.quaternion,
|
|
124
|
+
position: {
|
|
125
|
+
constant: origin,
|
|
126
|
+
columns: variables.flatMap(body =>
|
|
127
|
+
axes.map(axis => (body === index ? axis : origin)),
|
|
128
|
+
),
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
const actions =
|
|
132
|
+
bodies[index].relations.find(relation => relation.rotations.length)
|
|
133
|
+
?.rotations ?? [];
|
|
134
|
+
for (const action of actions) {
|
|
135
|
+
if ('local' in action) {
|
|
136
|
+
pose = {
|
|
137
|
+
position: shiftPoint(
|
|
138
|
+
pose.position,
|
|
139
|
+
rotateVector(action.local.position, pose.quaternion),
|
|
140
|
+
),
|
|
141
|
+
quaternion: composeTransforms(
|
|
142
|
+
rotation(pose.quaternion),
|
|
143
|
+
rotation(action.local.quaternion),
|
|
144
|
+
).quaternion,
|
|
145
|
+
};
|
|
146
|
+
} else {
|
|
147
|
+
const axisPose = positionBody(action.body);
|
|
148
|
+
const axis = composeTransforms(
|
|
149
|
+
rotation(axisPose.quaternion),
|
|
150
|
+
action.axis,
|
|
151
|
+
);
|
|
152
|
+
const q = axisRotation(axis, action.angle).quaternion;
|
|
153
|
+
const axisPoint = shiftPoint(axisPose.position, axis.position);
|
|
154
|
+
const rotatedAxis = rotatePoint(axisPoint, q);
|
|
155
|
+
pose = {
|
|
156
|
+
position: addPoints(
|
|
157
|
+
rotatePoint(pose.position, q),
|
|
158
|
+
addPoints(axisPoint, {
|
|
159
|
+
constant: negateVector(rotatedAxis.constant),
|
|
160
|
+
columns: rotatedAxis.columns.map(negateVector),
|
|
161
|
+
}),
|
|
162
|
+
),
|
|
163
|
+
quaternion: composeTransforms(rotation(q), rotation(pose.quaternion))
|
|
164
|
+
.quaternion,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
positioned.set(index, pose);
|
|
169
|
+
return pose;
|
|
170
|
+
};
|
|
171
|
+
const poses = bodies.map((_, index) => positionBody(index));
|
|
172
|
+
const equations: Equation[] = [];
|
|
173
|
+
const preferences = new Map<string, Equation>();
|
|
174
|
+
bodies.forEach((body, owner) =>
|
|
175
|
+
body.relations.forEach(relation => {
|
|
176
|
+
// A chain describes contact followed by its explicit rotations. Invert only
|
|
177
|
+
// this chain; every other relation still sees and constrains the final pose.
|
|
178
|
+
const baseline = undoRotations(poses[owner], relation.rotations, poses);
|
|
179
|
+
// All authored contact stages contribute equally to the free-position
|
|
180
|
+
// choice. Deduplicate identical stages so repeated conditions add no bias.
|
|
181
|
+
for (let axis = 0; axis < 3; axis++) {
|
|
182
|
+
const preference = {
|
|
183
|
+
id: relation.id,
|
|
184
|
+
coefficients: baseline.position.columns.map(column => column[axis]),
|
|
185
|
+
value: -baseline.position.constant[axis],
|
|
186
|
+
};
|
|
187
|
+
preferences.set(
|
|
188
|
+
JSON.stringify([preference.coefficients, preference.value]),
|
|
189
|
+
preference,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
const source =
|
|
193
|
+
relation.source.body === owner ? baseline : poses[relation.source.body];
|
|
194
|
+
const target =
|
|
195
|
+
relation.target.body === owner ? baseline : poses[relation.target.body];
|
|
196
|
+
const targetFrame = composeTransforms(
|
|
197
|
+
rotation(target.quaternion),
|
|
198
|
+
relation.target.transform,
|
|
199
|
+
);
|
|
200
|
+
const inverseFrame = invertTransform(rotation(targetFrame.quaternion));
|
|
201
|
+
const relativeOrientation = composeTransforms(
|
|
202
|
+
inverseFrame,
|
|
203
|
+
rotation(source.quaternion),
|
|
204
|
+
).quaternion;
|
|
205
|
+
const bounds = relation.source.bounds(relativeOrientation);
|
|
206
|
+
const center: Vec3 = [
|
|
207
|
+
(bounds[0][0] + bounds[1][0]) / 2,
|
|
208
|
+
bounds[relation.target.facing === 1 ? 0 : 1][1],
|
|
209
|
+
(bounds[0][2] + bounds[1][2]) / 2,
|
|
210
|
+
];
|
|
211
|
+
const sourcePoint = shiftPoint(
|
|
212
|
+
source.position,
|
|
213
|
+
rotateVector(center, targetFrame.quaternion),
|
|
214
|
+
);
|
|
215
|
+
const targetPoint = shiftPoint(
|
|
216
|
+
target.position,
|
|
217
|
+
addVectors(
|
|
218
|
+
targetFrame.position,
|
|
219
|
+
rotateVector(relation.offset ?? origin, targetFrame.quaternion),
|
|
220
|
+
),
|
|
221
|
+
);
|
|
222
|
+
for (const localAxis of relation.offset ? axes : [axes[1]]) {
|
|
223
|
+
const normal = rotateVector(localAxis, targetFrame.quaternion);
|
|
224
|
+
equations.push({
|
|
225
|
+
id: relation.id,
|
|
226
|
+
coefficients: sourcePoint.columns.map((column, i) =>
|
|
227
|
+
dot(
|
|
228
|
+
addVectors(column, negateVector(targetPoint.columns[i])),
|
|
229
|
+
normal,
|
|
230
|
+
),
|
|
231
|
+
),
|
|
232
|
+
value: dot(
|
|
233
|
+
addVectors(
|
|
234
|
+
targetPoint.constant,
|
|
235
|
+
negateVector(sourcePoint.constant),
|
|
236
|
+
),
|
|
237
|
+
normal,
|
|
238
|
+
),
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}),
|
|
242
|
+
);
|
|
243
|
+
const solution = solveTranslations(
|
|
244
|
+
equations,
|
|
245
|
+
[...preferences.values()],
|
|
246
|
+
dimension,
|
|
247
|
+
);
|
|
248
|
+
return poses.map(pose => ({
|
|
249
|
+
quaternion: pose.quaternion,
|
|
250
|
+
position: pose.position.columns.reduce(
|
|
251
|
+
(point, column, i) => addVectors(point, scale(column, solution[i])),
|
|
252
|
+
pose.position.constant,
|
|
253
|
+
),
|
|
254
|
+
}));
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function shiftPoint(point: AffinePoint, offset: Vec3): AffinePoint {
|
|
258
|
+
return {...point, constant: addVectors(point.constant, offset)};
|
|
259
|
+
}
|
|
260
|
+
function rotatePoint(point: AffinePoint, quaternion: Quaternion): AffinePoint {
|
|
261
|
+
return {
|
|
262
|
+
constant: rotateVector(point.constant, quaternion),
|
|
263
|
+
columns: point.columns.map(column => rotateVector(column, quaternion)),
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
function addPoints(left: AffinePoint, right: AffinePoint): AffinePoint {
|
|
267
|
+
return {
|
|
268
|
+
constant: addVectors(left.constant, right.constant),
|
|
269
|
+
columns: left.columns.map((column, i) =>
|
|
270
|
+
addVectors(column, right.columns[i]),
|
|
271
|
+
),
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function undoRotations(
|
|
276
|
+
pose: AffinePose,
|
|
277
|
+
actions: readonly BodyRotation[],
|
|
278
|
+
poses: readonly AffinePose[],
|
|
279
|
+
): AffinePose {
|
|
280
|
+
for (const action of [...actions].reverse()) {
|
|
281
|
+
if ('local' in action) {
|
|
282
|
+
const inverse = invertTransform(action.local);
|
|
283
|
+
pose = {
|
|
284
|
+
position: shiftPoint(
|
|
285
|
+
pose.position,
|
|
286
|
+
rotateVector(inverse.position, pose.quaternion),
|
|
287
|
+
),
|
|
288
|
+
quaternion: composeTransforms(
|
|
289
|
+
rotation(pose.quaternion),
|
|
290
|
+
rotation(inverse.quaternion),
|
|
291
|
+
).quaternion,
|
|
292
|
+
};
|
|
293
|
+
} else {
|
|
294
|
+
const axisPose = poses[action.body];
|
|
295
|
+
const axis = composeTransforms(
|
|
296
|
+
rotation(axisPose.quaternion),
|
|
297
|
+
action.axis,
|
|
298
|
+
);
|
|
299
|
+
const q = axisRotation(axis, -action.angle).quaternion;
|
|
300
|
+
const axisPoint = shiftPoint(axisPose.position, axis.position);
|
|
301
|
+
const rotatedAxis = rotatePoint(axisPoint, q);
|
|
302
|
+
pose = {
|
|
303
|
+
position: addPoints(
|
|
304
|
+
rotatePoint(pose.position, q),
|
|
305
|
+
addPoints(axisPoint, {
|
|
306
|
+
constant: negateVector(rotatedAxis.constant),
|
|
307
|
+
columns: rotatedAxis.columns.map(negateVector),
|
|
308
|
+
}),
|
|
309
|
+
),
|
|
310
|
+
quaternion: composeTransforms(rotation(q), rotation(pose.quaternion))
|
|
311
|
+
.quaternion,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return pose;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** Solve Ax=b, then minimize contact-stage displacement in its nullspace. */
|
|
319
|
+
function solveTranslations(
|
|
320
|
+
equations: readonly Equation[],
|
|
321
|
+
preferences: readonly Equation[],
|
|
322
|
+
dimension: number,
|
|
323
|
+
): number[] {
|
|
324
|
+
const basis: {row: number[]; value: number}[] = [];
|
|
325
|
+
const magnitude = Math.max(
|
|
326
|
+
1,
|
|
327
|
+
...equations.map(equation => Math.abs(equation.value)),
|
|
328
|
+
);
|
|
329
|
+
for (const equation of equations) {
|
|
330
|
+
const row = [...equation.coefficients];
|
|
331
|
+
let value = equation.value;
|
|
332
|
+
for (let pass = 0; pass < 2; pass++) {
|
|
333
|
+
for (const previous of basis) {
|
|
334
|
+
const projection = row.reduce(
|
|
335
|
+
(sum, x, i) => sum + x * previous.row[i],
|
|
336
|
+
0,
|
|
337
|
+
);
|
|
338
|
+
row.forEach((x, i) => {
|
|
339
|
+
row[i] = x - projection * previous.row[i];
|
|
340
|
+
});
|
|
341
|
+
value -= projection * previous.value;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
const length = Math.hypot(...row);
|
|
345
|
+
if (length < 1e-10) {
|
|
346
|
+
if (Math.abs(value) > 1e-7 * magnitude)
|
|
347
|
+
throw new Error(
|
|
348
|
+
`Conflicting bound positions (${equation.id}). on() only translates; adjust the position or an explicit rotation.`,
|
|
349
|
+
);
|
|
350
|
+
} else basis.push({row: row.map(x => x / length), value: value / length});
|
|
351
|
+
}
|
|
352
|
+
const particular = Array.from({length: dimension}, (_, i) =>
|
|
353
|
+
basis.reduce((sum, entry) => sum + entry.row[i] * entry.value, 0),
|
|
354
|
+
);
|
|
355
|
+
const nullspace: number[][] = [];
|
|
356
|
+
for (let axis = 0; axis < dimension; axis++) {
|
|
357
|
+
const vector = Array.from({length: dimension}, (_, i) => +(axis === i));
|
|
358
|
+
for (let pass = 0; pass < 2; pass++)
|
|
359
|
+
for (const row of [...basis.map(entry => entry.row), ...nullspace]) {
|
|
360
|
+
const projection = vector.reduce(
|
|
361
|
+
(sum, value, i) => sum + value * row[i],
|
|
362
|
+
0,
|
|
363
|
+
);
|
|
364
|
+
vector.forEach((value, i) => {
|
|
365
|
+
vector[i] = value - projection * row[i];
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
const length = Math.hypot(...vector);
|
|
369
|
+
if (length > 1e-10) nullspace.push(vector.map(value => value / length));
|
|
370
|
+
}
|
|
371
|
+
if (!nullspace.length) return particular;
|
|
372
|
+
const project = (row: readonly number[], column: readonly number[]) =>
|
|
373
|
+
row.reduce((sum, value, i) => sum + value * column[i], 0);
|
|
374
|
+
const columns = nullspace.map(vector =>
|
|
375
|
+
preferences.map(preference => project(preference.coefficients, vector)),
|
|
376
|
+
);
|
|
377
|
+
const residual = preferences.map(
|
|
378
|
+
preference =>
|
|
379
|
+
preference.value - project(preference.coefficients, particular),
|
|
380
|
+
);
|
|
381
|
+
const orthogonal: number[][] = [];
|
|
382
|
+
const upper = columns.map(() => columns.map(() => 0));
|
|
383
|
+
const rhs: number[] = [];
|
|
384
|
+
columns.forEach((column, index) => {
|
|
385
|
+
const vector = [...column];
|
|
386
|
+
for (let pass = 0; pass < 2; pass++)
|
|
387
|
+
orthogonal.forEach((previous, i) => {
|
|
388
|
+
const projection = project(previous, vector);
|
|
389
|
+
upper[i][index] += projection;
|
|
390
|
+
vector.forEach((value, j) => {
|
|
391
|
+
vector[j] = value - projection * previous[j];
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
const length = Math.hypot(...vector);
|
|
395
|
+
upper[index][index] = length;
|
|
396
|
+
const normalized = vector.map(value => value / length);
|
|
397
|
+
orthogonal.push(normalized);
|
|
398
|
+
rhs.push(project(normalized, residual));
|
|
399
|
+
});
|
|
400
|
+
const free = [...rhs];
|
|
401
|
+
for (let i = free.length - 1; i >= 0; i--) {
|
|
402
|
+
for (let j = i + 1; j < free.length; j++) free[i] -= upper[i][j] * free[j];
|
|
403
|
+
free[i] /= upper[i][i];
|
|
404
|
+
}
|
|
405
|
+
return particular.map(
|
|
406
|
+
(value, i) =>
|
|
407
|
+
value +
|
|
408
|
+
nullspace.reduce((sum, vector, j) => sum + vector[i] * free[j], 0),
|
|
409
|
+
);
|
|
410
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import {getOC, Vector, type Shape3D} from 'replicad';
|
|
2
|
+
import {castOwnedShape3D} from './kernel-shapes.js';
|
|
3
|
+
import {
|
|
4
|
+
transferShapeTopology,
|
|
5
|
+
type TopologyInput,
|
|
6
|
+
type ShapeTopology,
|
|
7
|
+
} from './topology.js';
|
|
8
|
+
import type {Vec3} from './spatial.js';
|
|
9
|
+
|
|
10
|
+
export function extrudeWithTopology(
|
|
11
|
+
source: TopologyInput,
|
|
12
|
+
direction: Vec3,
|
|
13
|
+
): {shape: Shape3D; topology: ShapeTopology} {
|
|
14
|
+
const oc = getOC();
|
|
15
|
+
const vector = new Vector([...direction]);
|
|
16
|
+
let shape: Shape3D | undefined;
|
|
17
|
+
try {
|
|
18
|
+
const builder = new oc.BRepPrimAPI_MakePrism(
|
|
19
|
+
source.shape.wrapped,
|
|
20
|
+
vector.wrapped,
|
|
21
|
+
false,
|
|
22
|
+
true,
|
|
23
|
+
);
|
|
24
|
+
try {
|
|
25
|
+
if (!builder.IsDone())
|
|
26
|
+
throw new Error('Could not extrude the face into a solid.');
|
|
27
|
+
shape = castOwnedShape3D(builder.Shape());
|
|
28
|
+
return {shape, topology: transferShapeTopology([source], shape, builder)};
|
|
29
|
+
} finally {
|
|
30
|
+
builder.delete();
|
|
31
|
+
}
|
|
32
|
+
} catch (error) {
|
|
33
|
+
shape?.delete();
|
|
34
|
+
throw error;
|
|
35
|
+
} finally {
|
|
36
|
+
vector.delete();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
export {sketch} from './sketch.js';
|
|
2
|
+
export type {
|
|
3
|
+
Sketch,
|
|
4
|
+
SketchEntry,
|
|
5
|
+
SketchConstraint,
|
|
6
|
+
SketchOptions,
|
|
7
|
+
SketchPoint,
|
|
8
|
+
SketchPosition,
|
|
9
|
+
SketchArcDirection,
|
|
10
|
+
} from './sketch.js';
|
|
11
|
+
|
|
12
|
+
export {
|
|
13
|
+
arc,
|
|
14
|
+
bezier,
|
|
15
|
+
box,
|
|
16
|
+
circle,
|
|
17
|
+
coil,
|
|
18
|
+
cut,
|
|
19
|
+
cylinder,
|
|
20
|
+
ellipse,
|
|
21
|
+
extrude,
|
|
22
|
+
frustum,
|
|
23
|
+
group,
|
|
24
|
+
intersect,
|
|
25
|
+
line,
|
|
26
|
+
loft,
|
|
27
|
+
point,
|
|
28
|
+
rectangle,
|
|
29
|
+
regularPolygon,
|
|
30
|
+
regularPrism,
|
|
31
|
+
spline,
|
|
32
|
+
sphere,
|
|
33
|
+
tube,
|
|
34
|
+
union,
|
|
35
|
+
} from './runtime.js';
|
|
36
|
+
|
|
37
|
+
export type {
|
|
38
|
+
Anchor,
|
|
39
|
+
Bound,
|
|
40
|
+
DirectionalBounds,
|
|
41
|
+
ConstraintPivotChain,
|
|
42
|
+
ConstraintAroundChain,
|
|
43
|
+
CanonicalElements,
|
|
44
|
+
Constraint,
|
|
45
|
+
CurveElements,
|
|
46
|
+
Edge,
|
|
47
|
+
EdgeModel,
|
|
48
|
+
EdgeTopologyCapabilities,
|
|
49
|
+
ElementKind,
|
|
50
|
+
ElementSources,
|
|
51
|
+
ExposedElements,
|
|
52
|
+
ExposedValue,
|
|
53
|
+
FaceAnchor,
|
|
54
|
+
FaceModel,
|
|
55
|
+
GeometryCapabilities,
|
|
56
|
+
GeometryQueryCapabilities,
|
|
57
|
+
GroupModel,
|
|
58
|
+
LineAnchor,
|
|
59
|
+
LoftOptions,
|
|
60
|
+
MergedElements,
|
|
61
|
+
Model,
|
|
62
|
+
ModelCapabilities,
|
|
63
|
+
ModelElementKind,
|
|
64
|
+
ModelFamily,
|
|
65
|
+
ModelFamilyElementKind,
|
|
66
|
+
ModelForFamily,
|
|
67
|
+
ModelGeometryKind,
|
|
68
|
+
ModelKind,
|
|
69
|
+
NamedElements,
|
|
70
|
+
PlanarElements,
|
|
71
|
+
PointAnchor,
|
|
72
|
+
Solid,
|
|
73
|
+
SolidModel,
|
|
74
|
+
SolidModificationCapabilities,
|
|
75
|
+
Surface,
|
|
76
|
+
SurfaceTopologyCapabilities,
|
|
77
|
+
Vertex,
|
|
78
|
+
VertexModel,
|
|
79
|
+
VertexTopologyCapabilities,
|
|
80
|
+
} from './runtime.js';
|
|
81
|
+
export type {Vec3} from './spatial.js';
|
|
82
|
+
export type {
|
|
83
|
+
EdgeId,
|
|
84
|
+
SurfaceId,
|
|
85
|
+
TopologyId,
|
|
86
|
+
TopologyKind,
|
|
87
|
+
VertexId,
|
|
88
|
+
} from './topology.js';
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import {getOC, Shape, type AnyShape} from 'replicad';
|
|
2
|
+
import {castOwnedShape} from './kernel-shapes.js';
|
|
3
|
+
|
|
4
|
+
const encoder = new TextEncoder();
|
|
5
|
+
const decoder = new TextDecoder('utf-8', {fatal: true});
|
|
6
|
+
|
|
7
|
+
// Every node has an explicit tag, so authored keys cannot collide with binary
|
|
8
|
+
// references. Native handles never escape their runtime; only BinTools bytes do.
|
|
9
|
+
type Node = null | boolean | string | number | readonly [string, unknown];
|
|
10
|
+
|
|
11
|
+
export function encodeKernelArtifact(
|
|
12
|
+
signature: string,
|
|
13
|
+
value: unknown,
|
|
14
|
+
): Uint8Array {
|
|
15
|
+
const chunks: Uint8Array[] = [];
|
|
16
|
+
let offset = 0;
|
|
17
|
+
function binary(kind: string, bytes: Uint8Array): Node {
|
|
18
|
+
const node: Node = [kind, [offset, bytes.byteLength]];
|
|
19
|
+
chunks.push(bytes);
|
|
20
|
+
offset += bytes.byteLength;
|
|
21
|
+
return node;
|
|
22
|
+
}
|
|
23
|
+
function encode(value: unknown): Node {
|
|
24
|
+
if (value === undefined) return ['undefined', null];
|
|
25
|
+
if (typeof value === 'number') {
|
|
26
|
+
if (Object.is(value, -0)) return ['number', '-0'];
|
|
27
|
+
return Number.isFinite(value) ? value : ['number', String(value)];
|
|
28
|
+
}
|
|
29
|
+
if (
|
|
30
|
+
value === null ||
|
|
31
|
+
typeof value === 'string' ||
|
|
32
|
+
typeof value === 'boolean'
|
|
33
|
+
)
|
|
34
|
+
return value;
|
|
35
|
+
if (value instanceof Shape)
|
|
36
|
+
return binary('shape', encodeShape(value as AnyShape));
|
|
37
|
+
if (value instanceof Float32Array || value instanceof Uint32Array) {
|
|
38
|
+
return binary(
|
|
39
|
+
value instanceof Float32Array ? 'float32' : 'uint32',
|
|
40
|
+
new Uint8Array(value.buffer, value.byteOffset, value.byteLength),
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
if (Array.isArray(value)) return ['array', value.map(encode)];
|
|
44
|
+
if (
|
|
45
|
+
typeof value === 'object' &&
|
|
46
|
+
Object.getPrototypeOf(value) === Object.prototype
|
|
47
|
+
) {
|
|
48
|
+
return [
|
|
49
|
+
'object',
|
|
50
|
+
Object.entries(value).map(([key, item]) => [key, encode(item)]),
|
|
51
|
+
];
|
|
52
|
+
}
|
|
53
|
+
throw new Error('Unsupported kernel artifact value');
|
|
54
|
+
}
|
|
55
|
+
const metadata = encoder.encode(JSON.stringify([signature, encode(value)]));
|
|
56
|
+
const bytes = new Uint8Array(4 + metadata.byteLength + offset);
|
|
57
|
+
new DataView(bytes.buffer).setUint32(0, metadata.byteLength, true);
|
|
58
|
+
bytes.set(metadata, 4);
|
|
59
|
+
offset = 4 + metadata.byteLength;
|
|
60
|
+
for (const chunk of chunks) {
|
|
61
|
+
bytes.set(chunk, offset);
|
|
62
|
+
offset += chunk.byteLength;
|
|
63
|
+
}
|
|
64
|
+
return bytes;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function decodeKernelArtifact<Value>(
|
|
68
|
+
bytes: Uint8Array,
|
|
69
|
+
signature: string,
|
|
70
|
+
): Value {
|
|
71
|
+
const metadataLength = new DataView(
|
|
72
|
+
bytes.buffer,
|
|
73
|
+
bytes.byteOffset,
|
|
74
|
+
bytes.byteLength,
|
|
75
|
+
).getUint32(0, true);
|
|
76
|
+
const start = 4 + metadataLength;
|
|
77
|
+
if (start > bytes.byteLength) throw new Error('Truncated kernel artifact');
|
|
78
|
+
const [storedSignature, tree] = JSON.parse(
|
|
79
|
+
decoder.decode(bytes.subarray(4, start)),
|
|
80
|
+
);
|
|
81
|
+
if (storedSignature !== signature)
|
|
82
|
+
throw new Error('Kernel artifact signature mismatch');
|
|
83
|
+
const shapes: AnyShape[] = [];
|
|
84
|
+
function decode(node: Node): unknown {
|
|
85
|
+
if (!Array.isArray(node)) return node;
|
|
86
|
+
const [kind, data] = node;
|
|
87
|
+
switch (kind) {
|
|
88
|
+
case 'undefined':
|
|
89
|
+
return undefined;
|
|
90
|
+
case 'number':
|
|
91
|
+
return Number(data);
|
|
92
|
+
case 'array':
|
|
93
|
+
return (data as Node[]).map(decode);
|
|
94
|
+
case 'object':
|
|
95
|
+
return Object.fromEntries(
|
|
96
|
+
(data as [string, Node][]).map(([key, item]) => [key, decode(item)]),
|
|
97
|
+
);
|
|
98
|
+
case 'shape':
|
|
99
|
+
case 'float32':
|
|
100
|
+
case 'uint32': {
|
|
101
|
+
const [offset, length] = data as number[];
|
|
102
|
+
if (
|
|
103
|
+
!Number.isSafeInteger(offset) ||
|
|
104
|
+
!Number.isSafeInteger(length) ||
|
|
105
|
+
offset < 0 ||
|
|
106
|
+
length < 0 ||
|
|
107
|
+
start + offset + length > bytes.byteLength
|
|
108
|
+
) {
|
|
109
|
+
throw new Error('Invalid kernel artifact binary range');
|
|
110
|
+
}
|
|
111
|
+
// Each retained array owns exactly its bytes, not the whole disk record.
|
|
112
|
+
const chunk = bytes.slice(start + offset, start + offset + length);
|
|
113
|
+
if (kind === 'float32') return new Float32Array(chunk.buffer);
|
|
114
|
+
if (kind === 'uint32') return new Uint32Array(chunk.buffer);
|
|
115
|
+
const shape = decodeShape(chunk);
|
|
116
|
+
shapes.push(shape);
|
|
117
|
+
return shape;
|
|
118
|
+
}
|
|
119
|
+
default:
|
|
120
|
+
throw new Error('Invalid kernel artifact node');
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
return decode(tree) as Value;
|
|
125
|
+
} catch (error) {
|
|
126
|
+
for (const shape of shapes) shape.delete();
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Synchronous calls are serial within an OC instance. MEMFS is only a temporary
|
|
132
|
+
// bridge to the bound BinTools file overload, never the persistent store itself.
|
|
133
|
+
const shapePath = '/code3d-kernel-artifact.bin';
|
|
134
|
+
function encodeShape(shape: AnyShape): Uint8Array {
|
|
135
|
+
const oc = getOC();
|
|
136
|
+
const progress = new oc.Message_ProgressRange();
|
|
137
|
+
let created = false;
|
|
138
|
+
try {
|
|
139
|
+
oc.FS.writeFile(shapePath, new Uint8Array());
|
|
140
|
+
created = true;
|
|
141
|
+
if (!oc.BinTools.Write(shape.wrapped, shapePath, progress))
|
|
142
|
+
throw new Error('Could not encode kernel shape');
|
|
143
|
+
return oc.FS.readFile(shapePath);
|
|
144
|
+
} finally {
|
|
145
|
+
progress.delete();
|
|
146
|
+
if (created) oc.FS.unlink(shapePath);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function decodeShape(bytes: Uint8Array): AnyShape {
|
|
151
|
+
const oc = getOC();
|
|
152
|
+
const progress = new oc.Message_ProgressRange();
|
|
153
|
+
const raw = new oc.TopoDS_Shape();
|
|
154
|
+
let created = false;
|
|
155
|
+
try {
|
|
156
|
+
oc.FS.writeFile(shapePath, bytes);
|
|
157
|
+
created = true;
|
|
158
|
+
if (!oc.BinTools.Read(raw, shapePath, progress) || raw.IsNull())
|
|
159
|
+
throw new Error('Invalid kernel shape');
|
|
160
|
+
return castOwnedShape(raw.clone());
|
|
161
|
+
} finally {
|
|
162
|
+
raw.delete();
|
|
163
|
+
progress.delete();
|
|
164
|
+
if (created) oc.FS.unlink(shapePath);
|
|
165
|
+
}
|
|
166
|
+
}
|