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.
- package/AGENT_API_IMPLEMENTATION_SUMMARY.md +399 -0
- package/AGENT_API_MANIFEST.md +343 -0
- package/AGENT_API_QUICKSTART.md +316 -0
- package/AGENT_API_WIRING.md +495 -0
- package/CLAUDE.md +120 -8
- package/DELIVERABLES.txt +471 -0
- package/app/agent-demo.html +1990 -1294
- package/app/agent-test.html +486 -0
- package/app/index.html +236 -5
- package/app/js/agent-api.js +953 -98
- package/app/js/viewer-mode.js +899 -0
- package/architecture.html +372 -0
- package/docs/EXPLODEVIEW-FEATURE-MAPPING.md +602 -0
- package/docs/README-VIEWER-MODE-MERGE.md +364 -0
- package/docs/VIEWER-MODE-IMPLEMENTATION-GUIDE.md +412 -0
- package/docs/explodeview-merge-plan.md +476 -0
- package/docs/opencascade-integration.md +1102 -0
- package/linkedin-post.md +24 -0
- package/package.json +1 -1
|
@@ -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
|
+
|