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.
@@ -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;