cyclecad 0.1.9 → 0.2.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.
@@ -0,0 +1,1102 @@
1
+ # OpenCascade.js Integration Plan for cycleCAD
2
+
3
+ ## Overview
4
+
5
+ This document details the integration strategy for real-time STEP/IGES/BREP import and B-rep (Boundary Representation) modeling operations into cycleCAD using OpenCascade WASM technology. This enables replacement of current mesh approximations with true parametric, topologically-correct geometry operations.
6
+
7
+ **Status**: Phase A (Q2 2026) — Ready for implementation
8
+ **Priority**: High (competitive necessity vs. OnShape, Fusion 360, Aurorin)
9
+ **Effort Estimate**: 3–4 weeks (including testing and optimization)
10
+
11
+ ---
12
+
13
+ ## 1. Technology Decision Matrix
14
+
15
+ ### Competing Solutions
16
+
17
+ | Solution | Strengths | Weaknesses | Recommendation |
18
+ |----------|-----------|-----------|-----------------|
19
+ | **occt-import-js** | UMD via CDN, mature, proven (ExplodeView uses it), ≤100KB WASM | Import-only, no modeling API, triangulation only | ✅ **Phase A (import)** |
20
+ | **opencascade.js** | Full API (Boolean, Fillet, etc.), ~3MB WASM, multi-threaded, maintained | Larger bundle, complex bundling, steeper learning curve | ✅ **Phase B (modeling)** |
21
+ | **replicad** | High-level API, builder pattern, polished | Thin wrapper, depends on opencascade.js, less control | Consider Phase C |
22
+ | **bitbybit.dev** | Production-ready, 32/64/MT builds, STEP assembly support | Proprietary licensing model, npm-only | Enterprise option |
23
+ | **Chili3D** | Full CAD app reference, TypeScript, OSS, modern | Heavy, monorepo structure, not a library | Reference only |
24
+
25
+ ### Recommended Approach: Two-Phase Strategy
26
+
27
+ **Phase A (Immediate)**: Use **occt-import-js** for STEP/IGES import
28
+ **Phase B (Q3 2026)**: Upgrade to **opencascade.js** for true B-rep operations
29
+ **Phase C (Q4+)**: Consider replicad wrapper for better ergonomics
30
+
31
+ ---
32
+
33
+ ## 2. Phase A: STEP Import via occt-import-js (Weeks 1–2)
34
+
35
+ ### 2.1 Current Architecture (cycleCAD)
36
+
37
+ **File**: `/app/index.html`, `/app/js/export.js`, `/app/js/viewport.js`
38
+
39
+ Current geometry pipeline:
40
+ ```
41
+ Sketch → Extrude/Revolve → Mesh (THREE.BufferGeometry) → Viewport
42
+ ```
43
+
44
+ Operations are **visual approximations** (torus for fillet, cone for chamfer, visual booleans).
45
+
46
+ ### 2.2 Integration Pattern (from ExplodeView)
47
+
48
+ ExplodeView's proven implementation (app.js lines 1077–1156):
49
+
50
+ ```javascript
51
+ // 1. Lazy-load UMD script (not ES module)
52
+ async function getOcct() {
53
+ if (typeof window.occtimportjs === 'undefined') {
54
+ await new Promise((resolve, reject) => {
55
+ const script = document.createElement('script');
56
+ script.src = 'https://cdn.jsdelivr.net/npm/occt-import-js@0.0.23/dist/occt-import-js.js';
57
+ script.onload = resolve;
58
+ script.onerror = () => reject(new Error('Failed to load OpenCASCADE WASM'));
59
+ document.head.appendChild(script);
60
+ });
61
+ }
62
+ return await window.occtimportjs();
63
+ }
64
+
65
+ // 2. Parse STEP file
66
+ async function loadSTEP(arrayBuffer) {
67
+ const occt = await getOcct();
68
+ const fileBuffer = new Uint8Array(arrayBuffer);
69
+ const result = occt.ReadStepFile(fileBuffer, null);
70
+
71
+ if (!result.success) {
72
+ throw new Error('Failed to parse STEP file');
73
+ }
74
+
75
+ return result; // { meshes: [...], success: true }
76
+ }
77
+
78
+ // 3. Convert to Three.js geometry
79
+ function meshToThree(occtMesh) {
80
+ const geometry = new THREE.BufferGeometry();
81
+ const posArr = new Float32Array(occtMesh.attributes.position.array);
82
+ geometry.setAttribute('position', new THREE.BufferAttribute(posArr, 3));
83
+
84
+ if (occtMesh.attributes.normal) {
85
+ const normArr = new Float32Array(occtMesh.attributes.normal.array);
86
+ geometry.setAttribute('normal', new THREE.BufferAttribute(normArr, 3));
87
+ } else {
88
+ geometry.computeVertexNormals();
89
+ }
90
+
91
+ if (occtMesh.index) {
92
+ const idxArr = new Uint32Array(occtMesh.index.array);
93
+ geometry.setIndex(new THREE.BufferAttribute(idxArr, 1));
94
+ }
95
+
96
+ return geometry;
97
+ }
98
+ ```
99
+
100
+ ### 2.3 Implementation for cycleCAD
101
+
102
+ **New file**: `/app/js/step-importer.js` (~300 lines)
103
+
104
+ ```javascript
105
+ // app/js/step-importer.js
106
+
107
+ import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js';
108
+
109
+ let _occtLoaded = null;
110
+
111
+ /**
112
+ * Lazy-load occt-import-js from CDN
113
+ * Returns the occt API object
114
+ */
115
+ export async function getOcctAPI() {
116
+ if (_occtLoaded) return _occtLoaded;
117
+
118
+ if (typeof window.occtimportjs === 'undefined') {
119
+ await new Promise((resolve, reject) => {
120
+ const script = document.createElement('script');
121
+ script.src = 'https://cdn.jsdelivr.net/npm/occt-import-js@0.0.23/dist/occt-import-js.js';
122
+ script.onload = () => resolve();
123
+ script.onerror = () => reject(new Error('Failed to load occt-import-js WASM engine'));
124
+ document.head.appendChild(script);
125
+ });
126
+ }
127
+
128
+ _occtLoaded = await window.occtimportjs();
129
+ return _occtLoaded;
130
+ }
131
+
132
+ /**
133
+ * Import STEP/IGES/BREP file and return Three.js Group
134
+ * @param {ArrayBuffer} fileBuffer - File data
135
+ * @param {string} ext - File extension: 'step'|'stp'|'iges'|'igs'|'brep'
136
+ * @param {object} params - Triangulation params {linearDeflection: 0.1, angularDeflection: 0.5}
137
+ * @returns {THREE.Group} Group containing all imported meshes
138
+ */
139
+ export async function importCADFile(fileBuffer, ext, params = null) {
140
+ const occt = await getOcctAPI();
141
+ const uint8Array = new Uint8Array(fileBuffer);
142
+
143
+ let result;
144
+ const extLower = ext.toLowerCase();
145
+
146
+ try {
147
+ if (extLower === 'iges' || extLower === 'igs') {
148
+ result = occt.ReadIgesFile(uint8Array, params);
149
+ } else if (extLower === 'brep') {
150
+ result = occt.ReadBrepFile(uint8Array, params);
151
+ } else if (extLower === 'step' || extLower === 'stp') {
152
+ result = occt.ReadStepFile(uint8Array, params);
153
+ } else {
154
+ throw new Error(`Unsupported format: ${ext}`);
155
+ }
156
+ } catch (e) {
157
+ throw new Error(`OpenCASCADE parse error: ${e.message}`);
158
+ }
159
+
160
+ if (!result.success) {
161
+ throw new Error(`Failed to parse ${ext.toUpperCase()} file`);
162
+ }
163
+
164
+ // Convert OCCT meshes to Three.js Group
165
+ const group = new THREE.Group();
166
+
167
+ for (let i = 0; i < result.meshes.length; i++) {
168
+ const occtMesh = result.meshes[i];
169
+ const mesh = occtMeshToThreeMesh(occtMesh, i);
170
+ group.add(mesh);
171
+ }
172
+
173
+ console.log(`[STEP Import] Loaded ${result.meshes.length} meshes from ${ext.toUpperCase()}`);
174
+ return group;
175
+ }
176
+
177
+ /**
178
+ * Convert a single OCCT mesh to THREE.Mesh
179
+ * @param {object} occtMesh - OCCT mesh object
180
+ * @param {number} index - Mesh index for naming
181
+ * @returns {THREE.Mesh}
182
+ */
183
+ function occtMeshToThreeMesh(occtMesh, index) {
184
+ const geometry = new THREE.BufferGeometry();
185
+
186
+ // Position data — ensure Float32Array
187
+ const positions = occtMesh.attributes.position.array;
188
+ const posArray = positions instanceof Float32Array
189
+ ? positions
190
+ : new Float32Array(positions);
191
+ geometry.setAttribute('position', new THREE.BufferAttribute(posArray, 3));
192
+
193
+ // Normal data — ensure Float32Array
194
+ if (occtMesh.attributes.normal) {
195
+ const normals = occtMesh.attributes.normal.array;
196
+ const normArray = normals instanceof Float32Array
197
+ ? normals
198
+ : new Float32Array(normals);
199
+ geometry.setAttribute('normal', new THREE.BufferAttribute(normArray, 3));
200
+ } else {
201
+ geometry.computeVertexNormals();
202
+ }
203
+
204
+ // Index data — ensure Uint32Array
205
+ if (occtMesh.index) {
206
+ const indices = occtMesh.index.array;
207
+ let idxArray;
208
+ if (indices instanceof Uint32Array) {
209
+ idxArray = indices;
210
+ } else if (indices instanceof Uint16Array) {
211
+ idxArray = indices; // Keep as-is
212
+ } else {
213
+ idxArray = new Uint32Array(indices);
214
+ }
215
+ geometry.setIndex(new THREE.BufferAttribute(idxArray, 1));
216
+ }
217
+
218
+ // Material with color from OCCT if available
219
+ let color = 0xa8b5bc; // Default grey
220
+ if (occtMesh.color) {
221
+ color = new THREE.Color(
222
+ occtMesh.color[0] / 255,
223
+ occtMesh.color[1] / 255,
224
+ occtMesh.color[2] / 255
225
+ );
226
+ }
227
+
228
+ const material = new THREE.MeshStandardMaterial({
229
+ color: color,
230
+ metalness: 0.35,
231
+ roughness: 0.4,
232
+ side: THREE.DoubleSide
233
+ });
234
+
235
+ const mesh = new THREE.Mesh(geometry, material);
236
+ mesh.castShadow = true;
237
+ mesh.receiveShadow = true;
238
+ mesh.name = `Imported_${index}`;
239
+
240
+ return mesh;
241
+ }
242
+
243
+ /**
244
+ * Handle file drop/upload in UI
245
+ * @param {File} file - File from drop/input
246
+ * @returns {Promise<THREE.Group>}
247
+ */
248
+ export async function handleCADFileUpload(file) {
249
+ const ext = file.name.split('.').pop();
250
+ const supportedExts = ['step', 'stp', 'iges', 'igs', 'brep'];
251
+
252
+ if (!supportedExts.includes(ext.toLowerCase())) {
253
+ throw new Error(`Unsupported format: ${ext}`);
254
+ }
255
+
256
+ const arrayBuffer = await file.arrayBuffer();
257
+ return importCADFile(arrayBuffer, ext);
258
+ }
259
+
260
+ /**
261
+ * Get triangulation parameters based on model size
262
+ * @param {THREE.Group} group - Imported group
263
+ * @returns {object} Suggested params for re-import with different quality
264
+ */
265
+ export function getTriangulationParams(boundingBox) {
266
+ const size = boundingBox.getSize(new THREE.Vector3());
267
+ const diagonal = size.length();
268
+
269
+ // Suggested parameters based on model size
270
+ return {
271
+ coarse: {
272
+ linearDeflection: diagonal * 0.01,
273
+ angularDeflection: 10
274
+ },
275
+ medium: {
276
+ linearDeflection: diagonal * 0.005,
277
+ angularDeflection: 5
278
+ },
279
+ fine: {
280
+ linearDeflection: diagonal * 0.001,
281
+ angularDeflection: 1
282
+ }
283
+ };
284
+ }
285
+ ```
286
+
287
+ ### 2.4 UI Integration
288
+
289
+ **Update**: `/app/index.html` toolbar and file input
290
+
291
+ ```html
292
+ <!-- In the toolbar, add File menu -->
293
+ <div id="file-menu" class="dropdown">
294
+ <button class="toolbar-btn">📁 File</button>
295
+ <div class="dropdown-content">
296
+ <button onclick="handleImportClick()">Import STEP/IGES</button>
297
+ <button onclick="handleExportSTEP()">Export as STEP</button>
298
+ </div>
299
+ </div>
300
+
301
+ <!-- Hidden file input for import -->
302
+ <input
303
+ type="file"
304
+ id="cad-file-input"
305
+ style="display: none"
306
+ accept=".step,.stp,.iges,.igs,.brep"
307
+ onchange="handleCADFileInputChange(event)"
308
+ />
309
+ ```
310
+
311
+ **New inline script in index.html**:
312
+
313
+ ```javascript
314
+ // Import handler
315
+ async function handleImportClick() {
316
+ document.getElementById('cad-file-input').click();
317
+ }
318
+
319
+ async function handleCADFileInputChange(event) {
320
+ const file = event.target.files[0];
321
+ if (!file) return;
322
+
323
+ try {
324
+ // Show loading spinner
325
+ document.getElementById('loading-spinner').style.display = 'block';
326
+
327
+ // Import the file
328
+ const group = await window.stepImporter.handleCADFileUpload(file);
329
+
330
+ // Clear current scene
331
+ viewport.clearScene();
332
+
333
+ // Add imported geometry to scene
334
+ viewport.scene.add(group);
335
+
336
+ // Fit to view
337
+ const bbox = new THREE.Box3().setFromObject(group);
338
+ viewport.fitToObject(group);
339
+
340
+ // Show success message
341
+ showToast(`✓ Imported: ${file.name} (${group.children.length} parts)`);
342
+
343
+ // Update feature tree
344
+ updateTreeForImportedModel(group);
345
+
346
+ } catch (error) {
347
+ console.error('[Import Error]', error);
348
+ showToast(`❌ Import failed: ${error.message}`, 'error');
349
+ } finally {
350
+ document.getElementById('loading-spinner').style.display = 'none';
351
+ event.target.value = ''; // Reset input
352
+ }
353
+ }
354
+ ```
355
+
356
+ ### 2.5 Export to STEP (Limitation & Workaround)
357
+
358
+ **Current limitation**: occt-import-js is **import-only**. For STEP export, we need opencascade.js (Phase B) or use a workaround:
359
+
360
+ ```javascript
361
+ // Phase A workaround: Export current mesh as STL, offer STEP import
362
+ export async function exportAsSTL(mesh, filename) {
363
+ // Use existing export.js meshToSTL()
364
+ const stlData = meshToSTL(mesh);
365
+ downloadFile(stlData, filename, 'application/octet-stream');
366
+ }
367
+
368
+ // Phase B: Real STEP export (future)
369
+ // export async function exportAsSTEP(geometry) {
370
+ // const oc = await opencascade.js();
371
+ // const shape = /* convert Three.js geometry to OCP TopoDS_Shape */;
372
+ // const stepData = oc.StepWriter().WriteStepFile(shape);
373
+ // downloadFile(stepData, filename, 'application/step');
374
+ // }
375
+ ```
376
+
377
+ ---
378
+
379
+ ## 3. Phase B: Real B-rep Modeling via opencascade.js (Weeks 3–4 of Iteration 2)
380
+
381
+ ### 3.1 Why opencascade.js (not occt-import-js)
382
+
383
+ | Capability | occt-import-js | opencascade.js |
384
+ |-----------|-----------------|-----------------|
385
+ | STEP Import | ✅ | ✅ |
386
+ | STEP Export | ❌ | ✅ |
387
+ | Boolean Union/Cut/Intersect | ❌ | ✅ |
388
+ | Real Fillet | ❌ | ✅ |
389
+ | Real Chamfer | ❌ | ✅ |
390
+ | Shell/Offset | ❌ | ✅ |
391
+ | Sweep/Loft from B-rep | ❌ | ✅ |
392
+ | Bundle Size | 100 KB | 3 MB |
393
+ | API Stability | Stable | Evolving |
394
+
395
+ ### 3.2 opencascade.js Setup
396
+
397
+ **Installation** (ES module + CDN hybrid):
398
+
399
+ ```javascript
400
+ // Option 1: npm + bundler (recommended for production)
401
+ // npm install opencascade.js
402
+ // import * as OC from 'opencascade.js';
403
+
404
+ // Option 2: CDN + UMD (for GitHub Pages/no build step)
405
+ async function getOpenCascade() {
406
+ if (typeof window.opencascade !== 'undefined') {
407
+ return window.opencascade;
408
+ }
409
+
410
+ return new Promise((resolve, reject) => {
411
+ const script = document.createElement('script');
412
+ script.src = 'https://cdn.jsdelivr.net/npm/opencascade.js@1.0.2/dist/opencascade.wasm.js';
413
+ script.onload = () => {
414
+ if (typeof window.opencascade !== 'undefined') {
415
+ resolve(window.opencascade);
416
+ } else {
417
+ reject(new Error('opencascade.js failed to load'));
418
+ }
419
+ };
420
+ script.onerror = reject;
421
+ document.head.appendChild(script);
422
+ });
423
+ }
424
+ ```
425
+
426
+ ### 3.3 Core Operations: Boolean Union, Cut, Intersect
427
+
428
+ **New file**: `/app/js/brep-operations.js` (~500 lines)
429
+
430
+ ```javascript
431
+ // app/js/brep-operations.js
432
+
433
+ let _ocCached = null;
434
+
435
+ async function getOC() {
436
+ if (!_ocCached) {
437
+ const oc = await window.opencascadeLoaded;
438
+ _ocCached = oc;
439
+ }
440
+ return _ocCached;
441
+ }
442
+
443
+ /**
444
+ * Perform Boolean Union on two shapes
445
+ * @param {TopoDS_Shape} shapeA - First shape
446
+ * @param {TopoDS_Shape} shapeB - Second shape
447
+ * @returns {TopoDS_Shape} Union result
448
+ */
449
+ export async function booleanUnion(shapeA, shapeB) {
450
+ const oc = await getOC();
451
+ const algo = new oc.BRepAlgoAPI_Fuse(shapeA, shapeB);
452
+ algo.Build();
453
+
454
+ if (!algo.IsDone()) {
455
+ throw new Error('Boolean union failed');
456
+ }
457
+
458
+ return algo.Shape();
459
+ }
460
+
461
+ /**
462
+ * Perform Boolean Cut (A - B)
463
+ * @param {TopoDS_Shape} shapeA - Base shape
464
+ * @param {TopoDS_Shape} shapeB - Shape to subtract
465
+ * @returns {TopoDS_Shape} Cut result
466
+ */
467
+ export async function booleanCut(shapeA, shapeB) {
468
+ const oc = await getOC();
469
+ const algo = new oc.BRepAlgoAPI_Cut(shapeA, shapeB);
470
+ algo.Build();
471
+
472
+ if (!algo.IsDone()) {
473
+ throw new Error('Boolean cut failed');
474
+ }
475
+
476
+ return algo.Shape();
477
+ }
478
+
479
+ /**
480
+ * Perform Boolean Intersection
481
+ * @param {TopoDS_Shape} shapeA - First shape
482
+ * @param {TopoDS_Shape} shapeB - Second shape
483
+ * @returns {TopoDS_Shape} Intersection result
484
+ */
485
+ export async function booleanIntersect(shapeA, shapeB) {
486
+ const oc = await getOC();
487
+ const algo = new oc.BRepAlgoAPI_Common(shapeA, shapeB);
488
+ algo.Build();
489
+
490
+ if (!algo.IsDone()) {
491
+ throw new Error('Boolean intersection failed');
492
+ }
493
+
494
+ return algo.Shape();
495
+ }
496
+
497
+ /**
498
+ * Apply fillet to edges of a shape
499
+ * @param {TopoDS_Shape} shape - Input shape
500
+ * @param {number[]} edgeIndices - Indices of edges to fillet
501
+ * @param {number} radius - Fillet radius in mm
502
+ * @returns {TopoDS_Shape} Filleted shape
503
+ */
504
+ export async function applyFillet(shape, edgeIndices, radius) {
505
+ const oc = await getOC();
506
+ const fillet = new oc.BRepFilletAPI_MakeFillet(shape);
507
+
508
+ // Select edges to fillet
509
+ const explorer = new oc.TopExp_Explorer(shape, oc.TopAbs_EdgeType.TopAbs_EDGE);
510
+ let edgeIdx = 0;
511
+
512
+ while (explorer.More()) {
513
+ const edge = oc.TopoDS.Edge(explorer.Current());
514
+
515
+ if (edgeIndices.includes(edgeIdx)) {
516
+ fillet.Add(radius, edge);
517
+ }
518
+
519
+ edgeIdx++;
520
+ explorer.Next();
521
+ }
522
+
523
+ fillet.Build();
524
+
525
+ if (!fillet.IsDone()) {
526
+ throw new Error('Fillet operation failed');
527
+ }
528
+
529
+ return fillet.Shape();
530
+ }
531
+
532
+ /**
533
+ * Apply chamfer to edges
534
+ * @param {TopoDS_Shape} shape - Input shape
535
+ * @param {number[]} edgeIndices - Indices of edges
536
+ * @param {number} distance - Chamfer distance (mm)
537
+ * @returns {TopoDS_Shape} Chamfered shape
538
+ */
539
+ export async function applyChamfer(shape, edgeIndices, distance) {
540
+ const oc = await getOC();
541
+ const chamfer = new oc.BRepFilletAPI_MakeChamfer(shape);
542
+
543
+ const explorer = new oc.TopExp_Explorer(shape, oc.TopAbs_EdgeType.TopAbs_EDGE);
544
+ let edgeIdx = 0;
545
+
546
+ while (explorer.More()) {
547
+ const edge = oc.TopoDS.Edge(explorer.Current());
548
+
549
+ if (edgeIndices.includes(edgeIdx)) {
550
+ chamfer.Add(distance, edge);
551
+ }
552
+
553
+ edgeIdx++;
554
+ explorer.Next();
555
+ }
556
+
557
+ chamfer.Build();
558
+
559
+ if (!chamfer.IsDone()) {
560
+ throw new Error('Chamfer operation failed');
561
+ }
562
+
563
+ return chamfer.Shape();
564
+ }
565
+
566
+ /**
567
+ * Create a shell/offset surface from a solid
568
+ * @param {TopoDS_Shape} shape - Input solid
569
+ * @param {number} offset - Offset distance (mm)
570
+ * @returns {TopoDS_Shape} Offset shape
571
+ */
572
+ export async function createShell(shape, offset) {
573
+ const oc = await getOC();
574
+ const shellMaker = new oc.BRepOffsetAPI_MakeOffset();
575
+ shellMaker.Initialize(shape, offset, 1e-5);
576
+ shellMaker.MakeOffset();
577
+
578
+ if (!shellMaker.IsDone()) {
579
+ throw new Error('Shell operation failed');
580
+ }
581
+
582
+ return shellMaker.Shape();
583
+ }
584
+
585
+ /**
586
+ * Convert TopoDS_Shape to Three.js geometry
587
+ * Requires mesh generation (triangulation)
588
+ * @param {TopoDS_Shape} shape - OpenCascade shape
589
+ * @param {number} linearDeflection - Mesh quality (default 0.01)
590
+ * @returns {THREE.BufferGeometry}
591
+ */
592
+ export async function shapeToThreeGeometry(shape, linearDeflection = 0.01) {
593
+ const oc = await getOC();
594
+
595
+ // Triangulate the shape
596
+ const aParams = new oc.BRepMesh_IncrementalMesh(shape, linearDeflection, false, 0.5, true);
597
+
598
+ // Extract triangles
599
+ const triangles = [];
600
+ const vertices = [];
601
+ const indices = [];
602
+ const vertexMap = new Map();
603
+
604
+ // Iterate over faces
605
+ const faceExplorer = new oc.TopExp_Explorer(shape, oc.TopAbs_FaceType.TopAbs_FACE);
606
+
607
+ while (faceExplorer.More()) {
608
+ const face = oc.TopoDS.Face(faceExplorer.Current());
609
+
610
+ // Get triangulation
611
+ let location = new oc.TopLoc_Location();
612
+ const triangulation = oc.BRep_Tool.Triangulation(face, location);
613
+
614
+ if (triangulation) {
615
+ const nodes = triangulation.Nodes();
616
+ const triangles = triangulation.Triangles();
617
+
618
+ // Add vertices
619
+ for (let i = 0; i < nodes.Length(); i++) {
620
+ const pnt = nodes.Value(i + 1);
621
+ vertices.push(pnt.X(), pnt.Y(), pnt.Z());
622
+ }
623
+
624
+ // Add indices
625
+ for (let i = 0; i < triangles.Length(); i++) {
626
+ const tri = triangles.Value(i + 1);
627
+ indices.push(tri.X() - 1, tri.Y() - 1, tri.Z() - 1);
628
+ }
629
+ }
630
+
631
+ faceExplorer.Next();
632
+ }
633
+
634
+ // Create Three.js geometry
635
+ const geometry = new THREE.BufferGeometry();
636
+ geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(vertices), 3));
637
+ geometry.setIndex(new THREE.BufferAttribute(new Uint32Array(indices), 1));
638
+ geometry.computeVertexNormals();
639
+
640
+ return geometry;
641
+ }
642
+ ```
643
+
644
+ ### 3.4 Integration with operations.js
645
+
646
+ Replace current approximations with real B-rep operations:
647
+
648
+ ```javascript
649
+ // In operations.js, update the boolean operations
650
+
651
+ export async function boolean(operandAIdx, operandBIdx, operation) {
652
+ const operandA = window.allParts[operandAIdx];
653
+ const operandB = window.allParts[operandBIdx];
654
+
655
+ if (!operandA || !operandB) {
656
+ throw new Error('Invalid operands');
657
+ }
658
+
659
+ // Convert current mesh geometry to TopoDS_Shape
660
+ // (requires mesh-to-shape reconstruction — see Phase C)
661
+
662
+ const ocShape = await window.brepOps.meshToShape(operandA.geometry);
663
+ const toolShape = await window.brepOps.meshToShape(operandB.geometry);
664
+
665
+ let resultShape;
666
+
667
+ switch (operation) {
668
+ case 'union':
669
+ resultShape = await window.brepOps.booleanUnion(ocShape, toolShape);
670
+ break;
671
+ case 'cut':
672
+ resultShape = await window.brepOps.booleanCut(ocShape, toolShape);
673
+ break;
674
+ case 'intersect':
675
+ resultShape = await window.brepOps.booleanIntersect(ocShape, toolShape);
676
+ break;
677
+ default:
678
+ throw new Error(`Unknown operation: ${operation}`);
679
+ }
680
+
681
+ // Convert back to Three.js
682
+ const resultGeometry = await window.brepOps.shapeToThreeGeometry(resultShape, 0.01);
683
+
684
+ // Create feature history entry
685
+ window.history.push({
686
+ type: 'boolean',
687
+ operandA: operandAIdx,
688
+ operandB: operandBIdx,
689
+ operation: operation,
690
+ geometry: resultGeometry
691
+ });
692
+
693
+ return resultGeometry;
694
+ }
695
+ ```
696
+
697
+ ---
698
+
699
+ ## 4. Performance Optimization
700
+
701
+ ### 4.1 Web Worker Thread Pool
702
+
703
+ For heavy operations (boolean cuts on complex models), use Web Workers to avoid blocking the UI:
704
+
705
+ **New file**: `/app/js/geometry-worker.js` (Worker script)
706
+
707
+ ```javascript
708
+ // app/js/geometry-worker.js
709
+
710
+ let opencascade = null;
711
+
712
+ self.onmessage = async (event) => {
713
+ const { id, task, data } = event.data;
714
+
715
+ try {
716
+ let result;
717
+
718
+ switch (task) {
719
+ case 'booleanCut':
720
+ result = await performBooleanCut(data.shapeA, data.shapeB);
721
+ break;
722
+ case 'applyFillet':
723
+ result = await performFillet(data.shape, data.radius);
724
+ break;
725
+ case 'triangulate':
726
+ result = await triangulateShape(data.shape);
727
+ break;
728
+ default:
729
+ throw new Error(`Unknown task: ${task}`);
730
+ }
731
+
732
+ self.postMessage({ id, status: 'success', result });
733
+ } catch (error) {
734
+ self.postMessage({ id, status: 'error', error: error.message });
735
+ }
736
+ };
737
+
738
+ async function performBooleanCut(shapeA, shapeB) {
739
+ // Deserialize, perform operation, serialize result
740
+ // ...
741
+ }
742
+ ```
743
+
744
+ **Main thread usage**:
745
+
746
+ ```javascript
747
+ export async function booleanCutAsync(shapeA, shapeB) {
748
+ return new Promise((resolve, reject) => {
749
+ const id = Math.random();
750
+ const worker = new Worker('/app/js/geometry-worker.js');
751
+
752
+ worker.onmessage = (e) => {
753
+ if (e.data.id === id) {
754
+ if (e.data.status === 'success') {
755
+ resolve(e.data.result);
756
+ } else {
757
+ reject(new Error(e.data.error));
758
+ }
759
+ worker.terminate();
760
+ }
761
+ };
762
+
763
+ worker.postMessage({ id, task: 'booleanCut', data: { shapeA, shapeB } });
764
+
765
+ // Timeout after 30s
766
+ setTimeout(() => {
767
+ worker.terminate();
768
+ reject(new Error('Geometry operation timeout'));
769
+ }, 30000);
770
+ });
771
+ }
772
+ ```
773
+
774
+ ### 4.2 Caching & Incremental Updates
775
+
776
+ Store computed shapes to avoid re-computation:
777
+
778
+ ```javascript
779
+ class FeatureHistory {
780
+ constructor() {
781
+ this.features = [];
782
+ this.shapeCache = new Map(); // featureIdx -> TopoDS_Shape
783
+ }
784
+
785
+ async getShapeAt(featureIdx) {
786
+ if (this.shapeCache.has(featureIdx)) {
787
+ return this.shapeCache.get(featureIdx);
788
+ }
789
+
790
+ // Recompute from scratch or from parent
791
+ const shape = await this.computeFeature(featureIdx);
792
+ this.shapeCache.set(featureIdx, shape);
793
+ return shape;
794
+ }
795
+
796
+ invalidateCache(fromIdx) {
797
+ // Clear cache from this feature onward
798
+ for (let i = fromIdx; i < this.features.length; i++) {
799
+ this.shapeCache.delete(i);
800
+ }
801
+ }
802
+ }
803
+ ```
804
+
805
+ ### 4.3 Memory Management
806
+
807
+ WASM modules can consume significant memory. Implement cleanup:
808
+
809
+ ```javascript
810
+ export function cleanupOCCT() {
811
+ // Delete unused shapes from OCCT memory
812
+ if (window.shapeCacheOC) {
813
+ for (const [key, shape] of window.shapeCacheOC) {
814
+ // OpenCascade.js cleanup
815
+ // shape.delete?.(); // if destructor exposed
816
+ }
817
+ window.shapeCacheOC.clear();
818
+ }
819
+ }
820
+
821
+ // Call on app cleanup or periodically
822
+ window.addEventListener('beforeunload', cleanupOCCT);
823
+ ```
824
+
825
+ ---
826
+
827
+ ## 5. STEP Export Implementation
828
+
829
+ Once opencascade.js is integrated, enable real STEP export:
830
+
831
+ ```javascript
832
+ // app/js/step-exporter.js
833
+
834
+ export async function exportShapeAsSTEP(shape, filename) {
835
+ const oc = await getOpenCascade();
836
+
837
+ // Create a STEP writer
838
+ const writer = new oc.STEPCAFControl_Writer();
839
+
840
+ // Add shape to document
841
+ const doc = new oc.TDocStd_Document('STEP');
842
+ const handle = new oc.Handle_TDataStd_TreeNode();
843
+ // ... complex document building ...
844
+
845
+ // Write STEP file
846
+ const stepStatus = writer.Write('output.step');
847
+
848
+ if (stepStatus !== oc.IFSelect_ReturnStatus.IFSelect_RetOK) {
849
+ throw new Error('STEP export failed');
850
+ }
851
+
852
+ // Read file and download
853
+ const stepData = fs.readFileSync('output.step');
854
+ downloadFile(stepData, filename, 'application/step');
855
+ }
856
+ ```
857
+
858
+ ---
859
+
860
+ ## 6. Testing Strategy
861
+
862
+ ### 6.1 Unit Tests
863
+
864
+ ```javascript
865
+ // test/step-importer.test.js
866
+
867
+ import { importCADFile, handleCADFileUpload } from '../app/js/step-importer.js';
868
+
869
+ describe('STEP Importer', () => {
870
+ test('should import STEP file and return Three.js Group', async () => {
871
+ const file = new File([stepFileBuffer], 'test.stp');
872
+ const group = await handleCADFileUpload(file);
873
+
874
+ expect(group).toBeInstanceOf(THREE.Group);
875
+ expect(group.children.length).toBeGreaterThan(0);
876
+ });
877
+
878
+ test('should handle IGES files', async () => {
879
+ const file = new File([igesFileBuffer], 'test.iges');
880
+ const group = await handleCADFileUpload(file);
881
+
882
+ expect(group).toBeInstanceOf(THREE.Group);
883
+ });
884
+ });
885
+ ```
886
+
887
+ ### 6.2 Integration Tests (with real DUO files)
888
+
889
+ Test against the actual Inventor project:
890
+
891
+ ```bash
892
+ # In /sessions/sharp-modest-allen/test-step-import.js
893
+
894
+ const { importCADFile } = require('./mnt/cyclecad/app/js/step-importer.js');
895
+ const fs = require('fs');
896
+
897
+ // Test against DUO main assembly
898
+ const duoPath = '/sessions/sharp-modest-allen/mnt/cyclecad/example/DUO Durchgehend Inventor/Zusatzoptionen/DUOdurch/D-ZBG-DUO-Anlage.iam';
899
+ const buffer = fs.readFileSync(duoPath);
900
+
901
+ importCADFile(buffer, 'iam').then(group => {
902
+ console.log(`✓ Loaded DUO assembly: ${group.children.length} parts`);
903
+
904
+ // Validate geometry
905
+ group.children.forEach((mesh, idx) => {
906
+ console.log(` Part ${idx}: ${mesh.geometry.attributes.position.array.length / 3} vertices`);
907
+ });
908
+ }).catch(err => {
909
+ console.error(`✗ Import failed: ${err.message}`);
910
+ });
911
+ ```
912
+
913
+ ### 6.3 Performance Benchmarks
914
+
915
+ Track operation time for optimization:
916
+
917
+ ```javascript
918
+ class PerformanceMonitor {
919
+ static timings = {};
920
+
921
+ static mark(label) {
922
+ this.timings[label] = { start: performance.now() };
923
+ }
924
+
925
+ static measure(label) {
926
+ if (!this.timings[label]) return null;
927
+ const duration = performance.now() - this.timings[label].start;
928
+ console.log(`[Perf] ${label}: ${duration.toFixed(2)}ms`);
929
+ return duration;
930
+ }
931
+ }
932
+
933
+ // Usage:
934
+ PerformanceMonitor.mark('boolean-cut');
935
+ const result = await booleanCut(shapeA, shapeB);
936
+ PerformanceMonitor.measure('boolean-cut');
937
+ ```
938
+
939
+ ---
940
+
941
+ ## 7. Migration Path: From Approximations to B-rep
942
+
943
+ ### Current State (v0.1.7)
944
+ - Fillet: torus radius approximation
945
+ - Chamfer: cone approximation
946
+ - Boolean: visual only, no topology
947
+ - Export: STL only
948
+
949
+ ### After Phase A (Week 2)
950
+ - STEP/IGES import ✅
951
+ - Existing operations unchanged
952
+ - Both import & model operations coexist
953
+
954
+ ### After Phase B (Week 4)
955
+ - Real fillet/chamfer replace approximations
956
+ - True boolean operations
957
+ - STEP export enabled
958
+ - Mesh approximation becomes optional backup
959
+
960
+ ### Transition Strategy
961
+
962
+ To avoid breaking existing models:
963
+
964
+ ```javascript
965
+ // In operations.js
966
+
967
+ export async function applyFillet(edges, radius, useRealBrEp = true) {
968
+ if (useRealBrEp && window.brepOps) {
969
+ try {
970
+ return await window.brepOps.applyFillet(shape, edges, radius);
971
+ } catch (e) {
972
+ console.warn('B-rep fillet failed, falling back to approximation', e);
973
+ return applyFilletApproximation(edges, radius); // Old method
974
+ }
975
+ } else {
976
+ return applyFilletApproximation(edges, radius);
977
+ }
978
+ }
979
+ ```
980
+
981
+ ---
982
+
983
+ ## 8. Reference Architectures
984
+
985
+ ### Chili3D Pattern (TypeScript + Monorepo)
986
+ - `/packages/chili-wasm/` — OpenCASCADE WASM bindings
987
+ - `/packages/chili-core/` — Core data model (FeatureTree, Document)
988
+ - `/packages/chili/` — 66+ CAD commands
989
+ - `/packages/chili-three/` — Three.js rendering bridge
990
+
991
+ **Lesson**: Separate geometry (WASM) from UI/rendering
992
+
993
+ ### bitbybit.dev Pattern (npm Package + Rendering Agnostic)
994
+ - `@bitbybit-dev/occt` — Pure CAD engine
995
+ - Rendering: BabylonJS, Three.js, PlayCanvas (user's choice)
996
+ - Assembly support: XCAF colors preserved
997
+ - Build variants: 32-bit, 64-bit, 64-bit MT
998
+
999
+ **Lesson**: Decouple CAD kernel from rendering for flexibility
1000
+
1001
+ ### ExplodeView Pattern (occt-import-js for Import Only)
1002
+ - UMD script lazy-load (no build system needed)
1003
+ - Convert OCCT meshes → THREE.BufferGeometry
1004
+ - Works on GitHub Pages without bundler
1005
+
1006
+ **Lesson**: occt-import-js sufficient for viewers, but insufficient for modelers
1007
+
1008
+ ---
1009
+
1010
+ ## 9. Remaining Gaps (Phase C+)
1011
+
1012
+ The following require additional work beyond Phase A-B:
1013
+
1014
+ 1. **Mesh-to-Shape Reconstruction** — Convert Three.js mesh → TopoDS_Shape
1015
+ - Needed for: booleans on parametric features
1016
+ - Solution: Implement mesh→B-rep reconstruction algorithm
1017
+ - Complexity: Medium (use OCCT BRepBuilderAPI_Sewing)
1018
+
1019
+ 2. **Constraints from B-rep** — Extract dimensions/constraints from imported STEP
1020
+ - Needed for: Parametric editing of imported models
1021
+ - Solution: Parse STEP XML for dimension/constraint metadata
1022
+ - Complexity: High
1023
+
1024
+ 3. **Assembly Mate Constraints** — Import assembly relationships
1025
+ - Needed for: Exploded view, kinematics
1026
+ - Solution: Parse STEP assembly structure (via bitbybit.dev reference)
1027
+ - Complexity: High
1028
+
1029
+ 4. **Persistent Feature Tree** — Save/load STEP as feature history
1030
+ - Needed for: Round-trip parametric editing
1031
+ - Solution: Store geometry history + STEP references
1032
+ - Complexity: Medium
1033
+
1034
+ ---
1035
+
1036
+ ## 10. Checklist & Timeline
1037
+
1038
+ ### Phase A: STEP Import (Week 1–2)
1039
+ - [ ] Create `/app/js/step-importer.js` (~300 lines)
1040
+ - [ ] Add file upload UI in index.html
1041
+ - [ ] Test with DUO assembly files
1042
+ - [ ] Update feature tree for imported models
1043
+ - [ ] Commit and deploy to GitHub Pages
1044
+ - [ ] Document usage in README
1045
+
1046
+ ### Phase B: Real B-rep Operations (Week 3–4)
1047
+ - [ ] Integrate opencascade.js via CDN
1048
+ - [ ] Create `/app/js/brep-operations.js` (~500 lines)
1049
+ - [ ] Replace fillet/chamfer approximations
1050
+ - [ ] Implement real boolean union/cut/intersect
1051
+ - [ ] Add Web Worker for heavy operations
1052
+ - [ ] Test boolean ops on parametric features
1053
+ - [ ] Implement STEP export
1054
+ - [ ] Update operations.js to use new ops
1055
+ - [ ] Performance benchmark against competitors
1056
+
1057
+ ### Phase C & Beyond
1058
+ - [ ] Mesh-to-shape reconstruction (needed for parametric booleans)
1059
+ - [ ] Constraint extraction from STEP
1060
+ - [ ] Assembly kinematics
1061
+ - [ ] Persistent feature serialization
1062
+
1063
+ ---
1064
+
1065
+ ## 11. References & Resources
1066
+
1067
+ ### Official Documentation
1068
+ - [OpenCascade.js Official Docs](https://ocjs.org/)
1069
+ - [occt-import-js Repository](https://github.com/kovacsv/occt-import-js)
1070
+ - [OpenCascade.js GitHub](https://github.com/donalffons/opencascade.js)
1071
+ - [Open CASCADE Technology Docs](https://dev.opencascade.org/doc/overview/html/)
1072
+
1073
+ ### Reference Projects
1074
+ - **Chili3D** (TypeScript, full CAD app): [xiangechen/chili3d](https://github.com/xiangechen/chili3d)
1075
+ - **bitbybit.dev** (npm package, production): [bitbybit.dev](https://learn.bitbybit.dev/)
1076
+ - **ReplicAD** (high-level API): [sgenoud/replicad](https://github.com/sgenoud/replicad)
1077
+ - **ExplodeView** (existing, uses occt-import-js): `/sessions/sharp-modest-allen/mnt/explodeview/docs/demo/app.js` (lines 1077–1156)
1078
+
1079
+ ### Articles
1080
+ - [WebAssembly for CAD: When JavaScript Isn't Fast Enough](https://altersquare.medium.com/webassembly-for-cad-applications-when-javascript-isnt-fast-enough-56fcdc892004)
1081
+ - [CadQuery WASM Discussion](https://github.com/CadQuery/cadquery/discussions/1876)
1082
+
1083
+ ### Competitors' Tech Stacks
1084
+ - **OnShape**: Proprietary, cloud C++ kernel
1085
+ - **Fusion 360**: Desktop + cloud, closed-source
1086
+ - **Aurorin CAD**: Uses replicad + custom UI
1087
+ - **MecAgent**: SolidWorks plugin, not browser-native
1088
+
1089
+ ---
1090
+
1091
+ ## Conclusion
1092
+
1093
+ This integration plan provides a phased, low-risk approach to adding STEP import and real B-rep operations to cycleCAD. Phase A (occt-import-js) enables file import on GitHub Pages without bundling complexity. Phase B (opencascade.js) upgrades to full modeling capabilities, directly competing with OnShape and Fusion 360 on critical features.
1094
+
1095
+ **Key Decision**: occt-import-js for Phase A ensures quick wins and maintains ES module architecture. opencascade.js in Phase B provides the full power needed for the killer app.
1096
+
1097
+ **Competitive Advantage**: By Q3 2026, cycleCAD will offer:
1098
+ - Free STEP import (vs. OnShape's $1,500/yr paywall)
1099
+ - Real B-rep operations (vs. Aurorin's limited mesh editing)
1100
+ - Agent-first API for AI workflows (unique)
1101
+ - Open source & self-hostable (vs. proprietary cloud)
1102
+