cyclecad 1.3.2 → 1.3.3
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/DRAWING_MODULE_INTEGRATION.md +633 -0
- package/README.md +138 -317
- package/app/index.html +2 -0
- package/app/js/brep-kernel.js +853 -0
- package/app/js/kernel.js +684 -0
- package/app/js/modules/assembly-module.js +582 -0
- package/app/js/modules/brep-module.js +583 -0
- package/app/js/modules/drawing-module.js +883 -0
- package/app/js/modules/operations-module.js +660 -0
- package/app/js/modules/simulation-module.js +834 -0
- package/app/js/modules/sketch-module.js +720 -0
- package/app/js/modules/step-module.js +510 -0
- package/app/js/modules/viewport-module.js +530 -0
- package/fusion360-gap-analysis.html +636 -0
- package/package.json +1 -1
|
@@ -0,0 +1,853 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* B-Rep Kernel Module for cycleCAD
|
|
3
|
+
*
|
|
4
|
+
* Wraps OpenCascade.js (WASM build of OpenCASCADE) to provide real solid modeling.
|
|
5
|
+
* Lazy-loads the 50MB WASM file on first geometry operation.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* const kernel = window.brepKernel;
|
|
9
|
+
* const box = await kernel.makeBox(10, 20, 30);
|
|
10
|
+
* const mesh = await kernel.shapeToMesh(box.shape);
|
|
11
|
+
* scene.add(mesh);
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
class BRepKernel {
|
|
15
|
+
constructor() {
|
|
16
|
+
this.oc = null; // OpenCascade instance (loaded on demand)
|
|
17
|
+
this.shapeCache = new Map(); // { id: TopoDS_Shape }
|
|
18
|
+
this.nextShapeId = 0; // Auto-incrementing shape IDs
|
|
19
|
+
this.isInitializing = false; // Prevent double-init
|
|
20
|
+
this.initPromise = null; // Promise for async init
|
|
21
|
+
|
|
22
|
+
// CDN paths for OpenCascade.js
|
|
23
|
+
this.OCCDNBase = 'https://cdn.jsdelivr.net/npm/opencascade.js@2.0.0-beta.b5ff984/dist/';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Initialize OpenCascade.js WASM lazily
|
|
28
|
+
* Called automatically on first operation
|
|
29
|
+
*/
|
|
30
|
+
async init() {
|
|
31
|
+
// Return cached promise if already initializing/initialized
|
|
32
|
+
if (this.oc) return this.oc;
|
|
33
|
+
if (this.initPromise) return this.initPromise;
|
|
34
|
+
if (this.isInitializing) return this.initPromise;
|
|
35
|
+
|
|
36
|
+
this.isInitializing = true;
|
|
37
|
+
this.initPromise = this._initOpenCascade();
|
|
38
|
+
return this.initPromise;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async _initOpenCascade() {
|
|
42
|
+
try {
|
|
43
|
+
console.log('[BRepKernel] Initializing OpenCascade.js WASM...');
|
|
44
|
+
|
|
45
|
+
// Load the full OpenCascade.js library
|
|
46
|
+
// This is a large file (~50MB WASM + 400KB JS)
|
|
47
|
+
// The library exports as window.Module (Emscripten pattern)
|
|
48
|
+
|
|
49
|
+
// Save any existing Module to avoid conflicts
|
|
50
|
+
const savedModule = window.Module;
|
|
51
|
+
|
|
52
|
+
return new Promise((resolve, reject) => {
|
|
53
|
+
// Create and load script
|
|
54
|
+
const script = document.createElement('script');
|
|
55
|
+
script.src = this.OCCDNBase + 'opencascade.full.js';
|
|
56
|
+
script.async = true;
|
|
57
|
+
|
|
58
|
+
script.onload = async () => {
|
|
59
|
+
try {
|
|
60
|
+
// Get the factory function that was set by the script
|
|
61
|
+
const occFactory = window.Module;
|
|
62
|
+
|
|
63
|
+
if (!occFactory) {
|
|
64
|
+
throw new Error('OpenCascade.js Module not found after script load');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Initialize with custom locateFile to load .wasm from CDN
|
|
68
|
+
console.log('[BRepKernel] Loading WASM file from CDN...');
|
|
69
|
+
this.oc = await new occFactory({
|
|
70
|
+
locateFile: (file) => {
|
|
71
|
+
return this.OCCDNBase + file;
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
console.log('[BRepKernel] OpenCascade.js initialized successfully');
|
|
76
|
+
this.isInitializing = false;
|
|
77
|
+
resolve(this.oc);
|
|
78
|
+
} catch (err) {
|
|
79
|
+
console.error('[BRepKernel] Initialization error:', err);
|
|
80
|
+
this.isInitializing = false;
|
|
81
|
+
|
|
82
|
+
// Restore saved Module
|
|
83
|
+
if (savedModule !== undefined) {
|
|
84
|
+
window.Module = savedModule;
|
|
85
|
+
}
|
|
86
|
+
reject(err);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
script.onerror = () => {
|
|
91
|
+
console.error('[BRepKernel] Failed to load opencascade.full.js from CDN');
|
|
92
|
+
this.isInitializing = false;
|
|
93
|
+
|
|
94
|
+
// Restore saved Module
|
|
95
|
+
if (savedModule !== undefined) {
|
|
96
|
+
window.Module = savedModule;
|
|
97
|
+
}
|
|
98
|
+
reject(new Error('Failed to load OpenCascade.js from CDN'));
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
document.head.appendChild(script);
|
|
102
|
+
});
|
|
103
|
+
} catch (err) {
|
|
104
|
+
console.error('[BRepKernel] Fatal initialization error:', err);
|
|
105
|
+
throw err;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Helper: Generate unique shape ID
|
|
111
|
+
*/
|
|
112
|
+
_newShapeId() {
|
|
113
|
+
return `shape_${this.nextShapeId++}`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Helper: Cache and return shape
|
|
118
|
+
*/
|
|
119
|
+
_cacheShape(shape) {
|
|
120
|
+
const id = this._newShapeId();
|
|
121
|
+
this.shapeCache.set(id, shape);
|
|
122
|
+
return { id, shape };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ============================================================================
|
|
126
|
+
// PRIMITIVE OPERATIONS
|
|
127
|
+
// ============================================================================
|
|
128
|
+
|
|
129
|
+
async makeBox(width, height, depth) {
|
|
130
|
+
await this.init();
|
|
131
|
+
try {
|
|
132
|
+
const shape = new this.oc.BRepPrimAPI_MakeBox_2(width, height, depth).Shape();
|
|
133
|
+
return this._cacheShape(shape);
|
|
134
|
+
} catch (err) {
|
|
135
|
+
console.error('[BRepKernel] makeBox failed:', err);
|
|
136
|
+
throw err;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async makeCylinder(radius, height) {
|
|
141
|
+
await this.init();
|
|
142
|
+
try {
|
|
143
|
+
const shape = new this.oc.BRepPrimAPI_MakeCylinder_2(radius, height).Shape();
|
|
144
|
+
return this._cacheShape(shape);
|
|
145
|
+
} catch (err) {
|
|
146
|
+
console.error('[BRepKernel] makeCylinder failed:', err);
|
|
147
|
+
throw err;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async makeSphere(radius) {
|
|
152
|
+
await this.init();
|
|
153
|
+
try {
|
|
154
|
+
const shape = new this.oc.BRepPrimAPI_MakeSphere_3(radius).Shape();
|
|
155
|
+
return this._cacheShape(shape);
|
|
156
|
+
} catch (err) {
|
|
157
|
+
console.error('[BRepKernel] makeSphere failed:', err);
|
|
158
|
+
throw err;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async makeCone(radius1, radius2, height) {
|
|
163
|
+
await this.init();
|
|
164
|
+
try {
|
|
165
|
+
const shape = new this.oc.BRepPrimAPI_MakeCone_3(radius1, radius2, height).Shape();
|
|
166
|
+
return this._cacheShape(shape);
|
|
167
|
+
} catch (err) {
|
|
168
|
+
console.error('[BRepKernel] makeCone failed:', err);
|
|
169
|
+
throw err;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async makeTorus(majorRadius, minorRadius) {
|
|
174
|
+
await this.init();
|
|
175
|
+
try {
|
|
176
|
+
const shape = new this.oc.BRepPrimAPI_MakeTorus_2(majorRadius, minorRadius).Shape();
|
|
177
|
+
return this._cacheShape(shape);
|
|
178
|
+
} catch (err) {
|
|
179
|
+
console.error('[BRepKernel] makeTorus failed:', err);
|
|
180
|
+
throw err;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ============================================================================
|
|
185
|
+
// SHAPE OPERATIONS
|
|
186
|
+
// ============================================================================
|
|
187
|
+
|
|
188
|
+
async extrude(shapeIdOrShape, direction, distance) {
|
|
189
|
+
await this.init();
|
|
190
|
+
try {
|
|
191
|
+
const shape = typeof shapeIdOrShape === 'string'
|
|
192
|
+
? this.shapeCache.get(shapeIdOrShape)
|
|
193
|
+
: shapeIdOrShape;
|
|
194
|
+
|
|
195
|
+
if (!shape) throw new Error('Shape not found');
|
|
196
|
+
|
|
197
|
+
// Create direction vector
|
|
198
|
+
const dir = new this.oc.gp_Dir_3(direction.x, direction.y, direction.z);
|
|
199
|
+
|
|
200
|
+
// Use BRepPrimAPI_MakePrism for extrusion
|
|
201
|
+
const prism = new this.oc.BRepPrimAPI_MakePrism_2(shape, dir, distance, false);
|
|
202
|
+
const result = prism.Shape();
|
|
203
|
+
|
|
204
|
+
return this._cacheShape(result);
|
|
205
|
+
} catch (err) {
|
|
206
|
+
console.error('[BRepKernel] extrude failed:', err);
|
|
207
|
+
throw err;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async revolve(shapeIdOrShape, axis, angle) {
|
|
212
|
+
await this.init();
|
|
213
|
+
try {
|
|
214
|
+
const shape = typeof shapeIdOrShape === 'string'
|
|
215
|
+
? this.shapeCache.get(shapeIdOrShape)
|
|
216
|
+
: shapeIdOrShape;
|
|
217
|
+
|
|
218
|
+
if (!shape) throw new Error('Shape not found');
|
|
219
|
+
|
|
220
|
+
// Create axis (gp_Ax1)
|
|
221
|
+
// axis = { origin: { x, y, z }, direction: { x, y, z } }
|
|
222
|
+
const origin = new this.oc.gp_Pnt_3(axis.origin.x, axis.origin.y, axis.origin.z);
|
|
223
|
+
const dir = new this.oc.gp_Dir_3(axis.direction.x, axis.direction.y, axis.direction.z);
|
|
224
|
+
const ax1 = new this.oc.gp_Ax1_2(origin, dir);
|
|
225
|
+
|
|
226
|
+
// Use BRepPrimAPI_MakeRevolution
|
|
227
|
+
const rev = new this.oc.BRepPrimAPI_MakeRevolution_2(ax1, shape, angle * Math.PI / 180, false);
|
|
228
|
+
const result = rev.Shape();
|
|
229
|
+
|
|
230
|
+
return this._cacheShape(result);
|
|
231
|
+
} catch (err) {
|
|
232
|
+
console.error('[BRepKernel] revolve failed:', err);
|
|
233
|
+
throw err;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async fillet(shapeIdOrShape, edgeIndices, radius) {
|
|
238
|
+
await this.init();
|
|
239
|
+
try {
|
|
240
|
+
const shape = typeof shapeIdOrShape === 'string'
|
|
241
|
+
? this.shapeCache.get(shapeIdOrShape)
|
|
242
|
+
: shapeIdOrShape;
|
|
243
|
+
|
|
244
|
+
if (!shape) throw new Error('Shape not found');
|
|
245
|
+
|
|
246
|
+
const filler = new this.oc.BRepFilletAPI_MakeFillet(shape, this.oc.ChFi3d_Rational);
|
|
247
|
+
|
|
248
|
+
// Get edges and apply fillet
|
|
249
|
+
const edges = this._getEdgesFromShape(shape);
|
|
250
|
+
|
|
251
|
+
for (let idx of edgeIndices) {
|
|
252
|
+
if (idx < edges.length) {
|
|
253
|
+
filler.Add_2(radius, edges[idx]);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
filler.Build();
|
|
258
|
+
const result = filler.Shape();
|
|
259
|
+
|
|
260
|
+
return this._cacheShape(result);
|
|
261
|
+
} catch (err) {
|
|
262
|
+
console.error('[BRepKernel] fillet failed:', err);
|
|
263
|
+
throw err;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async chamfer(shapeIdOrShape, edgeIndices, distance) {
|
|
268
|
+
await this.init();
|
|
269
|
+
try {
|
|
270
|
+
const shape = typeof shapeIdOrShape === 'string'
|
|
271
|
+
? this.shapeCache.get(shapeIdOrShape)
|
|
272
|
+
: shapeIdOrShape;
|
|
273
|
+
|
|
274
|
+
if (!shape) throw new Error('Shape not found');
|
|
275
|
+
|
|
276
|
+
const chamferer = new this.oc.BRepFilletAPI_MakeChamfer(shape);
|
|
277
|
+
|
|
278
|
+
// Get edges and apply chamfer
|
|
279
|
+
const edges = this._getEdgesFromShape(shape);
|
|
280
|
+
|
|
281
|
+
for (let idx of edgeIndices) {
|
|
282
|
+
if (idx < edges.length) {
|
|
283
|
+
chamferer.Add_2(distance, edges[idx]);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
chamferer.Build();
|
|
288
|
+
const result = chamferer.Shape();
|
|
289
|
+
|
|
290
|
+
return this._cacheShape(result);
|
|
291
|
+
} catch (err) {
|
|
292
|
+
console.error('[BRepKernel] chamfer failed:', err);
|
|
293
|
+
throw err;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async booleanUnion(shapeId1, shapeId2) {
|
|
298
|
+
await this.init();
|
|
299
|
+
try {
|
|
300
|
+
const shape1 = this.shapeCache.get(shapeId1);
|
|
301
|
+
const shape2 = this.shapeCache.get(shapeId2);
|
|
302
|
+
|
|
303
|
+
if (!shape1 || !shape2) throw new Error('One or both shapes not found');
|
|
304
|
+
|
|
305
|
+
const fuse = new this.oc.BRepAlgoAPI_Fuse_3(
|
|
306
|
+
shape1,
|
|
307
|
+
shape2,
|
|
308
|
+
new this.oc.Message_ProgressRange_1()
|
|
309
|
+
);
|
|
310
|
+
const result = fuse.Shape();
|
|
311
|
+
|
|
312
|
+
return this._cacheShape(result);
|
|
313
|
+
} catch (err) {
|
|
314
|
+
console.error('[BRepKernel] booleanUnion failed:', err);
|
|
315
|
+
throw err;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async booleanCut(shapeIdTool, shapeIdBase) {
|
|
320
|
+
await this.init();
|
|
321
|
+
try {
|
|
322
|
+
const base = this.shapeCache.get(shapeIdBase);
|
|
323
|
+
const tool = this.shapeCache.get(shapeIdTool);
|
|
324
|
+
|
|
325
|
+
if (!base || !tool) throw new Error('One or both shapes not found');
|
|
326
|
+
|
|
327
|
+
const cut = new this.oc.BRepAlgoAPI_Cut_3(
|
|
328
|
+
base,
|
|
329
|
+
tool,
|
|
330
|
+
new this.oc.Message_ProgressRange_1()
|
|
331
|
+
);
|
|
332
|
+
const result = cut.Shape();
|
|
333
|
+
|
|
334
|
+
return this._cacheShape(result);
|
|
335
|
+
} catch (err) {
|
|
336
|
+
console.error('[BRepKernel] booleanCut failed:', err);
|
|
337
|
+
throw err;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
async booleanIntersect(shapeId1, shapeId2) {
|
|
342
|
+
await this.init();
|
|
343
|
+
try {
|
|
344
|
+
const shape1 = this.shapeCache.get(shapeId1);
|
|
345
|
+
const shape2 = this.shapeCache.get(shapeId2);
|
|
346
|
+
|
|
347
|
+
if (!shape1 || !shape2) throw new Error('One or both shapes not found');
|
|
348
|
+
|
|
349
|
+
const common = new this.oc.BRepAlgoAPI_Common_3(
|
|
350
|
+
shape1,
|
|
351
|
+
shape2,
|
|
352
|
+
new this.oc.Message_ProgressRange_1()
|
|
353
|
+
);
|
|
354
|
+
const result = common.Shape();
|
|
355
|
+
|
|
356
|
+
return this._cacheShape(result);
|
|
357
|
+
} catch (err) {
|
|
358
|
+
console.error('[BRepKernel] booleanIntersect failed:', err);
|
|
359
|
+
throw err;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
async shell(shapeIdOrShape, faceIndices, thickness) {
|
|
364
|
+
await this.init();
|
|
365
|
+
try {
|
|
366
|
+
const shape = typeof shapeIdOrShape === 'string'
|
|
367
|
+
? this.shapeCache.get(shapeIdOrShape)
|
|
368
|
+
: shapeIdOrShape;
|
|
369
|
+
|
|
370
|
+
if (!shape) throw new Error('Shape not found');
|
|
371
|
+
|
|
372
|
+
const sheller = new this.oc.BRepOffsetAPI_MakeThickSolid();
|
|
373
|
+
|
|
374
|
+
// Get faces to remove
|
|
375
|
+
const faces = this._getFacesFromShape(shape);
|
|
376
|
+
|
|
377
|
+
for (let idx of faceIndices) {
|
|
378
|
+
if (idx < faces.length) {
|
|
379
|
+
sheller.Add(faces[idx]);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
sheller.MakeThickSolidByJoin(shape, thickness, 0.01);
|
|
384
|
+
const result = sheller.Shape();
|
|
385
|
+
|
|
386
|
+
return this._cacheShape(result);
|
|
387
|
+
} catch (err) {
|
|
388
|
+
console.error('[BRepKernel] shell failed:', err);
|
|
389
|
+
throw err;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
async sweep(profileShapeId, pathShapeId) {
|
|
394
|
+
await this.init();
|
|
395
|
+
try {
|
|
396
|
+
const profile = this.shapeCache.get(profileShapeId);
|
|
397
|
+
const path = this.shapeCache.get(pathShapeId);
|
|
398
|
+
|
|
399
|
+
if (!profile || !path) throw new Error('Profile or path shape not found');
|
|
400
|
+
|
|
401
|
+
const sweeper = new this.oc.BRepOffsetAPI_MakePipe(path, profile, false);
|
|
402
|
+
const result = sweeper.Shape();
|
|
403
|
+
|
|
404
|
+
return this._cacheShape(result);
|
|
405
|
+
} catch (err) {
|
|
406
|
+
console.error('[BRepKernel] sweep failed:', err);
|
|
407
|
+
throw err;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
async loft(shapeIds) {
|
|
412
|
+
await this.init();
|
|
413
|
+
try {
|
|
414
|
+
if (!shapeIds || shapeIds.length < 2) {
|
|
415
|
+
throw new Error('Loft requires at least 2 profile shapes');
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const profiles = new this.oc.TopTools_ListOfShape_1();
|
|
419
|
+
|
|
420
|
+
for (let id of shapeIds) {
|
|
421
|
+
const shape = this.shapeCache.get(id);
|
|
422
|
+
if (!shape) throw new Error(`Shape ${id} not found`);
|
|
423
|
+
profiles.Append_1(shape);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const lofter = new this.oc.BRepOffsetAPI_MakeLoft_2(profiles, false, false);
|
|
427
|
+
const result = lofter.Shape();
|
|
428
|
+
|
|
429
|
+
return this._cacheShape(result);
|
|
430
|
+
} catch (err) {
|
|
431
|
+
console.error('[BRepKernel] loft failed:', err);
|
|
432
|
+
throw err;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
async mirror(shapeIdOrShape, plane) {
|
|
437
|
+
await this.init();
|
|
438
|
+
try {
|
|
439
|
+
const shape = typeof shapeIdOrShape === 'string'
|
|
440
|
+
? this.shapeCache.get(shapeIdOrShape)
|
|
441
|
+
: shapeIdOrShape;
|
|
442
|
+
|
|
443
|
+
if (!shape) throw new Error('Shape not found');
|
|
444
|
+
|
|
445
|
+
// Create mirror plane (gp_Pln)
|
|
446
|
+
// plane = { origin: { x, y, z }, normal: { x, y, z } }
|
|
447
|
+
const origin = new this.oc.gp_Pnt_3(plane.origin.x, plane.origin.y, plane.origin.z);
|
|
448
|
+
const normal = new this.oc.gp_Dir_3(plane.normal.x, plane.normal.y, plane.normal.z);
|
|
449
|
+
const pln = new this.oc.gp_Pln_2(origin, normal);
|
|
450
|
+
|
|
451
|
+
// Create mirror transformation
|
|
452
|
+
const trsf = new this.oc.gp_Trsf_1();
|
|
453
|
+
trsf.SetMirror_2(pln);
|
|
454
|
+
|
|
455
|
+
// Apply transformation
|
|
456
|
+
const brep = new this.oc.BRepBuilderAPI_Transform_2(shape, trsf, false);
|
|
457
|
+
const result = brep.Shape();
|
|
458
|
+
|
|
459
|
+
return this._cacheShape(result);
|
|
460
|
+
} catch (err) {
|
|
461
|
+
console.error('[BRepKernel] mirror failed:', err);
|
|
462
|
+
throw err;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
async draft(shapeIdOrShape, faceIndices, angle, pullDirection) {
|
|
467
|
+
await this.init();
|
|
468
|
+
try {
|
|
469
|
+
const shape = typeof shapeIdOrShape === 'string'
|
|
470
|
+
? this.shapeCache.get(shapeIdOrShape)
|
|
471
|
+
: shapeIdOrShape;
|
|
472
|
+
|
|
473
|
+
if (!shape) throw new Error('Shape not found');
|
|
474
|
+
|
|
475
|
+
const draftDir = new this.oc.gp_Dir_3(
|
|
476
|
+
pullDirection.x,
|
|
477
|
+
pullDirection.y,
|
|
478
|
+
pullDirection.z
|
|
479
|
+
);
|
|
480
|
+
|
|
481
|
+
const drafter = new this.oc.BRepOffsetAPI_MakeDraft(
|
|
482
|
+
shape,
|
|
483
|
+
draftDir,
|
|
484
|
+
angle * Math.PI / 180
|
|
485
|
+
);
|
|
486
|
+
|
|
487
|
+
// Get faces and add to draft
|
|
488
|
+
const faces = this._getFacesFromShape(shape);
|
|
489
|
+
for (let idx of faceIndices) {
|
|
490
|
+
if (idx < faces.length) {
|
|
491
|
+
drafter.Add_1(faces[idx]);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
drafter.Build();
|
|
496
|
+
const result = drafter.Shape();
|
|
497
|
+
|
|
498
|
+
return this._cacheShape(result);
|
|
499
|
+
} catch (err) {
|
|
500
|
+
console.error('[BRepKernel] draft failed:', err);
|
|
501
|
+
throw err;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// ============================================================================
|
|
506
|
+
// MESHING (Convert TopoDS_Shape to THREE.js BufferGeometry)
|
|
507
|
+
// ============================================================================
|
|
508
|
+
|
|
509
|
+
async shapeToMesh(shapeIdOrShape, linearDeflection = 0.1, angularDeflection = 0.5) {
|
|
510
|
+
await this.init();
|
|
511
|
+
try {
|
|
512
|
+
const shape = typeof shapeIdOrShape === 'string'
|
|
513
|
+
? this.shapeCache.get(shapeIdOrShape)
|
|
514
|
+
: shapeIdOrShape;
|
|
515
|
+
|
|
516
|
+
if (!shape) throw new Error('Shape not found');
|
|
517
|
+
|
|
518
|
+
// Mesh the shape using incremental mesh
|
|
519
|
+
const mesh = new this.oc.BRepMesh_IncrementalMesh_2(
|
|
520
|
+
shape,
|
|
521
|
+
linearDeflection,
|
|
522
|
+
false,
|
|
523
|
+
angularDeflection,
|
|
524
|
+
false
|
|
525
|
+
);
|
|
526
|
+
|
|
527
|
+
mesh.Perform();
|
|
528
|
+
|
|
529
|
+
// Extract triangles and normals
|
|
530
|
+
const geometry = new THREE.BufferGeometry();
|
|
531
|
+
const vertices = [];
|
|
532
|
+
const normals = [];
|
|
533
|
+
const indices = [];
|
|
534
|
+
let vertexCount = 0;
|
|
535
|
+
|
|
536
|
+
// Iterate over faces
|
|
537
|
+
const explorer = new this.oc.TopExp_Explorer_2(shape, this.oc.TopAbs_FACE);
|
|
538
|
+
|
|
539
|
+
while (explorer.More()) {
|
|
540
|
+
const face = this.oc.TopoDS.Face_1(explorer.Current());
|
|
541
|
+
const location = new this.oc.TopLoc_Location_1();
|
|
542
|
+
const triangulation = this.oc.BRep_Tool.Triangulation_2(face, location);
|
|
543
|
+
|
|
544
|
+
if (triangulation && triangulation.NbTriangles() > 0) {
|
|
545
|
+
const nodes = triangulation.Nodes();
|
|
546
|
+
const triangles = triangulation.Triangles();
|
|
547
|
+
|
|
548
|
+
// Add vertices
|
|
549
|
+
for (let i = 1; i <= triangulation.NbNodes(); i++) {
|
|
550
|
+
const node = nodes.Value(i);
|
|
551
|
+
vertices.push(node.X(), node.Y(), node.Z());
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Add triangles as indices
|
|
555
|
+
for (let i = 1; i <= triangulation.NbTriangles(); i++) {
|
|
556
|
+
const tri = triangles.Value(i);
|
|
557
|
+
const n = triangulation.NbNodes();
|
|
558
|
+
|
|
559
|
+
// Get vertex indices (1-based in OCC, convert to 0-based)
|
|
560
|
+
const v1 = tri.Value(1) - 1 + vertexCount;
|
|
561
|
+
const v2 = tri.Value(2) - 1 + vertexCount;
|
|
562
|
+
const v3 = tri.Value(3) - 1 + vertexCount;
|
|
563
|
+
|
|
564
|
+
indices.push(v1, v2, v3);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
vertexCount += triangulation.NbNodes();
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
explorer.Next();
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
if (vertices.length === 0) {
|
|
574
|
+
console.warn('[BRepKernel] No triangles generated from shape');
|
|
575
|
+
return null;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(vertices), 3));
|
|
579
|
+
geometry.setIndex(new THREE.BufferAttribute(new Uint32Array(indices), 1));
|
|
580
|
+
geometry.computeVertexNormals();
|
|
581
|
+
geometry.computeBoundingBox();
|
|
582
|
+
|
|
583
|
+
return geometry;
|
|
584
|
+
} catch (err) {
|
|
585
|
+
console.error('[BRepKernel] shapeToMesh failed:', err);
|
|
586
|
+
throw err;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// ============================================================================
|
|
591
|
+
// STEP I/O
|
|
592
|
+
// ============================================================================
|
|
593
|
+
|
|
594
|
+
async importSTEP(arrayBuffer) {
|
|
595
|
+
await this.init();
|
|
596
|
+
try {
|
|
597
|
+
// Write buffer to WASM filesystem
|
|
598
|
+
const filename = 'temp_import.step';
|
|
599
|
+
const bytes = new Uint8Array(arrayBuffer);
|
|
600
|
+
const stream = new this.oc.std_ofstream_1(filename);
|
|
601
|
+
|
|
602
|
+
for (let byte of bytes) {
|
|
603
|
+
stream.put_1(byte);
|
|
604
|
+
}
|
|
605
|
+
stream.close();
|
|
606
|
+
|
|
607
|
+
// Read STEP file
|
|
608
|
+
const doc = new this.oc.TDocStd_Document_1(new this.oc.TCollection_AsciiString_2('STEP'));
|
|
609
|
+
const reader = new this.oc.STEPCAFControl_Reader_1();
|
|
610
|
+
|
|
611
|
+
reader.ReadFile_1(filename);
|
|
612
|
+
reader.Transfer_1(doc, 2); // TransferMode_ShapeWrite
|
|
613
|
+
|
|
614
|
+
// Extract all shapes from document
|
|
615
|
+
const shapes = [];
|
|
616
|
+
const explorer = new this.oc.TDocStd_LabelSequence_1();
|
|
617
|
+
doc.Main().FindAttribute_2(this.oc.XCAFDoc_DocumentTool.ShapesLabel_1(), explorer);
|
|
618
|
+
|
|
619
|
+
for (let i = 1; i <= explorer.Length(); i++) {
|
|
620
|
+
const label = explorer.Value(i);
|
|
621
|
+
const shape = this.oc.XCAFDoc_DocumentTool.GetShape_1(label);
|
|
622
|
+
|
|
623
|
+
if (shape && !shape.IsNull()) {
|
|
624
|
+
shapes.push(this._cacheShape(shape));
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
console.log(`[BRepKernel] Imported ${shapes.length} shapes from STEP`);
|
|
629
|
+
return shapes;
|
|
630
|
+
} catch (err) {
|
|
631
|
+
console.error('[BRepKernel] importSTEP failed:', err);
|
|
632
|
+
throw err;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
async exportSTEP(shapeIds) {
|
|
637
|
+
await this.init();
|
|
638
|
+
try {
|
|
639
|
+
const doc = new this.oc.TDocStd_Document_1(new this.oc.TCollection_AsciiString_2('STEP'));
|
|
640
|
+
|
|
641
|
+
// Add shapes to document
|
|
642
|
+
for (let id of shapeIds) {
|
|
643
|
+
const shape = this.shapeCache.get(id);
|
|
644
|
+
if (shape) {
|
|
645
|
+
const label = doc.Main().NewChild_1();
|
|
646
|
+
this.oc.XCAFDoc_DocumentTool.SetShape_2(label, shape);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// Write to STEP file
|
|
651
|
+
const filename = 'temp_export.step';
|
|
652
|
+
const writer = new this.oc.STEPCAFControl_Writer_1();
|
|
653
|
+
|
|
654
|
+
writer.Transfer_2(doc, 2); // TransferMode_ShapeWrite
|
|
655
|
+
writer.Write_1(filename);
|
|
656
|
+
|
|
657
|
+
// Read file back as buffer
|
|
658
|
+
const stream = new this.oc.std_ifstream_1(filename);
|
|
659
|
+
const bytes = [];
|
|
660
|
+
|
|
661
|
+
while (true) {
|
|
662
|
+
const byte = stream.get_1();
|
|
663
|
+
if (byte === -1) break;
|
|
664
|
+
bytes.push(byte);
|
|
665
|
+
}
|
|
666
|
+
stream.close();
|
|
667
|
+
|
|
668
|
+
console.log(`[BRepKernel] Exported ${shapeIds.length} shapes to STEP`);
|
|
669
|
+
return new Uint8Array(bytes);
|
|
670
|
+
} catch (err) {
|
|
671
|
+
console.error('[BRepKernel] exportSTEP failed:', err);
|
|
672
|
+
throw err;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// ============================================================================
|
|
677
|
+
// SHAPE INSPECTION
|
|
678
|
+
// ============================================================================
|
|
679
|
+
|
|
680
|
+
async getEdges(shapeIdOrShape) {
|
|
681
|
+
await this.init();
|
|
682
|
+
try {
|
|
683
|
+
const shape = typeof shapeIdOrShape === 'string'
|
|
684
|
+
? this.shapeCache.get(shapeIdOrShape)
|
|
685
|
+
: shapeIdOrShape;
|
|
686
|
+
|
|
687
|
+
if (!shape) throw new Error('Shape not found');
|
|
688
|
+
|
|
689
|
+
return this._getEdgesFromShape(shape);
|
|
690
|
+
} catch (err) {
|
|
691
|
+
console.error('[BRepKernel] getEdges failed:', err);
|
|
692
|
+
throw err;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
async getFaces(shapeIdOrShape) {
|
|
697
|
+
await this.init();
|
|
698
|
+
try {
|
|
699
|
+
const shape = typeof shapeIdOrShape === 'string'
|
|
700
|
+
? this.shapeCache.get(shapeIdOrShape)
|
|
701
|
+
: shapeIdOrShape;
|
|
702
|
+
|
|
703
|
+
if (!shape) throw new Error('Shape not found');
|
|
704
|
+
|
|
705
|
+
return this._getFacesFromShape(shape);
|
|
706
|
+
} catch (err) {
|
|
707
|
+
console.error('[BRepKernel] getFaces failed:', err);
|
|
708
|
+
throw err;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
async getMassProperties(shapeIdOrShape) {
|
|
713
|
+
await this.init();
|
|
714
|
+
try {
|
|
715
|
+
const shape = typeof shapeIdOrShape === 'string'
|
|
716
|
+
? this.shapeCache.get(shapeIdOrShape)
|
|
717
|
+
: shapeIdOrShape;
|
|
718
|
+
|
|
719
|
+
if (!shape) throw new Error('Shape not found');
|
|
720
|
+
|
|
721
|
+
const props = new this.oc.GProp_GProps_1();
|
|
722
|
+
this.oc.BRepGProp.VolumeProperties_2(shape, props, false);
|
|
723
|
+
|
|
724
|
+
const cog = props.CentreOfMass();
|
|
725
|
+
|
|
726
|
+
return {
|
|
727
|
+
volume: props.Mass(),
|
|
728
|
+
area: this._getSurfaceArea(shape),
|
|
729
|
+
centerOfGravity: { x: cog.X(), y: cog.Y(), z: cog.Z() },
|
|
730
|
+
momentOfInertia: this._getMomentOfInertia(shape, props)
|
|
731
|
+
};
|
|
732
|
+
} catch (err) {
|
|
733
|
+
console.error('[BRepKernel] getMassProperties failed:', err);
|
|
734
|
+
throw err;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
async getBoundingBox(shapeIdOrShape) {
|
|
739
|
+
await this.init();
|
|
740
|
+
try {
|
|
741
|
+
const shape = typeof shapeIdOrShape === 'string'
|
|
742
|
+
? this.shapeCache.get(shapeIdOrShape)
|
|
743
|
+
: shapeIdOrShape;
|
|
744
|
+
|
|
745
|
+
if (!shape) throw new Error('Shape not found');
|
|
746
|
+
|
|
747
|
+
const bbox = new this.oc.Bnd_Box_1();
|
|
748
|
+
this.oc.BRepBndLib.Add_2(shape, bbox);
|
|
749
|
+
|
|
750
|
+
const min = new this.oc.gp_Pnt_1();
|
|
751
|
+
const max = new this.oc.gp_Pnt_1();
|
|
752
|
+
bbox.Get_1(min, max);
|
|
753
|
+
|
|
754
|
+
return {
|
|
755
|
+
min: { x: min.X(), y: min.Y(), z: min.Z() },
|
|
756
|
+
max: { x: max.X(), y: max.Y(), z: max.Z() },
|
|
757
|
+
size: {
|
|
758
|
+
x: max.X() - min.X(),
|
|
759
|
+
y: max.Y() - min.Y(),
|
|
760
|
+
z: max.Z() - min.Z()
|
|
761
|
+
}
|
|
762
|
+
};
|
|
763
|
+
} catch (err) {
|
|
764
|
+
console.error('[BRepKernel] getBoundingBox failed:', err);
|
|
765
|
+
throw err;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// ============================================================================
|
|
770
|
+
// PRIVATE HELPERS
|
|
771
|
+
// ============================================================================
|
|
772
|
+
|
|
773
|
+
_getEdgesFromShape(shape) {
|
|
774
|
+
const edges = [];
|
|
775
|
+
const explorer = new this.oc.TopExp_Explorer_2(shape, this.oc.TopAbs_EDGE);
|
|
776
|
+
|
|
777
|
+
while (explorer.More()) {
|
|
778
|
+
const edge = explorer.Current();
|
|
779
|
+
edges.push(edge);
|
|
780
|
+
explorer.Next();
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
return edges;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
_getFacesFromShape(shape) {
|
|
787
|
+
const faces = [];
|
|
788
|
+
const explorer = new this.oc.TopExp_Explorer_2(shape, this.oc.TopAbs_FACE);
|
|
789
|
+
|
|
790
|
+
while (explorer.More()) {
|
|
791
|
+
const face = explorer.Current();
|
|
792
|
+
faces.push(face);
|
|
793
|
+
explorer.Next();
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
return faces;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
_getSurfaceArea(shape) {
|
|
800
|
+
try {
|
|
801
|
+
const props = new this.oc.GProp_GProps_1();
|
|
802
|
+
this.oc.BRepGProp.SurfaceProperties_2(shape, props, false);
|
|
803
|
+
return props.Mass();
|
|
804
|
+
} catch (err) {
|
|
805
|
+
console.warn('[BRepKernel] Could not calculate surface area:', err);
|
|
806
|
+
return 0;
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
_getMomentOfInertia(shape, props) {
|
|
811
|
+
try {
|
|
812
|
+
const mat = props.MatrixOfInertia();
|
|
813
|
+
return {
|
|
814
|
+
ixx: mat.Value(1, 1),
|
|
815
|
+
iyy: mat.Value(2, 2),
|
|
816
|
+
izz: mat.Value(3, 3),
|
|
817
|
+
ixy: mat.Value(1, 2),
|
|
818
|
+
ixz: mat.Value(1, 3),
|
|
819
|
+
iyz: mat.Value(2, 3)
|
|
820
|
+
};
|
|
821
|
+
} catch (err) {
|
|
822
|
+
console.warn('[BRepKernel] Could not calculate moment of inertia:', err);
|
|
823
|
+
return null;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
/**
|
|
828
|
+
* Clear all cached shapes
|
|
829
|
+
*/
|
|
830
|
+
clearCache() {
|
|
831
|
+
this.shapeCache.clear();
|
|
832
|
+
console.log('[BRepKernel] Shape cache cleared');
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
/**
|
|
836
|
+
* Get cache statistics
|
|
837
|
+
*/
|
|
838
|
+
getCacheStats() {
|
|
839
|
+
return {
|
|
840
|
+
shapesCount: this.shapeCache.size,
|
|
841
|
+
nextId: this.nextShapeId,
|
|
842
|
+
isInitialized: !!this.oc
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// Create singleton instance and expose globally
|
|
848
|
+
const brepKernel = new BRepKernel();
|
|
849
|
+
window.brepKernel = brepKernel;
|
|
850
|
+
|
|
851
|
+
console.log('[BRepKernel] Module loaded. Call await brepKernel.init() to start.');
|
|
852
|
+
|
|
853
|
+
export default brepKernel;
|