cyclecad 1.1.1 → 1.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/app/index.html CHANGED
@@ -1387,13 +1387,14 @@
1387
1387
  <!-- Token Engine — Initialize early so window.cycleCAD.tokens is available -->
1388
1388
  <script src="./js/token-engine.js"></script>
1389
1389
  <!-- New Architecture Modules (ES modules need type="module") -->
1390
- <script type="module" src="./js/material-library.js?v=77"></script>
1390
+ <script type="module" src="./js/material-library.js?v=120"></script>
1391
1391
  <script src="./js/dfm-analyzer.js"></script>
1392
- <script type="module" src="./js/cam-pipeline.js?v=77"></script>
1392
+ <script type="module" src="./js/cam-pipeline.js?v=120"></script>
1393
1393
  <script src="./js/connected-fabs.js"></script>
1394
- <script type="module" src="./js/ai-copilot.js?v=77"></script>
1395
- <script type="module" src="./js/collaboration.js?v=77"></script>
1396
- <script type="module" src="./js/collaboration-ui.js?v=77"></script>
1394
+ <script type="module" src="./js/brep-engine.js?v=120"></script>
1395
+ <script type="module" src="./js/ai-copilot.js?v=120"></script>
1396
+ <script type="module" src="./js/collaboration.js?v=120"></script>
1397
+ <script type="module" src="./js/collaboration-ui.js?v=120"></script>
1397
1398
  <!-- CadXStudio-killer modules (IIFE, no imports) -->
1398
1399
  <script src="./js/text-to-cad.js"></script>
1399
1400
  <script src="./js/cam-operations.js"></script>
@@ -1419,7 +1420,7 @@
1419
1420
  <span class="splash-logo-cycle">cycle</span><span class="splash-logo-cad">CAD</span>
1420
1421
  </div>
1421
1422
  <p class="splash-subtitle">Parametric 3D CAD Modeler for the Mechanical Designer</p>
1422
- <p style="display:inline-block; color:#0066cc; font-size:1rem; margin:12px 0 0 0; letter-spacing:2px; font-family:monospace; font-weight:700; background:rgba(0,102,204,0.08); border:1.5px solid rgba(0,102,204,0.25); border-radius:8px; padding:5px 20px;">v1.1.1</p>
1423
+ <p style="display:inline-block; color:#0066cc; font-size:1rem; margin:12px 0 0 0; letter-spacing:2px; font-family:monospace; font-weight:700; background:rgba(0,102,204,0.08); border:1.5px solid rgba(0,102,204,0.25); border-radius:8px; padding:5px 20px;">v1.2.0</p>
1423
1424
  </div>
1424
1425
  <div class="splash-options">
1425
1426
  <button class="splash-button splash-button-primary" id="btn-empty-project" style="grid-column: 1 / -1;">
@@ -1781,27 +1782,27 @@
1781
1782
  <script type="module">
1782
1783
  import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js';
1783
1784
  const _v = '50';
1784
- import { initViewport, setView, addToScene, removeFromScene, getScene, getCamera, getControls, toggleGrid as vpToggleGrid, toggleWireframe as vpToggleWireframe, fitToObject } from './js/viewport.js?v=77';
1785
+ import { initViewport, setView, addToScene, removeFromScene, getScene, getCamera, getControls, toggleGrid as vpToggleGrid, toggleWireframe as vpToggleWireframe, fitToObject } from './js/viewport.js?v=120';
1785
1786
  // fitAll defined locally to avoid import failures from cached viewport.js
1786
1787
  function fitAll(padding = 1.2) { const s = getScene(); if (s) fitToObject(s, padding); }
