@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,254 @@
|
|
|
1
|
+
export type Vec3 = readonly [x: number, y: number, z: number];
|
|
2
|
+
|
|
3
|
+
export type Quaternion = readonly [x: number, y: number, z: number, w: number];
|
|
4
|
+
|
|
5
|
+
export type RigidTransform = Readonly<{
|
|
6
|
+
position: Vec3;
|
|
7
|
+
quaternion: Quaternion;
|
|
8
|
+
}>;
|
|
9
|
+
|
|
10
|
+
export const origin: Vec3 = [0, 0, 0];
|
|
11
|
+
export const identityQuaternion: Quaternion = [0, 0, 0, 1];
|
|
12
|
+
export const halfTurnAroundX: Quaternion = [1, 0, 0, 0];
|
|
13
|
+
export const identityRigidTransform: RigidTransform = {
|
|
14
|
+
position: origin,
|
|
15
|
+
quaternion: identityQuaternion,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function translation(position: Vec3): RigidTransform {
|
|
19
|
+
return {position, quaternion: identityQuaternion};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function rotation(quaternion: Quaternion): RigidTransform {
|
|
23
|
+
return {position: origin, quaternion};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Degrees, applied about fixed X, then Y, then Z axes. */
|
|
27
|
+
export function xyzRotation(angles: Vec3): Quaternion {
|
|
28
|
+
const [x, y, z] = angles.map(angle => (angle * Math.PI) / 360);
|
|
29
|
+
const rx: Quaternion = [Math.sin(x), 0, 0, Math.cos(x)];
|
|
30
|
+
const ry: Quaternion = [0, Math.sin(y), 0, Math.cos(y)];
|
|
31
|
+
const rz: Quaternion = [0, 0, Math.sin(z), Math.cos(z)];
|
|
32
|
+
return multiplyQuaternions(rz, multiplyQuaternions(ry, rx));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function rotationAround(position: Vec3, angles: Vec3): RigidTransform {
|
|
36
|
+
const quaternion = xyzRotation(angles);
|
|
37
|
+
return {
|
|
38
|
+
position: addVectors(
|
|
39
|
+
position,
|
|
40
|
+
negateVector(rotateVector(position, quaternion)),
|
|
41
|
+
),
|
|
42
|
+
quaternion,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function frameFromYAxis(
|
|
47
|
+
position: Vec3,
|
|
48
|
+
direction: Vec3,
|
|
49
|
+
): RigidTransform {
|
|
50
|
+
const y = normalizeVector(direction);
|
|
51
|
+
const preferredX: Vec3 = Math.abs(y[0]) < 0.9 ? [1, 0, 0] : [0, 0, 1];
|
|
52
|
+
const x = normalizeVector(rejectVector(preferredX, y));
|
|
53
|
+
const z = crossVectors(x, y);
|
|
54
|
+
return {
|
|
55
|
+
position,
|
|
56
|
+
quaternion: quaternionFromBasis(x, y, z),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function composeTransforms(
|
|
61
|
+
outer: RigidTransform,
|
|
62
|
+
inner: RigidTransform,
|
|
63
|
+
): RigidTransform {
|
|
64
|
+
return {
|
|
65
|
+
position: addVectors(
|
|
66
|
+
outer.position,
|
|
67
|
+
rotateVector(inner.position, outer.quaternion),
|
|
68
|
+
),
|
|
69
|
+
quaternion: multiplyQuaternions(outer.quaternion, inner.quaternion),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function invertTransform(transform: RigidTransform): RigidTransform {
|
|
74
|
+
const quaternion = conjugateQuaternion(transform.quaternion);
|
|
75
|
+
return {
|
|
76
|
+
position: rotateVector(negateVector(transform.position), quaternion),
|
|
77
|
+
quaternion,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function relativeTransform(
|
|
82
|
+
source: RigidTransform,
|
|
83
|
+
target: RigidTransform,
|
|
84
|
+
): RigidTransform {
|
|
85
|
+
return composeTransforms(invertTransform(target), source);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function rotateVector(vector: Vec3, quaternion: Quaternion): Vec3 {
|
|
89
|
+
const [x, y, z] = vector;
|
|
90
|
+
const [qx, qy, qz, qw] = quaternion;
|
|
91
|
+
const tx = 2 * (qy * z - qz * y);
|
|
92
|
+
const ty = 2 * (qz * x - qx * z);
|
|
93
|
+
const tz = 2 * (qx * y - qy * x);
|
|
94
|
+
return [
|
|
95
|
+
x + qw * tx + (qy * tz - qz * ty),
|
|
96
|
+
y + qw * ty + (qz * tx - qx * tz),
|
|
97
|
+
z + qw * tz + (qx * ty - qy * tx),
|
|
98
|
+
];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function addVectors(left: Vec3, right: Vec3): Vec3 {
|
|
102
|
+
return [left[0] + right[0], left[1] + right[1], left[2] + right[2]];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function negateVector(vector: Vec3): Vec3 {
|
|
106
|
+
return [-vector[0], -vector[1], -vector[2]];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function transformsAreEquivalent(
|
|
110
|
+
left: RigidTransform,
|
|
111
|
+
right: RigidTransform,
|
|
112
|
+
tolerance = 1e-7,
|
|
113
|
+
): boolean {
|
|
114
|
+
const positionDelta = Math.hypot(
|
|
115
|
+
left.position[0] - right.position[0],
|
|
116
|
+
left.position[1] - right.position[1],
|
|
117
|
+
left.position[2] - right.position[2],
|
|
118
|
+
);
|
|
119
|
+
const quaternionDot = Math.abs(
|
|
120
|
+
left.quaternion[0] * right.quaternion[0] +
|
|
121
|
+
left.quaternion[1] * right.quaternion[1] +
|
|
122
|
+
left.quaternion[2] * right.quaternion[2] +
|
|
123
|
+
left.quaternion[3] * right.quaternion[3],
|
|
124
|
+
);
|
|
125
|
+
return positionDelta <= tolerance && 1 - quaternionDot <= tolerance;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function quaternionAxisAngle(quaternion: Quaternion): Readonly<{
|
|
129
|
+
axis: Vec3;
|
|
130
|
+
angleDegrees: number;
|
|
131
|
+
}> {
|
|
132
|
+
const normalized = normalizeQuaternion(quaternion);
|
|
133
|
+
const sign = normalized[3] < 0 ? -1 : 1;
|
|
134
|
+
const canonical: Quaternion = [
|
|
135
|
+
normalized[0] * sign,
|
|
136
|
+
normalized[1] * sign,
|
|
137
|
+
normalized[2] * sign,
|
|
138
|
+
normalized[3] * sign,
|
|
139
|
+
];
|
|
140
|
+
const angle = 2 * Math.acos(clamp(canonical[3], -1, 1));
|
|
141
|
+
const sine = Math.sqrt(Math.max(0, 1 - canonical[3] * canonical[3]));
|
|
142
|
+
return {
|
|
143
|
+
axis:
|
|
144
|
+
sine < 1e-9
|
|
145
|
+
? [1, 0, 0]
|
|
146
|
+
: [canonical[0] / sine, canonical[1] / sine, canonical[2] / sine],
|
|
147
|
+
angleDegrees: (angle * 180) / Math.PI,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function multiplyQuaternions(left: Quaternion, right: Quaternion): Quaternion {
|
|
152
|
+
const [lx, ly, lz, lw] = left;
|
|
153
|
+
const [rx, ry, rz, rw] = right;
|
|
154
|
+
return normalizeQuaternion([
|
|
155
|
+
lw * rx + lx * rw + ly * rz - lz * ry,
|
|
156
|
+
lw * ry - lx * rz + ly * rw + lz * rx,
|
|
157
|
+
lw * rz + lx * ry - ly * rx + lz * rw,
|
|
158
|
+
lw * rw - lx * rx - ly * ry - lz * rz,
|
|
159
|
+
]);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function conjugateQuaternion(quaternion: Quaternion): Quaternion {
|
|
163
|
+
return [-quaternion[0], -quaternion[1], -quaternion[2], quaternion[3]];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function normalizeQuaternion(quaternion: Quaternion): Quaternion {
|
|
167
|
+
const length = Math.hypot(...quaternion);
|
|
168
|
+
return [
|
|
169
|
+
quaternion[0] / length,
|
|
170
|
+
quaternion[1] / length,
|
|
171
|
+
quaternion[2] / length,
|
|
172
|
+
quaternion[3] / length,
|
|
173
|
+
];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function normalizeVector(vector: Vec3): Vec3 {
|
|
177
|
+
const length = Math.hypot(...vector);
|
|
178
|
+
if (length < 1e-12) {
|
|
179
|
+
throw new Error('A relation frame direction cannot be zero.');
|
|
180
|
+
}
|
|
181
|
+
return [vector[0] / length, vector[1] / length, vector[2] / length];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function rejectVector(vector: Vec3, normal: Vec3): Vec3 {
|
|
185
|
+
const projection = dotVectors(vector, normal);
|
|
186
|
+
return [
|
|
187
|
+
vector[0] - projection * normal[0],
|
|
188
|
+
vector[1] - projection * normal[1],
|
|
189
|
+
vector[2] - projection * normal[2],
|
|
190
|
+
];
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function dotVectors(left: Vec3, right: Vec3): number {
|
|
194
|
+
return left[0] * right[0] + left[1] * right[1] + left[2] * right[2];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function crossVectors(left: Vec3, right: Vec3): Vec3 {
|
|
198
|
+
return [
|
|
199
|
+
left[1] * right[2] - left[2] * right[1],
|
|
200
|
+
left[2] * right[0] - left[0] * right[2],
|
|
201
|
+
left[0] * right[1] - left[1] * right[0],
|
|
202
|
+
];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function quaternionFromBasis(x: Vec3, y: Vec3, z: Vec3): Quaternion {
|
|
206
|
+
const m00 = x[0];
|
|
207
|
+
const m01 = y[0];
|
|
208
|
+
const m02 = z[0];
|
|
209
|
+
const m10 = x[1];
|
|
210
|
+
const m11 = y[1];
|
|
211
|
+
const m12 = z[1];
|
|
212
|
+
const m20 = x[2];
|
|
213
|
+
const m21 = y[2];
|
|
214
|
+
const m22 = z[2];
|
|
215
|
+
const trace = m00 + m11 + m22;
|
|
216
|
+
if (trace > 0) {
|
|
217
|
+
const scale = Math.sqrt(trace + 1) * 2;
|
|
218
|
+
return normalizeQuaternion([
|
|
219
|
+
(m21 - m12) / scale,
|
|
220
|
+
(m02 - m20) / scale,
|
|
221
|
+
(m10 - m01) / scale,
|
|
222
|
+
scale / 4,
|
|
223
|
+
]);
|
|
224
|
+
}
|
|
225
|
+
if (m00 > m11 && m00 > m22) {
|
|
226
|
+
const scale = Math.sqrt(1 + m00 - m11 - m22) * 2;
|
|
227
|
+
return normalizeQuaternion([
|
|
228
|
+
scale / 4,
|
|
229
|
+
(m01 + m10) / scale,
|
|
230
|
+
(m02 + m20) / scale,
|
|
231
|
+
(m21 - m12) / scale,
|
|
232
|
+
]);
|
|
233
|
+
}
|
|
234
|
+
if (m11 > m22) {
|
|
235
|
+
const scale = Math.sqrt(1 + m11 - m00 - m22) * 2;
|
|
236
|
+
return normalizeQuaternion([
|
|
237
|
+
(m01 + m10) / scale,
|
|
238
|
+
scale / 4,
|
|
239
|
+
(m12 + m21) / scale,
|
|
240
|
+
(m02 - m20) / scale,
|
|
241
|
+
]);
|
|
242
|
+
}
|
|
243
|
+
const scale = Math.sqrt(1 + m22 - m00 - m11) * 2;
|
|
244
|
+
return normalizeQuaternion([
|
|
245
|
+
(m02 + m20) / scale,
|
|
246
|
+
(m12 + m21) / scale,
|
|
247
|
+
scale / 4,
|
|
248
|
+
(m10 - m01) / scale,
|
|
249
|
+
]);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function clamp(value: number, min: number, max: number): number {
|
|
253
|
+
return Math.min(max, Math.max(min, value));
|
|
254
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/** A result-local ID, or an input path ending in an earlier result's local ID. */
|
|
2
|
+
export type TopologyId = number | readonly [number, number, ...number[]];
|
|
3
|
+
export type VertexId = TopologyId;
|
|
4
|
+
export type EdgeId = TopologyId;
|
|
5
|
+
export type SurfaceId = TopologyId;
|
|
6
|
+
export type TopologyKind = 'vertex' | 'edge' | 'surface';
|
|
7
|
+
|
|
8
|
+
export function isTopologyId(value: unknown): value is TopologyId {
|
|
9
|
+
const positiveInteger = (part: unknown): part is number =>
|
|
10
|
+
typeof part === 'number' && Number.isSafeInteger(part) && part > 0;
|
|
11
|
+
if (positiveInteger(value)) return true;
|
|
12
|
+
if (!Array.isArray(value) || value.length < 2) return false;
|
|
13
|
+
for (const part of value) if (!positiveInteger(part)) return false;
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function topologyIdKey(id: TopologyId): string {
|
|
18
|
+
return typeof id === 'number' ? String(id) : id.join('/');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function sameTopologyId(
|
|
22
|
+
left: TopologyId | undefined,
|
|
23
|
+
right: TopologyId | undefined,
|
|
24
|
+
): boolean {
|
|
25
|
+
return (
|
|
26
|
+
left === right ||
|
|
27
|
+
(left !== undefined &&
|
|
28
|
+
right !== undefined &&
|
|
29
|
+
topologyIdKey(left) === topologyIdKey(right))
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function inheritedTopologyId(input: number, id: TopologyId): TopologyId {
|
|
34
|
+
return typeof id === 'number' ? [input, id] : [input, ...id];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function compareTopologyIds(
|
|
38
|
+
left: TopologyId,
|
|
39
|
+
right: TopologyId,
|
|
40
|
+
): number {
|
|
41
|
+
if (typeof left === 'number')
|
|
42
|
+
return typeof right === 'number' ? left - right : -1;
|
|
43
|
+
if (typeof right === 'number') return 1;
|
|
44
|
+
for (let index = 0; index < Math.min(left.length, right.length); index++) {
|
|
45
|
+
const difference = left[index] - right[index];
|
|
46
|
+
if (difference) return difference;
|
|
47
|
+
}
|
|
48
|
+
return left.length - right.length;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function formatTopologyId(kind: TopologyKind, id: TopologyId): string {
|
|
52
|
+
const prefix = {vertex: 'V', edge: 'E', surface: 'S'}[kind];
|
|
53
|
+
return `${prefix}${typeof id === 'number' ? id : `[${id.join(',')}]`}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Value equality also works for IDs reconstructed from source or a Worker. */
|
|
57
|
+
export class TopologyIdSet implements Iterable<TopologyId> {
|
|
58
|
+
private readonly entries = new Map<string, TopologyId>();
|
|
59
|
+
constructor(ids: Iterable<TopologyId> = []) {
|
|
60
|
+
for (const id of ids) this.add(id);
|
|
61
|
+
}
|
|
62
|
+
get size(): number {
|
|
63
|
+
return this.entries.size;
|
|
64
|
+
}
|
|
65
|
+
has(id: TopologyId): boolean {
|
|
66
|
+
return this.entries.has(topologyIdKey(id));
|
|
67
|
+
}
|
|
68
|
+
add(id: TopologyId): this {
|
|
69
|
+
this.entries.set(topologyIdKey(id), id);
|
|
70
|
+
return this;
|
|
71
|
+
}
|
|
72
|
+
delete(id: TopologyId): boolean {
|
|
73
|
+
return this.entries.delete(topologyIdKey(id));
|
|
74
|
+
}
|
|
75
|
+
clear(): void {
|
|
76
|
+
this.entries.clear();
|
|
77
|
+
}
|
|
78
|
+
[Symbol.iterator](): MapIterator<TopologyId> {
|
|
79
|
+
return this.entries.values();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BoundingBox,
|
|
3
|
+
getOC,
|
|
4
|
+
measureShapeLinearProperties,
|
|
5
|
+
measureShapeSurfaceProperties,
|
|
6
|
+
type AnyShape,
|
|
7
|
+
type Edge,
|
|
8
|
+
type Face,
|
|
9
|
+
type Vector,
|
|
10
|
+
} from 'replicad';
|
|
11
|
+
import {shapeSubshapes} from './kernel-shapes.js';
|
|
12
|
+
import type {ShapeTopology, TopologyId, TopologyKind} from './topology.js';
|
|
13
|
+
import {sameTopologyId} from './topology-id.js';
|
|
14
|
+
import {
|
|
15
|
+
identityRigidTransform,
|
|
16
|
+
quaternionAxisAngle,
|
|
17
|
+
type RigidTransform,
|
|
18
|
+
type Vec3,
|
|
19
|
+
} from './spatial.js';
|
|
20
|
+
|
|
21
|
+
export type TopologyInspectionOptions = Readonly<{
|
|
22
|
+
kind?: TopologyKind;
|
|
23
|
+
ids?: readonly TopologyId[];
|
|
24
|
+
offset?: number;
|
|
25
|
+
limit?: number;
|
|
26
|
+
transform?: RigidTransform & {scale?: Vec3};
|
|
27
|
+
}>;
|
|
28
|
+
export type TopologyGeometry = {
|
|
29
|
+
type: string;
|
|
30
|
+
position?: Vec3;
|
|
31
|
+
length?: number;
|
|
32
|
+
area?: number;
|
|
33
|
+
centroid?: Vec3;
|
|
34
|
+
start?: Vec3;
|
|
35
|
+
end?: Vec3;
|
|
36
|
+
closed?: boolean;
|
|
37
|
+
direction?: Vec3;
|
|
38
|
+
normal?: Vec3;
|
|
39
|
+
normalSample?: {
|
|
40
|
+
position: Vec3;
|
|
41
|
+
normal: Vec3;
|
|
42
|
+
method: 'uv-domain-midpoint';
|
|
43
|
+
trimMembership: 'unchecked';
|
|
44
|
+
};
|
|
45
|
+
center?: Vec3;
|
|
46
|
+
axis?: Vec3;
|
|
47
|
+
radius?: number;
|
|
48
|
+
majorRadius?: number;
|
|
49
|
+
minorRadius?: number;
|
|
50
|
+
};
|
|
51
|
+
export type TopologyInspectionItem = {
|
|
52
|
+
kind: TopologyKind;
|
|
53
|
+
id: TopologyId;
|
|
54
|
+
geometry?: TopologyGeometry;
|
|
55
|
+
vertices?: readonly TopologyId[];
|
|
56
|
+
edges?: readonly TopologyId[];
|
|
57
|
+
surfaces?: readonly TopologyId[];
|
|
58
|
+
unavailable?: string;
|
|
59
|
+
};
|
|
60
|
+
export type TopologyInspection = Readonly<{
|
|
61
|
+
counts: Record<TopologyKind, number>;
|
|
62
|
+
bounds: {min: Vec3; max: Vec3; size: Vec3; method: 'kernel-enclosure'};
|
|
63
|
+
total: number;
|
|
64
|
+
offset: number;
|
|
65
|
+
nextOffset?: number;
|
|
66
|
+
items: readonly TopologyInspectionItem[];
|
|
67
|
+
units: 'model';
|
|
68
|
+
coordinates: 'scene';
|
|
69
|
+
geometrySource: 'kernel';
|
|
70
|
+
transform: NonNullable<TopologyInspectionOptions['transform']>;
|
|
71
|
+
}>;
|
|
72
|
+
|
|
73
|
+
/** Query retained B-rep geometry and its actual ID namespace, never the render mesh. */
|
|
74
|
+
export function inspectShapeTopology(
|
|
75
|
+
original: AnyShape,
|
|
76
|
+
topology: ShapeTopology,
|
|
77
|
+
options: TopologyInspectionOptions = {},
|
|
78
|
+
): TopologyInspection {
|
|
79
|
+
const offset = options.offset ?? 0;
|
|
80
|
+
const limit = options.limit ?? 48;
|
|
81
|
+
if (
|
|
82
|
+
!Number.isSafeInteger(offset) ||
|
|
83
|
+
offset < 0 ||
|
|
84
|
+
!Number.isSafeInteger(limit) ||
|
|
85
|
+
limit < 1 ||
|
|
86
|
+
limit > 200
|
|
87
|
+
)
|
|
88
|
+
throw new Error(
|
|
89
|
+
'Topology pages require a nonnegative offset and a limit from 1 to 200.',
|
|
90
|
+
);
|
|
91
|
+
if (options.ids && !options.kind)
|
|
92
|
+
throw new Error('Specify a topology kind when filtering IDs.');
|
|
93
|
+
const transform: NonNullable<TopologyInspectionOptions['transform']> =
|
|
94
|
+
options.transform ?? identityRigidTransform;
|
|
95
|
+
const scale = transform.scale ?? [1, 1, 1];
|
|
96
|
+
if (
|
|
97
|
+
scale.some(
|
|
98
|
+
value => !Number.isFinite(value) || value === 0 || value !== scale[0],
|
|
99
|
+
)
|
|
100
|
+
)
|
|
101
|
+
throw new Error('Topology inspection requires a uniform nonzero scale.');
|
|
102
|
+
let shape = original.clone();
|
|
103
|
+
const owned: AnyShape[] = [];
|
|
104
|
+
try {
|
|
105
|
+
if (scale[0] !== 1) shape = shape.scale(scale[0]);
|
|
106
|
+
const {axis, angleDegrees} = quaternionAxisAngle(transform.quaternion);
|
|
107
|
+
if (Math.abs(angleDegrees) > 1e-9)
|
|
108
|
+
shape = shape.rotate(angleDegrees, [0, 0, 0], [...axis]);
|
|
109
|
+
shape = shape.translate([...transform.position]);
|
|
110
|
+
const vertices = shapeSubshapes(shape, 'vertex');
|
|
111
|
+
owned.push(...vertices);
|
|
112
|
+
const edges = shapeSubshapes(shape, 'edge');
|
|
113
|
+
owned.push(...edges);
|
|
114
|
+
const surfaces = shapeSubshapes(shape, 'face');
|
|
115
|
+
owned.push(...surfaces);
|
|
116
|
+
const shapes = {vertex: vertices, edge: edges, surface: surfaces};
|
|
117
|
+
const ids = {
|
|
118
|
+
vertex: topology.vertices.ids,
|
|
119
|
+
edge: topology.edges.ids,
|
|
120
|
+
surface: topology.surfaces.ids,
|
|
121
|
+
};
|
|
122
|
+
for (const kind of ['vertex', 'edge', 'surface'] as const)
|
|
123
|
+
if (shapes[kind].length !== ids[kind].length)
|
|
124
|
+
throw new Error(
|
|
125
|
+
'Retained geometry does not match its topology namespace.',
|
|
126
|
+
);
|
|
127
|
+
if (
|
|
128
|
+
options.ids?.some(
|
|
129
|
+
id =>
|
|
130
|
+
!ids[options.kind!].some(available => sameTopologyId(available, id)),
|
|
131
|
+
)
|
|
132
|
+
)
|
|
133
|
+
throw new Error(
|
|
134
|
+
'A requested topology ID is absent from this model snapshot.',
|
|
135
|
+
);
|
|
136
|
+
const all = (
|
|
137
|
+
options.kind ? [options.kind] : (['surface', 'edge', 'vertex'] as const)
|
|
138
|
+
).flatMap(kind =>
|
|
139
|
+
ids[kind].flatMap((id, index) =>
|
|
140
|
+
!options.ids ||
|
|
141
|
+
options.ids.some(requested => sameTopologyId(requested, id))
|
|
142
|
+
? [{kind, id, index}]
|
|
143
|
+
: [],
|
|
144
|
+
),
|
|
145
|
+
);
|
|
146
|
+
const page = all.slice(offset, offset + limit);
|
|
147
|
+
const childIndices = (
|
|
148
|
+
parent: AnyShape,
|
|
149
|
+
kind: 'edge' | 'vertex',
|
|
150
|
+
): number[] => {
|
|
151
|
+
const children = shapeSubshapes(parent, kind);
|
|
152
|
+
try {
|
|
153
|
+
return shapes[kind].flatMap((candidate, index) =>
|
|
154
|
+
children.some(child => child.isSame(candidate)) ? [index] : [],
|
|
155
|
+
);
|
|
156
|
+
} finally {
|
|
157
|
+
children.forEach(child => child.delete());
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
const edgeVertices = new Map<number, number[]>();
|
|
161
|
+
const surfaceEdges = new Map<number, number[]>();
|
|
162
|
+
const verticesOf = (index: number) => {
|
|
163
|
+
if (!edgeVertices.has(index))
|
|
164
|
+
edgeVertices.set(index, childIndices(edges[index], 'vertex'));
|
|
165
|
+
return edgeVertices.get(index)!;
|
|
166
|
+
};
|
|
167
|
+
const edgesOf = (index: number) => {
|
|
168
|
+
if (!surfaceEdges.has(index))
|
|
169
|
+
surfaceEdges.set(index, childIndices(surfaces[index], 'edge'));
|
|
170
|
+
return surfaceEdges.get(index)!;
|
|
171
|
+
};
|
|
172
|
+
const items = page.map(({kind, id, index}): TopologyInspectionItem => {
|
|
173
|
+
try {
|
|
174
|
+
if (kind === 'vertex')
|
|
175
|
+
return {
|
|
176
|
+
kind,
|
|
177
|
+
id,
|
|
178
|
+
geometry: {type: 'POINT', position: vertices[index].asTuple()},
|
|
179
|
+
edges: edges.flatMap((_, edge) =>
|
|
180
|
+
verticesOf(edge).includes(index) ? [ids.edge[edge]] : [],
|
|
181
|
+
),
|
|
182
|
+
};
|
|
183
|
+
if (kind === 'edge')
|
|
184
|
+
return {
|
|
185
|
+
kind,
|
|
186
|
+
id,
|
|
187
|
+
geometry: inspectEdge(edges[index]),
|
|
188
|
+
vertices: verticesOf(index).map(vertex => ids.vertex[vertex]),
|
|
189
|
+
surfaces: surfaces.flatMap((_, surface) =>
|
|
190
|
+
edgesOf(surface).includes(index) ? [ids.surface[surface]] : [],
|
|
191
|
+
),
|
|
192
|
+
};
|
|
193
|
+
return {
|
|
194
|
+
kind,
|
|
195
|
+
id,
|
|
196
|
+
geometry: inspectSurface(surfaces[index]),
|
|
197
|
+
edges: edgesOf(index).map(edge => ids.edge[edge]),
|
|
198
|
+
};
|
|
199
|
+
} catch (error) {
|
|
200
|
+
return {
|
|
201
|
+
kind,
|
|
202
|
+
id,
|
|
203
|
+
unavailable:
|
|
204
|
+
error instanceof Error
|
|
205
|
+
? error.message
|
|
206
|
+
: 'Kernel geometry query failed.',
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
const box = new BoundingBox();
|
|
211
|
+
let min: Vec3, max: Vec3;
|
|
212
|
+
try {
|
|
213
|
+
getOC().BRepBndLib.Add(shape.wrapped, box.wrapped, false);
|
|
214
|
+
[min, max] = box.bounds;
|
|
215
|
+
} finally {
|
|
216
|
+
box.delete();
|
|
217
|
+
}
|
|
218
|
+
return {
|
|
219
|
+
counts: {
|
|
220
|
+
vertex: vertices.length,
|
|
221
|
+
edge: edges.length,
|
|
222
|
+
surface: surfaces.length,
|
|
223
|
+
},
|
|
224
|
+
bounds: {
|
|
225
|
+
min,
|
|
226
|
+
max,
|
|
227
|
+
size: [max[0] - min[0], max[1] - min[1], max[2] - min[2]],
|
|
228
|
+
method: 'kernel-enclosure',
|
|
229
|
+
},
|
|
230
|
+
total: all.length,
|
|
231
|
+
offset,
|
|
232
|
+
...(offset + page.length < all.length
|
|
233
|
+
? {nextOffset: offset + page.length}
|
|
234
|
+
: {}),
|
|
235
|
+
items,
|
|
236
|
+
units: 'model',
|
|
237
|
+
coordinates: 'scene',
|
|
238
|
+
geometrySource: 'kernel',
|
|
239
|
+
transform,
|
|
240
|
+
};
|
|
241
|
+
} finally {
|
|
242
|
+
owned.forEach(value => value.delete());
|
|
243
|
+
shape.delete();
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function vector(value: Vector, normalize = false): Vec3 {
|
|
248
|
+
try {
|
|
249
|
+
const tuple = value.toTuple();
|
|
250
|
+
if (!normalize) return tuple;
|
|
251
|
+
const length = Math.hypot(...tuple);
|
|
252
|
+
if (length < 1e-14) throw new Error('The sampled direction is singular.');
|
|
253
|
+
return [tuple[0] / length, tuple[1] / length, tuple[2] / length];
|
|
254
|
+
} finally {
|
|
255
|
+
value.delete();
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
function xyz(value: {
|
|
259
|
+
X(): number;
|
|
260
|
+
Y(): number;
|
|
261
|
+
Z(): number;
|
|
262
|
+
delete(): void;
|
|
263
|
+
}): Vec3 {
|
|
264
|
+
try {
|
|
265
|
+
return [value.X(), value.Y(), value.Z()];
|
|
266
|
+
} finally {
|
|
267
|
+
value.delete();
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
function analytic(value: {
|
|
271
|
+
Location(): {X(): number; Y(): number; Z(): number; delete(): void};
|
|
272
|
+
Axis(): {
|
|
273
|
+
Direction(): {X(): number; Y(): number; Z(): number; delete(): void};
|
|
274
|
+
delete(): void;
|
|
275
|
+
};
|
|
276
|
+
delete(): void;
|
|
277
|
+
}): {center: Vec3; axis: Vec3} {
|
|
278
|
+
try {
|
|
279
|
+
const axis = value.Axis();
|
|
280
|
+
try {
|
|
281
|
+
return {center: xyz(value.Location()), axis: xyz(axis.Direction())};
|
|
282
|
+
} finally {
|
|
283
|
+
axis.delete();
|
|
284
|
+
}
|
|
285
|
+
} finally {
|
|
286
|
+
value.delete();
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function inspectEdge(edge: Edge): TopologyGeometry {
|
|
291
|
+
const properties = measureShapeLinearProperties(edge);
|
|
292
|
+
let length: number;
|
|
293
|
+
try {
|
|
294
|
+
length = properties.length;
|
|
295
|
+
} finally {
|
|
296
|
+
properties.delete();
|
|
297
|
+
}
|
|
298
|
+
const type = edge.geomType;
|
|
299
|
+
const result: TopologyGeometry = {
|
|
300
|
+
type,
|
|
301
|
+
length,
|
|
302
|
+
start: vector(edge.startPoint),
|
|
303
|
+
end: vector(edge.endPoint),
|
|
304
|
+
closed: edge.isClosed,
|
|
305
|
+
};
|
|
306
|
+
if (type === 'LINE') result.direction = vector(edge.tangentAt(0.5), true);
|
|
307
|
+
if (type === 'CIRCLE' || type === 'ELLIPSE') {
|
|
308
|
+
const curve = new (getOC().BRepAdaptor_Curve)(edge.wrapped);
|
|
309
|
+
try {
|
|
310
|
+
if (type === 'CIRCLE') {
|
|
311
|
+
const circle = curve.Circle();
|
|
312
|
+
result.radius = circle.Radius();
|
|
313
|
+
Object.assign(result, analytic(circle));
|
|
314
|
+
} else {
|
|
315
|
+
const ellipse = curve.Ellipse();
|
|
316
|
+
result.majorRadius = ellipse.MajorRadius();
|
|
317
|
+
result.minorRadius = ellipse.MinorRadius();
|
|
318
|
+
Object.assign(result, analytic(ellipse));
|
|
319
|
+
}
|
|
320
|
+
} finally {
|
|
321
|
+
curve.delete();
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return result;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function inspectSurface(face: Face): TopologyGeometry {
|
|
328
|
+
const properties = measureShapeSurfaceProperties(face);
|
|
329
|
+
const type = face.geomType;
|
|
330
|
+
let result: TopologyGeometry;
|
|
331
|
+
try {
|
|
332
|
+
result = {
|
|
333
|
+
type: type === 'CYLINDRE' ? 'CYLINDER' : type,
|
|
334
|
+
area: properties.area,
|
|
335
|
+
centroid: properties.centerOfMass,
|
|
336
|
+
};
|
|
337
|
+
} finally {
|
|
338
|
+
properties.delete();
|
|
339
|
+
}
|
|
340
|
+
if (type === 'PLANE') result.normal = vector(face.normalAt(), true);
|
|
341
|
+
else
|
|
342
|
+
result.normalSample = {
|
|
343
|
+
position: vector(face.pointOnSurface(0.5, 0.5)),
|
|
344
|
+
normal: vector(face.normalAt(), true),
|
|
345
|
+
method: 'uv-domain-midpoint',
|
|
346
|
+
trimMembership: 'unchecked',
|
|
347
|
+
};
|
|
348
|
+
const surface = face.surface;
|
|
349
|
+
try {
|
|
350
|
+
if (type === 'CYLINDRE') {
|
|
351
|
+
const cylinder = surface.wrapped.Cylinder();
|
|
352
|
+
result.radius = cylinder.Radius();
|
|
353
|
+
Object.assign(result, analytic(cylinder));
|
|
354
|
+
}
|
|
355
|
+
if (type === 'SPHERE') {
|
|
356
|
+
const sphere = surface.wrapped.Sphere();
|
|
357
|
+
try {
|
|
358
|
+
result.radius = sphere.Radius();
|
|
359
|
+
result.center = xyz(sphere.Location());
|
|
360
|
+
} finally {
|
|
361
|
+
sphere.delete();
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
} finally {
|
|
365
|
+
surface.delete();
|
|
366
|
+
}
|
|
367
|
+
return result;
|
|
368
|
+
}
|