dsh-cad 0.16.2 → 0.17.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/README.md +9 -0
- package/README.zh-CN.md +9 -0
- package/lib/client.js +31 -31
- package/lib/client.js.map +3 -3
- package/lib/modeling/assembly.js +318 -215
- package/lib/modeling/cadorange-adapter.cjs +75 -0
- package/lib/modeling/cadorange-bridge.cjs +10 -0
- package/lib/modeling/joints.js +442 -0
- package/lib/modeling/modeling-worker.cjs +88 -0
- package/lib/tools/cad-model.js +2282 -1797
- package/lib/types/modeling/assembly.d.ts +106 -78
- package/lib/types/modeling/client.d.ts +279 -227
- package/lib/types/modeling/joints.d.ts +129 -0
- package/lib/types/tools/cad-model.d.ts +16 -16
- package/package.json +2 -2
package/lib/modeling/assembly.js
CHANGED
|
@@ -1,215 +1,318 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
nameUse.
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
const
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
positions[i
|
|
64
|
-
positions[i +
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
normals[i
|
|
76
|
-
normals[i +
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
case '
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
case '
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
case '
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
break;
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
}
|
|
1
|
+
import { applyConnectValue, mergeJointValue, normalizeJointSpec, jointValueText } from './joints.js';
|
|
2
|
+
/**
|
|
3
|
+
* The per-instance pastel palette: instance i takes palette[i % n] both in the
|
|
4
|
+
* composed 3D scene and in the assembly-tree swatches, so a part's color chip
|
|
5
|
+
* always matches its solid. Stable per list position (the worker's insertion
|
|
6
|
+
* order), like the reference CAD assembly panels.
|
|
7
|
+
*/
|
|
8
|
+
export const INSTANCE_PALETTE = [
|
|
9
|
+
0xd98e8e, // rose
|
|
10
|
+
0x9fd4a8, // green
|
|
11
|
+
0xc3a6e8, // violet
|
|
12
|
+
0xe8d58c, // sand
|
|
13
|
+
0x8fd0c9, // teal
|
|
14
|
+
0xf0b7c5, // pink
|
|
15
|
+
0xa8c68f, // olive
|
|
16
|
+
0x9fb8e8, // periwinkle
|
|
17
|
+
0xe8a87c, // orange
|
|
18
|
+
0x7fc8bc, // jade
|
|
19
|
+
0xc9a2d8, // lilac
|
|
20
|
+
0x8fb3d9, // steel blue
|
|
21
|
+
];
|
|
22
|
+
/** The palette color of the instance at `index` in the assembly list. */
|
|
23
|
+
export function instanceColor(index) {
|
|
24
|
+
return INSTANCE_PALETTE[index % INSTANCE_PALETTE.length];
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Instance display names with the ·N dedup suffix — the single source for
|
|
28
|
+
* mesh names in the composed scene AND part names in the assembly tree, so a
|
|
29
|
+
* tree row always names its mesh (highlight linking relies on it).
|
|
30
|
+
*/
|
|
31
|
+
export function assemblyDisplayNames(instances) {
|
|
32
|
+
const nameUse = new Map();
|
|
33
|
+
return instances.map((instance) => {
|
|
34
|
+
const seen = nameUse.get(instance.name) ?? 0;
|
|
35
|
+
nameUse.set(instance.name, seen + 1);
|
|
36
|
+
return seen === 0 ? instance.name : `${instance.name}·${seen + 1}`;
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
function rotationMatrix(rotate) {
|
|
40
|
+
const toRad = (deg) => (deg * Math.PI) / 180;
|
|
41
|
+
const [rx, ry, rz] = rotate.map(toRad);
|
|
42
|
+
const cx = Math.cos(rx), sx = Math.sin(rx);
|
|
43
|
+
const cy = Math.cos(ry), sy = Math.sin(ry);
|
|
44
|
+
const cz = Math.cos(rz), sz = Math.sin(rz);
|
|
45
|
+
// Row-major 3×3: Rx·Ry·Rz
|
|
46
|
+
return new Float64Array([
|
|
47
|
+
cy * cz, -cy * sz, sy,
|
|
48
|
+
cx * sz + sx * sy * cz, cx * cz - sx * sy * sz, -sx * cy,
|
|
49
|
+
sx * sz - cx * sy * cz, sx * cz + cx * sy * sz, cx * cy,
|
|
50
|
+
]);
|
|
51
|
+
}
|
|
52
|
+
function transformMesh(mesh, translate, rotate) {
|
|
53
|
+
const isIdentity = translate.every((v) => v === 0) && rotate.every((v) => v === 0);
|
|
54
|
+
if (isIdentity)
|
|
55
|
+
return mesh;
|
|
56
|
+
const m = rotationMatrix(rotate);
|
|
57
|
+
const [tx, ty, tz] = translate;
|
|
58
|
+
const positions = new Float32Array(mesh.positions.length);
|
|
59
|
+
for (let i = 0; i + 2 < mesh.positions.length; i += 3) {
|
|
60
|
+
const x = mesh.positions[i];
|
|
61
|
+
const y = mesh.positions[i + 1];
|
|
62
|
+
const z = mesh.positions[i + 2];
|
|
63
|
+
positions[i] = m[0] * x + m[1] * y + m[2] * z + tx;
|
|
64
|
+
positions[i + 1] = m[3] * x + m[4] * y + m[5] * z + ty;
|
|
65
|
+
positions[i + 2] = m[6] * x + m[7] * y + m[8] * z + tz;
|
|
66
|
+
}
|
|
67
|
+
// Normals rotate with the same linear part (translation never applies).
|
|
68
|
+
let normals;
|
|
69
|
+
if (mesh.normals !== undefined) {
|
|
70
|
+
normals = new Float32Array(mesh.normals.length);
|
|
71
|
+
for (let i = 0; i + 2 < mesh.normals.length; i += 3) {
|
|
72
|
+
const x = mesh.normals[i];
|
|
73
|
+
const y = mesh.normals[i + 1];
|
|
74
|
+
const z = mesh.normals[i + 2];
|
|
75
|
+
normals[i] = m[0] * x + m[1] * y + m[2] * z;
|
|
76
|
+
normals[i + 1] = m[3] * x + m[4] * y + m[5] * z;
|
|
77
|
+
normals[i + 2] = m[6] * x + m[7] * y + m[8] * z;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return { ...mesh, positions, normals };
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Compose the assembly scene: one transformed mesh copy per instance, colored
|
|
84
|
+
* by list position (see INSTANCE_PALETTE). Instances referencing bodies
|
|
85
|
+
* missing from `bodies` (consumed by a later boolean) are skipped — the
|
|
86
|
+
* worker's live list already filters them, this is the replay-side double
|
|
87
|
+
* guard. Duplicate names get a ·N suffix (assemblyDisplayNames). `explode`
|
|
88
|
+
* (from explodeOffsets) shifts each instance radially before composing —
|
|
89
|
+
* the exploded view, view-only math over the same placement data.
|
|
90
|
+
*/
|
|
91
|
+
export function composeAssemblyMeshes(bodies, instances, explode) {
|
|
92
|
+
const meshes = [];
|
|
93
|
+
const names = assemblyDisplayNames(instances);
|
|
94
|
+
for (let index = 0; index < instances.length; index++) {
|
|
95
|
+
const instance = instances[index];
|
|
96
|
+
const body = bodies.get(instance.bodyId);
|
|
97
|
+
if (body === undefined)
|
|
98
|
+
continue;
|
|
99
|
+
const offset = explode?.get(instance.instanceId);
|
|
100
|
+
const translate = offset === undefined
|
|
101
|
+
? instance.translate
|
|
102
|
+
: [instance.translate[0] + offset[0], instance.translate[1] + offset[1], instance.translate[2] + offset[2]];
|
|
103
|
+
meshes.push(transformMesh({ ...body, name: names[index], color: instanceColor(index) }, translate, instance.rotate));
|
|
104
|
+
}
|
|
105
|
+
return meshes;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Rebuild the assembly state purely from the persisted op log — the worker
|
|
109
|
+
* keeps the same data live (Map insertion order = push order here), so the
|
|
110
|
+
* tree endpoint serves restart-safe data without a worker round-trip.
|
|
111
|
+
*
|
|
112
|
+
* Joint ops are metadata: the placements they solve are written back through
|
|
113
|
+
* `assembly_transform` ops by the tool layer, so this fold carries specs and
|
|
114
|
+
* connections but computes no algebra — replay restores identical placements.
|
|
115
|
+
*/
|
|
116
|
+
export function foldAssemblyState(ops, bodyNames) {
|
|
117
|
+
const instances = [];
|
|
118
|
+
let entities = [];
|
|
119
|
+
let constraints = [];
|
|
120
|
+
const liveBodyIds = new Set();
|
|
121
|
+
const joints = new Map();
|
|
122
|
+
const connections = [];
|
|
123
|
+
let explode = 0;
|
|
124
|
+
/** Defensive normalize — a corrupted joint op must not break the endpoint. */
|
|
125
|
+
const safeSpec = (raw) => {
|
|
126
|
+
try {
|
|
127
|
+
return normalizeJointSpec(raw, 'joint');
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
for (const op of ops) {
|
|
134
|
+
switch (op.kind) {
|
|
135
|
+
case 'create_prim':
|
|
136
|
+
case 'extrude_profile':
|
|
137
|
+
case 'loft':
|
|
138
|
+
case 'revolve':
|
|
139
|
+
case 'sweep':
|
|
140
|
+
liveBodyIds.add(op.bodyId);
|
|
141
|
+
break;
|
|
142
|
+
case 'pattern': {
|
|
143
|
+
// Mirror the worker's unique-copy rule so replay-derived ids match
|
|
144
|
+
// (repeat patterns of the same target uniquify against live ids).
|
|
145
|
+
const count = Math.max(1, Math.trunc(op.count));
|
|
146
|
+
for (let i = 1; i < count; i++) {
|
|
147
|
+
let seq = i;
|
|
148
|
+
let id = `${op.target}p${seq}`;
|
|
149
|
+
while (liveBodyIds.has(id))
|
|
150
|
+
id = `${op.target}p${++seq}`;
|
|
151
|
+
liveBodyIds.add(id);
|
|
152
|
+
}
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
case 'delete':
|
|
156
|
+
liveBodyIds.delete(op.target);
|
|
157
|
+
break;
|
|
158
|
+
case 'boolean':
|
|
159
|
+
for (const tool of op.tools)
|
|
160
|
+
liveBodyIds.delete(tool);
|
|
161
|
+
break;
|
|
162
|
+
case 'assembly_insert':
|
|
163
|
+
instances.push({
|
|
164
|
+
instanceId: op.instanceId,
|
|
165
|
+
bodyId: op.bodyId,
|
|
166
|
+
name: op.name ?? bodyNames[op.bodyId] ?? op.bodyId,
|
|
167
|
+
translate: op.translate ?? [0, 0, 0],
|
|
168
|
+
rotate: op.rotate ?? [0, 0, 0],
|
|
169
|
+
});
|
|
170
|
+
break;
|
|
171
|
+
case 'assembly_transform': {
|
|
172
|
+
const instance = instances.find((entry) => entry.instanceId === op.instanceId);
|
|
173
|
+
if (instance !== undefined) {
|
|
174
|
+
if (op.translate !== undefined)
|
|
175
|
+
instance.translate = op.translate;
|
|
176
|
+
if (op.rotate !== undefined)
|
|
177
|
+
instance.rotate = op.rotate;
|
|
178
|
+
}
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
case 'assembly_remove': {
|
|
182
|
+
const index = instances.findIndex((entry) => entry.instanceId === op.instanceId);
|
|
183
|
+
if (index !== -1)
|
|
184
|
+
instances.splice(index, 1);
|
|
185
|
+
joints.delete(op.instanceId);
|
|
186
|
+
// Connections referencing the removed instance (either side) go with it.
|
|
187
|
+
for (let i = connections.length - 1; i >= 0; i--) {
|
|
188
|
+
if (connections[i].child === op.instanceId || connections[i].parent === op.instanceId)
|
|
189
|
+
connections.splice(i, 1);
|
|
190
|
+
}
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
case 'assembly_joint': {
|
|
194
|
+
const spec = safeSpec(op.joint);
|
|
195
|
+
if (spec !== null) {
|
|
196
|
+
const own = joints.get(op.instanceId) ?? {};
|
|
197
|
+
own[op.label] = spec;
|
|
198
|
+
joints.set(op.instanceId, own);
|
|
199
|
+
}
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
case 'assembly_connect': {
|
|
203
|
+
connections.push({ child: op.instanceId, parent: op.parent, parentLabel: op.parentLabel, childLabel: op.childLabel });
|
|
204
|
+
if (op.value !== undefined) {
|
|
205
|
+
const parentSpec = joints.get(op.parent)?.[op.parentLabel];
|
|
206
|
+
const childSpec = joints.get(op.instanceId)?.[op.childLabel];
|
|
207
|
+
if (parentSpec !== undefined && childSpec !== undefined) {
|
|
208
|
+
try {
|
|
209
|
+
const applied = applyConnectValue(parentSpec, childSpec, op.value, `connect ${op.parentLabel}`);
|
|
210
|
+
joints.get(op.parent)[op.parentLabel] = applied.parent;
|
|
211
|
+
const childOwn = joints.get(op.instanceId);
|
|
212
|
+
if (childOwn !== undefined)
|
|
213
|
+
childOwn[op.childLabel] = applied.child;
|
|
214
|
+
}
|
|
215
|
+
catch { /* invalid connect value — the connection still records */ }
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
case 'assembly_joint_value': {
|
|
221
|
+
const own = joints.get(op.instanceId);
|
|
222
|
+
const spec = own?.[op.label];
|
|
223
|
+
if (own !== undefined && spec !== undefined) {
|
|
224
|
+
try {
|
|
225
|
+
own[op.label] = mergeJointValue(spec, op.value, `joint ${op.label}`);
|
|
226
|
+
}
|
|
227
|
+
catch { /* invalid drive value — keep the previous spec */ }
|
|
228
|
+
}
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
case 'assembly_explode': {
|
|
232
|
+
explode = typeof op.factor === 'number' && Number.isFinite(op.factor) ? op.factor : 0;
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
case 'constraints':
|
|
236
|
+
// The last declaration wins (a clear records an empty model).
|
|
237
|
+
entities = op.model.entities;
|
|
238
|
+
constraints = op.model.constraints;
|
|
239
|
+
break;
|
|
240
|
+
default:
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return { instances, entities, constraints, liveBodyIds, joints, connections, explode };
|
|
245
|
+
}
|
|
246
|
+
/** Build the assembly-tree payload from a restored document. */
|
|
247
|
+
export function assemblyTreePayload(doc) {
|
|
248
|
+
const { instances, entities, constraints, liveBodyIds, joints, connections, explode } = foldAssemblyState(doc.ops, doc.bodyNames);
|
|
249
|
+
const names = assemblyDisplayNames(instances);
|
|
250
|
+
const nameByInstanceId = new Map(instances.map((instance, index) => [instance.instanceId, names[index]]));
|
|
251
|
+
const entityName = (id) => {
|
|
252
|
+
if (typeof id !== 'number')
|
|
253
|
+
return undefined;
|
|
254
|
+
const entity = entities.find((entry) => entry.id === id);
|
|
255
|
+
if (entity?.instance !== undefined)
|
|
256
|
+
return nameByInstanceId.get(entity.instance) ?? entity.instance;
|
|
257
|
+
return `#${id}`;
|
|
258
|
+
};
|
|
259
|
+
// One link string per joint side (a pairing renders on both its joints).
|
|
260
|
+
const linkOf = new Map();
|
|
261
|
+
for (const connection of connections) {
|
|
262
|
+
const parentName = nameByInstanceId.get(connection.parent) ?? connection.parent;
|
|
263
|
+
const childName = nameByInstanceId.get(connection.child) ?? connection.child;
|
|
264
|
+
const text = `${connection.parentLabel} ↔ ${connection.childLabel}`;
|
|
265
|
+
linkOf.set(`${connection.parent}\u0000${connection.parentLabel}`, `${parentName} ${text}`);
|
|
266
|
+
linkOf.set(`${connection.child}\u0000${connection.childLabel}`, `${text} ${childName}`);
|
|
267
|
+
}
|
|
268
|
+
const jointRows = [];
|
|
269
|
+
for (const instance of instances) {
|
|
270
|
+
const own = joints.get(instance.instanceId);
|
|
271
|
+
if (own === undefined)
|
|
272
|
+
continue;
|
|
273
|
+
for (const [label, spec] of Object.entries(own)) {
|
|
274
|
+
const row = {
|
|
275
|
+
instanceId: instance.instanceId,
|
|
276
|
+
label,
|
|
277
|
+
type: spec.type,
|
|
278
|
+
owner: nameByInstanceId.get(instance.instanceId) ?? instance.instanceId,
|
|
279
|
+
};
|
|
280
|
+
const value = jointValueText(spec);
|
|
281
|
+
if (value !== '')
|
|
282
|
+
row.value = value;
|
|
283
|
+
const link = linkOf.get(`${instance.instanceId}\u0000${label}`);
|
|
284
|
+
if (link !== undefined)
|
|
285
|
+
row.link = link;
|
|
286
|
+
jointRows.push(row);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return {
|
|
290
|
+
docId: doc.docId,
|
|
291
|
+
version: doc.version,
|
|
292
|
+
parts: instances.map((instance, index) => ({
|
|
293
|
+
instanceId: instance.instanceId,
|
|
294
|
+
bodyId: instance.bodyId,
|
|
295
|
+
name: names[index],
|
|
296
|
+
color: instanceColor(index),
|
|
297
|
+
// Missing only when BOTH witnesses lack the body: liveBodyIds (folded
|
|
298
|
+
// from the op log — covers pattern copies in documents written before
|
|
299
|
+
// copy names were recorded, issue #6) and bodyNames (the recorded name
|
|
300
|
+
// manifest).
|
|
301
|
+
missing: !liveBodyIds.has(instance.bodyId) && doc.bodyNames[instance.bodyId] === undefined,
|
|
302
|
+
})),
|
|
303
|
+
constraints: constraints.map((constraint) => {
|
|
304
|
+
const entry = { id: constraint.id, type: String(constraint.kind.type ?? 'constraint') };
|
|
305
|
+
if (constraint.label !== undefined)
|
|
306
|
+
entry.label = constraint.label;
|
|
307
|
+
const a = entityName(constraint.kind.a);
|
|
308
|
+
const b = entityName(constraint.kind.b);
|
|
309
|
+
if (a !== undefined)
|
|
310
|
+
entry.a = a;
|
|
311
|
+
if (b !== undefined)
|
|
312
|
+
entry.b = b;
|
|
313
|
+
return entry;
|
|
314
|
+
}),
|
|
315
|
+
joints: jointRows,
|
|
316
|
+
...(Math.abs(explode) > 1e-12 ? { explode } : {}),
|
|
317
|
+
};
|
|
318
|
+
}
|
|
@@ -651,6 +651,79 @@ function createCadorangeAdapter(mods) {
|
|
|
651
651
|
return C.exportStepAssembly([{ name: rootName, color: null, transform: null, children }])
|
|
652
652
|
}
|
|
653
653
|
|
|
654
|
+
// ── structured STEP import (cadorange 0.10.0, occt.ts 2.0.0) ──────────────
|
|
655
|
+
|
|
656
|
+
/** Euler degrees out of a row-major 3×3 Rx·Ry·Rz rotation (gimbal ±90° ry). */
|
|
657
|
+
function matrixEuler(r) {
|
|
658
|
+
const sy = Math.min(1, Math.max(-1, r[2] || 0))
|
|
659
|
+
const ry = Math.asin(sy) * RAD2DEG
|
|
660
|
+
if (Math.abs(Math.abs(sy) - 1) < 1e-9) {
|
|
661
|
+
return [((sy > 0 ? 1 : -1) * Math.atan2(r[3] || 0, r[4] || 0)) * RAD2DEG, ry, 0]
|
|
662
|
+
}
|
|
663
|
+
return [
|
|
664
|
+
Math.atan2(-(r[5] || 0), r[8] || 0) * RAD2DEG,
|
|
665
|
+
ry,
|
|
666
|
+
Math.atan2(-(r[1] || 0), r[0] || 0) * RAD2DEG,
|
|
667
|
+
]
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Parse a STEP assembly into unique parts + placed leaves. Goes through
|
|
672
|
+
* `Assembly.fromStep` — occt.ts 2.0.0 returns world-baked leaf solids with
|
|
673
|
+
* parent-relative node transforms, and fromStep un-bakes them back to local
|
|
674
|
+
* geometry + world placements (its round-trip contract). Parts dedupe by
|
|
675
|
+
* rounded (volume, area, bbox) of the LOCAL solid — instanced products in
|
|
676
|
+
* the file map to one body referenced by N instances.
|
|
677
|
+
*/
|
|
678
|
+
function importStepDocument(bytes) {
|
|
679
|
+
const doc = C.importStepAssembly(bytes)
|
|
680
|
+
const asm = C.Assembly.fromStep(doc)
|
|
681
|
+
const parts = []
|
|
682
|
+
const keyIndex = new Map()
|
|
683
|
+
const leaves = []
|
|
684
|
+
const round = (value) => Math.round(value * 1000) / 1000
|
|
685
|
+
for (const leaf of asm.leafNodes()) {
|
|
686
|
+
const solid = leaf.solid
|
|
687
|
+
if (solid === undefined) continue
|
|
688
|
+
const meta = solid.meta()
|
|
689
|
+
const bb = meta.bbox
|
|
690
|
+
const key = [
|
|
691
|
+
round(meta.volume), round(meta.area),
|
|
692
|
+
...bb.min.map(round), ...bb.max.map(round),
|
|
693
|
+
].join('|')
|
|
694
|
+
let index = keyIndex.get(key)
|
|
695
|
+
if (index === undefined) {
|
|
696
|
+
index = parts.length
|
|
697
|
+
keyIndex.set(key, index)
|
|
698
|
+
parts.push({ name: leaf.name ?? `part${index + 1}`, shape: solid, key })
|
|
699
|
+
}
|
|
700
|
+
// worldLoc: columns (xDir, zDir×xDir, zDir) + origin → placement matrix.
|
|
701
|
+
const world = leaf.worldLoc()
|
|
702
|
+
const x = world.xDir
|
|
703
|
+
const z = world.zDir
|
|
704
|
+
const y = [z[1] * x[2] - z[2] * x[1], z[2] * x[0] - z[0] * x[2], z[0] * x[1] - z[1] * x[0]]
|
|
705
|
+
leaves.push({
|
|
706
|
+
part: index,
|
|
707
|
+
name: leaf.name ?? parts[index].name,
|
|
708
|
+
translate: [...world.origin],
|
|
709
|
+
rotate: matrixEuler([x[0], y[0], z[0], x[1], y[1], z[1], x[2], y[2], z[2]]),
|
|
710
|
+
})
|
|
711
|
+
}
|
|
712
|
+
if (parts.length === 0) throw new Error('the STEP file carries no solid geometry (empty or non-solid document)')
|
|
713
|
+
return { parts, leaves }
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/** A session-owned copy of a shape (import materialization: every body
|
|
717
|
+
* gets its own kernel object so disposing one never dangles another). */
|
|
718
|
+
function copyShape(shape) {
|
|
719
|
+
const raw = occ.toRaw(asOcc(shape))
|
|
720
|
+
try {
|
|
721
|
+
return CO.fromRaw(raw)
|
|
722
|
+
} finally {
|
|
723
|
+
if (typeof raw.delete === 'function') raw.delete()
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
654
727
|
// ── engineering-drawing HLR (same kernel session, no STEP round-trip) ──────
|
|
655
728
|
|
|
656
729
|
/**
|
|
@@ -720,6 +793,8 @@ function createCadorangeAdapter(mods) {
|
|
|
720
793
|
faceNormals,
|
|
721
794
|
exportFile,
|
|
722
795
|
exportStepDocument,
|
|
796
|
+
importStepDocument,
|
|
797
|
+
copyShape,
|
|
723
798
|
hiddenLineViewsOf,
|
|
724
799
|
sketchWirePoints,
|
|
725
800
|
sketchFaceMesh,
|