1787
- import { startSketch, endSketch, setTool, getEntities, clearSketch } from './js/sketch.js?v=77';
1788
- import { extrudeProfile, createPrimitive, rebuildFeature, createMaterial } from './js/operations.js?v=77';
1789
- import { initChat, parseCADPrompt, addMessage } from './js/ai-chat.js?v=77';
1790
- import { initTree, addFeature, selectFeature, onSelect, removeFeature } from './js/tree.js?v=77';
1791
- import { initParams, showParams, onParamChange } from './js/params.js?v=77';
1792
- import { exportSTL, exportOBJ, exportJSON } from './js/export.js?v=77';
1793
- import { initShortcuts } from './js/shortcuts.js?v=77';
1794
- import { createReverseEngineerPanel, importFile, analyzeGeometry, reconstructFeatureTree, createWalkthrough } from './js/reverse-engineer.js?v=77';
1795
- import { createInventorPanel, parseInventorFile } from './js/inventor-parser.js?v=77';
1796
- import { loadProject, showFolderPicker, parseIPJ } from './js/project-loader.js?v=77';
1797
- import { initProjectBrowser, showBrowser, hideBrowser, setProject, onFileSelect } from './js/project-browser.js?v=77';
1798
- import { generateGuide, renderGuide, exportGuideHTML } from './js/rebuild-guide.js?v=77';
1799
- import { solveConstraints, addConstraint, removeConstraint, autoDetectConstraints, isFullyConstrained, getAllConstraints, clearAllConstraints } from './js/constraint-solver.js?v=77';
1800
- import { createSweep, createLoft, createBend, createFlange, createTab, createSlot, unfoldSheetMetal, createSpring, createThread } from './js/advanced-ops.js?v=77';
1801
- import Assembly from './js/assembly.js?v=77';
1802
- import { exportSketchToDXF, exportProjectionToDXF, exportMultiViewDXF, export3DDXF, downloadDXF } from './js/dxf-export.js?v=77';
1803
- import { initAgentAPI } from './js/agent-api.js?v=77';
1804
- import { initTokenDashboard } from './js/token-dashboard.js?v=77';
1788
+ import { startSketch, endSketch, setTool, getEntities, clearSketch } from './js/sketch.js?v=120';
1789
+ import { extrudeProfile, createPrimitive, rebuildFeature, createMaterial } from './js/operations.js?v=120';
1790
+ import { initChat, parseCADPrompt, addMessage } from './js/ai-chat.js?v=120';
1791
+ import { initTree, addFeature, selectFeature, onSelect, removeFeature } from './js/tree.js?v=120';
1792
+ import { initParams, showParams, onParamChange } from './js/params.js?v=120';
1793
+ import { exportSTL, exportOBJ, exportJSON } from './js/export.js?v=120';
1794
+ import { initShortcuts } from './js/shortcuts.js?v=120';
1795
+ import { createReverseEngineerPanel, importFile, analyzeGeometry, reconstructFeatureTree, createWalkthrough } from './js/reverse-engineer.js?v=120';
1796
+ import { createInventorPanel, parseInventorFile } from './js/inventor-parser.js?v=120';
1797
+ import { loadProject, showFolderPicker, parseIPJ } from './js/project-loader.js?v=120';
1798
+ import { initProjectBrowser, showBrowser, hideBrowser, setProject, onFileSelect } from './js/project-browser.js?v=120';
1799
+ import { generateGuide, renderGuide, exportGuideHTML } from './js/rebuild-guide.js?v=120';
1800
+ import { solveConstraints, addConstraint, removeConstraint, autoDetectConstraints, isFullyConstrained, getAllConstraints, clearAllConstraints } from './js/constraint-solver.js?v=120';
1801
+ import { createSweep, createLoft, createBend, createFlange, createTab, createSlot, unfoldSheetMetal, createSpring, createThread } from './js/advanced-ops.js?v=120';
1802
+ import Assembly from './js/assembly.js?v=120';
1803
+ import { exportSketchToDXF, exportProjectionToDXF, exportMultiViewDXF, export3DDXF, downloadDXF } from './js/dxf-export.js?v=120';
1804
+ import { initAgentAPI } from './js/agent-api.js?v=120';
1805
+ import { initTokenDashboard } from './js/token-dashboard.js?v=120';
1805
1806
 
1806
1807
  // ========== Application State ==========
1807
1808
  const APP = {
@@ -2015,7 +2016,7 @@
2015
2016
 
2016
2017
  // Initialize Agent API — the primary interface
2017
2018
  await tryStepAsync('agentAPI', async () => {
2018
- const agentImports = await import('./js/agent-api.js?v=77');
2019
+ const agentImports = await import('./js/agent-api.js?v=120');
2019
2020
  const agentSession = initAgentAPI({
2020
2021
  viewport: {
2021
2022
  getCamera,
@@ -2887,6 +2888,8 @@
2887
2888
 
2888
2889
  // Expose globally so Copilot + Agent API + tutorials can create geometry
2889
2890
  window._executeParsedPrompt = function(prompt) { executeParsedPrompt(prompt); };
2891
+ window.addToScene = addToScene;
2892
+ window.getScene = getScene;
2890
2893
 
2891
2894
  function executeParsedPrompt(prompt) {
2892
2895
  try {
@@ -5272,6 +5275,6 @@
5272
5275
  </div>
5273
5276
  </div>
5274
5277
 
5275
- <span id="version-badge" style="position:fixed;bottom:42px;left:50%;transform:translateX(-50%);z-index:999;font-size:0.9rem;color:rgba(255,255,255,0.9);letter-spacing:0.1em;white-space:nowrap;padding:6px 16px;user-select:all;pointer-events:auto;font-family:monospace;font-weight:700;background:rgba(0,0,0,0.7);border:1px solid rgba(88,166,255,0.4);border-radius:6px;text-shadow:0 1px 3px rgba(0,0,0,0.5);" title="cycleCAD version">cycleCAD v1.1.1</span>
5278
+ <span id="version-badge" style="position:fixed;bottom:42px;left:50%;transform:translateX(-50%);z-index:999;font-size:0.9rem;color:rgba(255,255,255,0.9);letter-spacing:0.1em;white-space:nowrap;padding:6px 16px;user-select:all;pointer-events:auto;font-family:monospace;font-weight:700;background:rgba(0,0,0,0.7);border:1px solid rgba(88,166,255,0.4);border-radius:6px;text-shadow:0 1px 3px rgba(0,0,0,0.5);" title="cycleCAD version">cycleCAD v1.2.0</span>
5276
5279
  </body>
5277
5280
  </html>
@@ -947,19 +947,62 @@ export async function executeTextCommand(prompt) {
947
947
 
948
948
  addMessage('ai', `Got it! ${preview}. Creating now...`);
949
949
 
950
- // Execute: try direct geometry creation first (fastest path)
950
+ // ── B-REP ENGINE PATH ──────────────────────────────────────────────
951
+ // Try real OpenCascade.js B-rep first. Falls back to mesh preview.
952
+ const brep = window.brepEngine;
953
+ const brepCommands = commands.map(cmd => {
954
+ const method = cmd.method || '';
955
+ const type = method.replace('shape.', '').replace('feature.', '');
956
+ return { type, params: { ...cmd.params } };
957
+ });
958
+
959
+ // Attempt B-rep execution
960
+ if (brep) {
961
+ try {
962
+ if (!brep.isReady()) {
963
+ addMessage('ai', '⏳ Loading OpenCascade.js B-rep kernel (~50MB WASM)... first time only.');
964
+ await brep.initBRep();
965
+ addMessage('ai', '✅ B-rep kernel loaded!');
966
+ }
967
+
968
+ // Remove previous B-rep mesh from scene
969
+ if (window._brepMesh) {
970
+ if (window._brepMesh.mesh?.parent) window._brepMesh.mesh.parent.remove(window._brepMesh.mesh);
971
+ if (window._brepMesh.wireframe?.parent) window._brepMesh.wireframe.parent.remove(window._brepMesh.wireframe);
972
+ }
973
+
974
+ const result = await brep.executeCommands(brepCommands);
975
+ if (result && result.mesh) {
976
+ // Add to Three.js scene
977
+ const addFn = window.addToScene || ((m) => {
978
+ const scene = window._scene || window.getScene?.();
979
+ if (scene) scene.add(m);
980
+ });
981
+ addFn(result.mesh);
982
+ addFn(result.wireframe);
983
+ window._brepMesh = result;
984
+
985
+ addMessage('ai', `🔧 **Real B-rep**: ${result.description} — solid model with true edges and faces.`);
986
+ return { ok: true, brep: true, description: result.description };
987
+ }
988
+ } catch (brepErr) {
989
+ console.warn('[Copilot] B-rep execution failed, falling back to mesh preview:', brepErr);
990
+ addMessage('ai', `⚠️ B-rep kernel error: ${brepErr.message}. Using mesh preview instead.`);
991
+ }
992
+ }
993
+
994
+ // ── MESH PREVIEW FALLBACK ──────────────────────────────────────────
951
995
  const results = [];
952
996
  for (const cmd of commands) {
953
997
  try {
954
- // Convert Agent API format {method: 'shape.cylinder', params: {}} to executeParsedPrompt format {type: 'cylinder', params: {}}
955
998
  if (window._executeParsedPrompt) {
956
999
  const method = cmd.method || '';
957
1000
  const type = method.replace('shape.', '').replace('feature.', '');
958
1001
 
959
- // Operations that modify existing geometry — skip createPrimitive, show message
1002
+ // Operations that modify existing geometry
960
1003
  const modifyOps = ['fillet', 'chamfer', 'pattern', 'mirror', 'shell'];
961
1004
  if (modifyOps.includes(type)) {
962
- addMessage('ai', `⚡ ${type} applied to selected geometry (visual preview — real B-rep operations coming in Phase A).`);
1005
+ addMessage('ai', `⚡ ${type} install B-rep kernel for real edge operations. Using visual preview.`);
963
1006
  results.push({ ok: true, method, note: 'modify-op' });
964
1007
  continue;
965
1008
  }
@@ -968,23 +1011,15 @@ export async function executeTextCommand(prompt) {
968
1011
  const count = cmd.params?.count || 1;
969
1012
  for (let ci = 0; ci < count; ci++) {
970
1013
  const p = Object.assign({}, cmd.params);
971
- // Position holes at 4 corners of a typical cube face
972
1014
  if (count > 1 && type === 'hole') {
973
- const cornerSpread = 3.5; // scene units — matches ~35mm on a 100mm cube at SCALE 0.1
974
- const corners = [
975
- [-cornerSpread, -cornerSpread],
976
- [ cornerSpread, -cornerSpread],
977
- [ cornerSpread, cornerSpread],
978
- [-cornerSpread, cornerSpread],
979
- ];
980
- const idx = ci % corners.length;
981
- p._offsetX = corners[idx][0];
982
- p._offsetZ = corners[idx][1];
1015
+ const cornerSpread = 3.5;
1016
+ const corners = [[-cornerSpread, -cornerSpread], [cornerSpread, -cornerSpread], [cornerSpread, cornerSpread], [-cornerSpread, cornerSpread]];
1017
+ p._offsetX = corners[ci % 4][0];
1018
+ p._offsetZ = corners[ci % 4][1];
983
1019
  } else if (count > 1) {
984
1020
  const angle = (ci / count) * Math.PI * 2;
985
- const spread = (p.radius || 5) * 3 * 0.1;
986
- p._offsetX = Math.cos(angle) * spread;
987
- p._offsetZ = Math.sin(angle) * spread;
1021
+ p._offsetX = Math.cos(angle) * (p.radius || 5) * 3 * 0.1;
1022
+ p._offsetZ = Math.sin(angle) * (p.radius || 5) * 3 * 0.1;
988
1023
  }
989
1024
  window._executeParsedPrompt({ type, params: p });
990
1025
  }
@@ -0,0 +1,661 @@
1
+ /**
2
+ * brep-engine.js — Real B-rep kernel for cycleCAD using OpenCascade.js
3
+ *
4
+ * Provides true solid modeling operations:
5
+ * - Primitive creation (box, cylinder, sphere, cone)
6
+ * - Boolean operations (cut, fuse, intersect)
7
+ * - Fillet and chamfer on real edges
8
+ * - Shape → Three.js BufferGeometry conversion
9
+ *
10
+ * Uses opencascade.js v2.0.0-beta (modular WASM build)
11
+ *
12
+ * MIT License — cycleCAD (c) 2026
13
+ */
14
+
15
+ // ============================================================================
16
+ // STATE
17
+ // ============================================================================
18
+
19
+ let oc = null; // OpenCascade.js instance
20
+ let _loading = false; // Prevent double-init
21
+ let _ready = false; // True when WASM is loaded
22
+ let _currentShape = null; // Active TopoDS_Shape (the "document")
23
+ const _shapeStack = []; // Undo stack of shapes
24
+
25
+ // CDN for opencascade.js 2.0.0-beta
26
+ const OCC_CDN = 'https://unpkg.com/opencascade.js@2.0.0-beta.533428a/dist/';
27
+
28
+ // ============================================================================
29
+ // INITIALIZATION
30
+ // ============================================================================
31
+
32
+ /**
33
+ * Load and initialize the OpenCascade.js WASM kernel.
34
+ * Returns a promise that resolves when ready.
35
+ * Subsequent calls return immediately if already loaded.
36
+ */
37
+ export async function initBRep() {
38
+ if (_ready && oc) return oc;
39
+ if (_loading) {
40
+ // Wait for in-progress load
41
+ return new Promise((resolve) => {
42
+ const check = setInterval(() => {
43
+ if (_ready && oc) { clearInterval(check); resolve(oc); }
44
+ }, 200);
45
+ });
46
+ }
47
+
48
+ _loading = true;
49
+ console.log('[BRep] Loading OpenCascade.js WASM kernel...');
50
+
51
+ try {
52
+ // Dynamic import from CDN
53
+ const module = await import(/* webpackIgnore: true */ OCC_CDN + 'opencascade.full.js');
54
+ const factory = module.default || module;
55
+
56
+ // Initialize WASM — factory returns a promise
57
+ oc = await factory({
58
+ locateFile: (file) => OCC_CDN + file,
59
+ });
60
+
61
+ _ready = true;
62
+ _loading = false;
63
+ console.log('[BRep] OpenCascade.js ready ✓');
64
+ return oc;
65
+ } catch (err) {
66
+ console.error('[BRep] Failed to load OpenCascade.js:', err);
67
+ _loading = false;
68
+
69
+ // Fallback: try loading via script tag
70
+ return new Promise((resolve, reject) => {
71
+ console.log('[BRep] Trying script tag fallback...');
72
+ const savedModule = window.Module;
73
+ const script = document.createElement('script');
74
+ script.src = OCC_CDN + 'opencascade.full.js';
75
+ script.onload = async () => {
76
+ try {
77
+ const occFactory = window.Module;
78
+ window.Module = savedModule; // restore
79
+ oc = await new occFactory({
80
+ locateFile: (file) => OCC_CDN + file,
81
+ });
82
+ _ready = true;
83
+ _loading = false;
84
+ console.log('[BRep] OpenCascade.js ready (script fallback) ✓');
85
+ resolve(oc);
86
+ } catch (e2) {
87
+ _loading = false;
88
+ reject(e2);
89
+ }
90
+ };
91
+ script.onerror = () => { _loading = false; reject(new Error('Script load failed')); };
92
+ document.head.appendChild(script);
93
+ });
94
+ }
95
+ }
96
+
97
+ /** Check if the B-rep kernel is ready */
98
+ export function isReady() { return _ready && oc !== null; }
99
+
100
+ // ============================================================================
101
+ // HELPER: gp_Pnt constructor
102
+ // ============================================================================
103
+
104
+ function pnt(x, y, z) {
105
+ return new oc.gp_Pnt_3(x, y, z);
106
+ }
107
+
108
+ function ax2(origin, dir) {
109
+ return new oc.gp_Ax2_3(
110
+ pnt(origin[0], origin[1], origin[2]),
111
+ new oc.gp_Dir_4(dir[0], dir[1], dir[2])
112
+ );
113
+ }
114
+
115
+ function progress() {
116
+ return new oc.Message_ProgressRange_1();
117
+ }
118
+
119
+ // ============================================================================
120
+ // PRIMITIVES
121
+ // ============================================================================
122
+
123
+ /**
124
+ * Create a B-rep box centered at origin
125
+ * @param {number} width - X dimension in mm
126
+ * @param {number} height - Y dimension in mm
127
+ * @param {number} depth - Z dimension in mm
128
+ * @returns {TopoDS_Shape}
129
+ */
130
+ export function makeBox(width, height, depth) {
131
+ const w = width, h = height, d = depth;
132
+ // Create box at (-w/2, -h/2, -d/2) so it's centered
133
+ const builder = new oc.BRepPrimAPI_MakeBox_3(
134
+ pnt(-w / 2, -h / 2, -d / 2), w, h, d
135
+ );
136
+ const shape = builder.Shape();
137
+ builder.delete();
138
+ return shape;
139
+ }
140
+
141
+ /**
142
+ * Create a B-rep cylinder centered at origin, axis along Y
143
+ * @param {number} radius - Radius in mm
144
+ * @param {number} height - Height in mm
145
+ * @returns {TopoDS_Shape}
146
+ */
147
+ export function makeCylinder(radius, height) {
148
+ const axis = ax2([0, -height / 2, 0], [0, 1, 0]);
149
+ const builder = new oc.BRepPrimAPI_MakeCylinder_2(axis, radius, height);
150
+ const shape = builder.Shape();
151
+ builder.delete();
152
+ return shape;
153
+ }
154
+
155
+ /**
156
+ * Create a B-rep sphere centered at origin
157
+ * @param {number} radius - Radius in mm
158
+ * @returns {TopoDS_Shape}
159
+ */
160
+ export function makeSphere(radius) {
161
+ const builder = new oc.BRepPrimAPI_MakeSphere_1(radius);
162
+ const shape = builder.Shape();
163
+ builder.delete();
164
+ return shape;
165
+ }
166
+
167
+ /**
168
+ * Create a B-rep cone centered at origin, axis along Y
169
+ * @param {number} bottomRadius
170
+ * @param {number} topRadius
171
+ * @param {number} height
172
+ * @returns {TopoDS_Shape}
173
+ */
174
+ export function makeCone(bottomRadius, topRadius, height) {
175
+ const axis = ax2([0, -height / 2, 0], [0, 1, 0]);
176
+ const builder = new oc.BRepPrimAPI_MakeCone_2(axis, bottomRadius, topRadius, height);
177
+ const shape = builder.Shape();
178
+ builder.delete();
179
+ return shape;
180
+ }
181
+
182
+ // ============================================================================
183
+ // BOOLEAN OPERATIONS
184
+ // ============================================================================
185
+
186
+ /**
187
+ * Boolean cut: result = base - tool
188
+ * @param {TopoDS_Shape} base - Shape to cut from
189
+ * @param {TopoDS_Shape} tool - Shape to subtract
190
+ * @returns {TopoDS_Shape}
191
+ */
192
+ export function booleanCut(base, tool) {
193
+ const cutter = new oc.BRepAlgoAPI_Cut_3(base, tool, progress());
194
+ cutter.Build(progress());
195
+ if (!cutter.IsDone()) {
196
+ console.warn('[BRep] Boolean cut failed');
197
+ cutter.delete();
198
+ return base;
199
+ }
200
+ const result = cutter.Shape();
201
+ cutter.delete();
202
+ return result;
203
+ }
204
+
205
+ /**
206
+ * Boolean fuse (union): result = base + tool
207
+ * @param {TopoDS_Shape} base
208
+ * @param {TopoDS_Shape} tool
209
+ * @returns {TopoDS_Shape}
210
+ */
211
+ export function booleanFuse(base, tool) {
212
+ const fuser = new oc.BRepAlgoAPI_Fuse_3(base, tool, progress());
213
+ fuser.Build(progress());
214
+ if (!fuser.IsDone()) {
215
+ console.warn('[BRep] Boolean fuse failed');
216
+ fuser.delete();
217
+ return base;
218
+ }
219
+ const result = fuser.Shape();
220
+ fuser.delete();
221
+ return result;
222
+ }
223
+
224
+ /**
225
+ * Boolean intersect: result = base ∩ tool
226
+ * @param {TopoDS_Shape} base
227
+ * @param {TopoDS_Shape} tool
228
+ * @returns {TopoDS_Shape}
229
+ */
230
+ export function booleanIntersect(base, tool) {
231
+ const common = new oc.BRepAlgoAPI_Common_3(base, tool, progress());
232
+ common.Build(progress());
233
+ if (!common.IsDone()) {
234
+ console.warn('[BRep] Boolean intersect failed');
235
+ common.delete();
236
+ return base;
237
+ }
238
+ const result = common.Shape();
239
+ common.delete();
240
+ return result;
241
+ }
242
+
243
+ // ============================================================================
244
+ // FILLET & CHAMFER
245
+ // ============================================================================
246
+
247
+ /**
248
+ * Apply fillet (round) to ALL edges of a shape
249
+ * @param {TopoDS_Shape} shape - Input shape
250
+ * @param {number} radius - Fillet radius in mm
251
+ * @returns {TopoDS_Shape}
252
+ */
253
+ export function filletAll(shape, radius) {
254
+ const fillet = new oc.BRepFilletAPI_MakeFillet(shape, oc.ChFi3d_Rational);
255
+ const explorer = new oc.TopExp_Explorer_2(shape, oc.TopAbs_ShapeEnum.TopAbs_EDGE, oc.TopAbs_ShapeEnum.TopAbs_SHAPE);
256
+
257
+ let edgeCount = 0;
258
+ while (explorer.More()) {
259
+ const edge = oc.TopoDS.Edge_1(explorer.Current());
260
+ fillet.Add_2(radius, edge);
261
+ edgeCount++;
262
+ explorer.Next();
263
+ }
264
+ explorer.delete();
265
+
266
+ if (edgeCount === 0) {
267
+ console.warn('[BRep] No edges found for fillet');
268
+ fillet.delete();
269
+ return shape;
270
+ }
271
+
272
+ try {
273
+ const result = fillet.Shape();
274
+ console.log(`[BRep] Fillet applied to ${edgeCount} edges (r=${radius}mm)`);
275
+ fillet.delete();
276
+ return result;
277
+ } catch (e) {
278
+ console.warn('[BRep] Fillet failed (radius may be too large):', e.message);
279
+ fillet.delete();
280
+ return shape;
281
+ }
282
+ }
283
+
284
+ /**
285
+ * Apply chamfer to ALL edges of a shape
286
+ * @param {TopoDS_Shape} shape - Input shape
287
+ * @param {number} distance - Chamfer distance in mm
288
+ * @returns {TopoDS_Shape}
289
+ */
290
+ export function chamferAll(shape, distance) {
291
+ const chamfer = new oc.BRepFilletAPI_MakeChamfer(shape);
292
+ const explorer = new oc.TopExp_Explorer_2(shape, oc.TopAbs_ShapeEnum.TopAbs_EDGE, oc.TopAbs_ShapeEnum.TopAbs_SHAPE);
293
+
294
+ let edgeCount = 0;
295
+ while (explorer.More()) {
296
+ const edge = oc.TopoDS.Edge_1(explorer.Current());
297
+ chamfer.Add_2(distance, edge);
298
+ edgeCount++;
299
+ explorer.Next();
300
+ }
301
+ explorer.delete();
302
+
303
+ if (edgeCount === 0) {
304
+ chamfer.delete();
305
+ return shape;
306
+ }
307
+
308
+ try {
309
+ const result = chamfer.Shape();
310
+ console.log(`[BRep] Chamfer applied to ${edgeCount} edges (d=${distance}mm)`);
311
+ chamfer.delete();
312
+ return result;
313
+ } catch (e) {
314
+ console.warn('[BRep] Chamfer failed:', e.message);
315
+ chamfer.delete();
316
+ return shape;
317
+ }
318
+ }
319
+
320
+ // ============================================================================
321
+ // TRANSFORM
322
+ // ============================================================================
323
+
324
+ /**
325
+ * Translate a shape by (dx, dy, dz)
326
+ * @param {TopoDS_Shape} shape
327
+ * @param {number} dx
328
+ * @param {number} dy
329
+ * @param {number} dz
330
+ * @returns {TopoDS_Shape}
331
+ */
332
+ export function translate(shape, dx, dy, dz) {
333
+ const trsf = new oc.gp_Trsf_1();
334
+ trsf.SetTranslation_1(new oc.gp_Vec_4(dx, dy, dz));
335
+ const transform = new oc.BRepBuilderAPI_Transform_2(shape, trsf, true);
336
+ const result = transform.Shape();
337
+ transform.delete();
338
+ trsf.delete();
339
+ return result;
340
+ }
341
+
342
+ // ============================================================================
343
+ // SHAPE → THREE.JS CONVERSION
344
+ // ============================================================================
345
+
346
+ /**
347
+ * Convert a TopoDS_Shape to Three.js BufferGeometry
348
+ * Tessellates the B-rep surface and extracts vertex/normal/index arrays
349
+ *
350
+ * @param {TopoDS_Shape} shape - OCC shape to convert
351
+ * @param {number} deflection - Mesh quality (smaller = finer). Default 0.5mm
352
+ * @returns {THREE.BufferGeometry}
353
+ */
354
+ export function shapeToGeometry(shape, deflection = 0.5) {
355
+ // Tessellate the shape
356
+ new oc.BRepMesh_IncrementalMesh_2(shape, deflection, false, deflection * 5, false);
357
+
358
+ const positions = [];
359
+ const normals = [];
360
+ const indices = [];
361
+ let vertexOffset = 0;
362
+
363
+ // Iterate over all faces
364
+ const faceExplorer = new oc.TopExp_Explorer_2(
365
+ shape,
366
+ oc.TopAbs_ShapeEnum.TopAbs_FACE,
367
+ oc.TopAbs_ShapeEnum.TopAbs_SHAPE
368
+ );
369
+
370
+ while (faceExplorer.More()) {
371
+ const face = oc.TopoDS.Face_1(faceExplorer.Current());
372
+ const location = new oc.TopLoc_Location_1();
373
+ const triangulation = oc.BRep_Tool.Triangulation(face, location);
374
+
375
+ if (!triangulation.IsNull()) {
376
+ const tri = triangulation.get();
377
+ const nbNodes = tri.NbNodes();
378
+ const nbTriangles = tri.NbTriangles();
379
+
380
+ // Get the transformation from the face location
381
+ const trsf = location.Transformation();
382
+
383
+ // Extract vertices
384
+ for (let i = 1; i <= nbNodes; i++) {
385
+ const node = tri.Node(i);
386
+ const transformed = node.Transformed(trsf);
387
+ positions.push(transformed.X(), transformed.Y(), transformed.Z());
388
+ }
389
+
390
+ // Compute face normal orientation
391
+ const orientation = face.Orientation_1();
392
+ const reversed = (orientation === oc.TopAbs_Orientation.TopAbs_REVERSED);
393
+
394
+ // Extract triangles
395
+ for (let i = 1; i <= nbTriangles; i++) {
396
+ const triangle = tri.Triangle(i);
397
+ let n1 = triangle.Value(1);
398
+ let n2 = triangle.Value(2);
399
+ let n3 = triangle.Value(3);
400
+
401
+ // Flip winding if face is reversed
402
+ if (reversed) { [n2, n3] = [n3, n2]; }
403
+
404
+ indices.push(
405
+ vertexOffset + n1 - 1,
406
+ vertexOffset + n2 - 1,
407
+ vertexOffset + n3 - 1
408
+ );
409
+ }
410
+
411
+ vertexOffset += nbNodes;
412
+ }
413
+
414
+ location.delete();
415
+ faceExplorer.Next();
416
+ }
417
+ faceExplorer.delete();
418
+
419
+ // Build Three.js BufferGeometry
420
+ const geometry = new THREE.BufferGeometry();
421
+ geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
422
+ geometry.setIndex(indices);
423
+ geometry.computeVertexNormals();
424
+
425
+ return geometry;
426
+ }
427
+
428
+ /**
429
+ * Convert a TopoDS_Shape to a full Three.js Mesh with material
430
+ *
431
+ * @param {TopoDS_Shape} shape - OCC shape
432
+ * @param {object} options - { color, metalness, roughness, opacity, wireframe }
433
+ * @param {number} deflection - Mesh quality
434
+ * @returns {{ mesh: THREE.Mesh, wireframe: THREE.LineSegments, shape: TopoDS_Shape }}
435
+ */
436
+ export function shapeToMesh(shape, options = {}, deflection = 0.5) {
437
+ const {
438
+ color = 0x4488cc,
439
+ metalness = 0.3,
440
+ roughness = 0.6,
441
+ opacity = 1.0,
442
+ wireframe = false,
443
+ } = options;
444
+
445
+ const geometry = shapeToGeometry(shape, deflection);
446
+
447
+ const material = new THREE.MeshStandardMaterial({
448
+ color,
449
+ metalness,
450
+ roughness,
451
+ transparent: opacity < 1,
452
+ opacity,
453
+ side: THREE.DoubleSide,
454
+ wireframe,
455
+ });
456
+
457
+ const mesh = new THREE.Mesh(geometry, material);
458
+ mesh.castShadow = true;
459
+ mesh.receiveShadow = true;
460
+
461
+ // Wireframe edges overlay
462
+ const edges = new THREE.EdgesGeometry(geometry, 15);
463
+ const lineMat = new THREE.LineBasicMaterial({ color: 0x000000, opacity: 0.3, transparent: true });
464
+ const wireframeLines = new THREE.LineSegments(edges, lineMat);
465
+
466
+ return { mesh, wireframe: wireframeLines, shape };
467
+ }
468
+
469
+ // ============================================================================
470
+ // HIGH-LEVEL COPILOT API
471
+ // ============================================================================
472
+
473
+ /**
474
+ * Execute a full copilot command sequence using real B-rep operations.
475
+ * Manages the current shape state and converts to Three.js.
476
+ *
477
+ * @param {Array} commands - Array of { type, params } objects
478
+ * @returns {Promise<{ mesh, wireframe, shape, description }>}
479
+ */
480
+ export async function executeCommands(commands) {
481
+ if (!_ready) {
482
+ await initBRep();
483
+ }
484
+
485
+ const SCALE = 0.1; // mm to scene units
486
+ let description = '';
487
+
488
+ for (const cmd of commands) {
489
+ const { type, params = {} } = cmd;
490
+
491
+ try {
492
+ switch (type) {
493
+ case 'box': {
494
+ const w = params.width || 100;
495
+ const h = params.height || 100;
496
+ const d = params.depth || 100;
497
+ _pushUndo();
498
+ _currentShape = makeBox(w, h, d);
499
+ description += `Box ${w}×${h}×${d}mm`;
500
+ break;
501
+ }
502
+
503
+ case 'cylinder': {
504
+ const r = params.radius || 25;
505
+ const h = params.height || 50;
506
+ _pushUndo();
507
+ _currentShape = makeCylinder(r, h);
508
+ description += `Cylinder r${r} h${h}mm`;
509
+ break;
510
+ }
511
+
512
+ case 'sphere': {
513
+ const r = params.radius || 25;
514
+ _pushUndo();
515
+ _currentShape = makeSphere(r);
516
+ description += `Sphere r${r}mm`;
517
+ break;
518
+ }
519
+
520
+ case 'cone': {
521
+ const br = params.bottomRadius || params.radius || 25;
522
+ const tr = params.topRadius || 0;
523
+ const h = params.height || 50;
524
+ _pushUndo();
525
+ _currentShape = makeCone(br, tr, h);
526
+ description += `Cone r${br}/${tr} h${h}mm`;
527
+ break;
528
+ }
529
+
530
+ case 'hole': {
531
+ if (!_currentShape) {
532
+ console.warn('[BRep] No shape to cut hole in — create a shape first');
533
+ break;
534
+ }
535
+ const r = params.radius || 5;
536
+ const depth = params.depth || params.height || 200;
537
+ const count = params.count || 1;
538
+
539
+ _pushUndo();
540
+
541
+ for (let i = 0; i < count; i++) {
542
+ // Position holes at corners for count=4, or center for count=1
543
+ let dx = 0, dz = 0;
544
+ if (count === 4) {
545
+ const spread = (params.spread || 30);
546
+ const corners = [[-spread, -spread], [spread, -spread], [spread, spread], [-spread, spread]];
547
+ [dx, dz] = corners[i % 4];
548
+ } else if (count > 1) {
549
+ const angle = (i / count) * Math.PI * 2;
550
+ const spread = (params.spread || 20);
551
+ dx = Math.cos(angle) * spread;
552
+ dz = Math.sin(angle) * spread;
553
+ }
554
+
555
+ // Create cylinder tool at position, axis along Y
556
+ let tool = makeCylinder(r, depth);
557
+ if (dx !== 0 || dz !== 0) {
558
+ tool = translate(tool, dx, 0, dz);
559
+ }
560
+
561
+ _currentShape = booleanCut(_currentShape, tool);
562
+ }
563
+
564
+ description += ` + ${count} hole${count > 1 ? 's' : ''} (r${r}mm)`;
565
+ break;
566
+ }
567
+
568
+ case 'fillet': {
569
+ if (!_currentShape) break;
570
+ const r = params.radius || 5;
571
+ _pushUndo();
572
+ _currentShape = filletAll(_currentShape, r);
573
+ description += ` + fillet r${r}mm`;
574
+ break;
575
+ }
576
+
577
+ case 'chamfer': {
578
+ if (!_currentShape) break;
579
+ const d = params.distance || 3;
580
+ _pushUndo();
581
+ _currentShape = chamferAll(_currentShape, d);
582
+ description += ` + chamfer ${d}mm`;
583
+ break;
584
+ }
585
+
586
+ case 'fuse': {
587
+ if (!_currentShape || !params._toolShape) break;
588
+ _pushUndo();
589
+ _currentShape = booleanFuse(_currentShape, params._toolShape);
590
+ description += ' + fuse';
591
+ break;
592
+ }
593
+
594
+ default:
595
+ console.warn(`[BRep] Unknown command type: ${type}`);
596
+ }
597
+ } catch (err) {
598
+ console.error(`[BRep] Command "${type}" failed:`, err);
599
+ }
600
+ }
601
+
602
+ // Convert final shape to Three.js
603
+ if (!_currentShape) {
604
+ return null;
605
+ }
606
+
607
+ const result = shapeToMesh(_currentShape, { color: 0x4488cc }, 0.5);
608
+ // Scale mesh to scene units
609
+ result.mesh.scale.set(SCALE, SCALE, SCALE);
610
+ result.wireframe.scale.set(SCALE, SCALE, SCALE);
611
+ result.description = description;
612
+
613
+ return result;
614
+ }
615
+
616
+ // ============================================================================
617
+ // UNDO SUPPORT
618
+ // ============================================================================
619
+
620
+ function _pushUndo() {
621
+ if (_currentShape) {
622
+ _shapeStack.push(_currentShape);
623
+ if (_shapeStack.length > 20) _shapeStack.shift(); // limit memory
624
+ }
625
+ }
626
+
627
+ export function undo() {
628
+ if (_shapeStack.length > 0) {
629
+ _currentShape = _shapeStack.pop();
630
+ return true;
631
+ }
632
+ return false;
633
+ }
634
+
635
+ export function getCurrentShape() { return _currentShape; }
636
+ export function clearShape() { _currentShape = null; _shapeStack.length = 0; }
637
+
638
+ // ============================================================================
639
+ // REGISTER ON WINDOW FOR COPILOT ACCESS
640
+ // ============================================================================
641
+
642
+ window.brepEngine = {
643
+ initBRep,
644
+ isReady,
645
+ makeBox,
646
+ makeCylinder,
647
+ makeSphere,
648
+ makeCone,
649
+ booleanCut,
650
+ booleanFuse,
651
+ booleanIntersect,
652
+ filletAll,
653
+ chamferAll,
654
+ translate,
655
+ shapeToGeometry,
656
+ shapeToMesh,
657
+ executeCommands,
658
+ undo,
659
+ getCurrentShape,
660
+ clearShape,
661
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cyclecad",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
4
4
  "description": "Browser-based parametric 3D CAD modeler with AI-powered tools, native Inventor file parsing, and smart assembly management. No install required.",
5
5
  "main": "index.html",
6
6
  "bin": {