cyclecad 0.1.4 → 0.1.5

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/CLAUDE.md CHANGED
@@ -7,7 +7,7 @@ SACHIN (vvlars@googlemail.com, GitHub: vvlars-cmd). Building cycleCAD — open-s
7
7
  | Repo | Local Path | GitHub | npm | What |
8
8
  |------|-----------|--------|-----|------|
9
9
  | **ExplodeView** | `~/explodeview` | `vvlars-cmd/explodeview` | `explodeview` v1.0.5 | 3D CAD **viewer** for STEP files. 19,000+ line monolith app.js |
10
- | **cycleCAD** | `~/cyclecad` | `vvlars-cmd/cyclecad` | `cyclecad` v0.1.3 | Parametric 3D CAD **modeler**. 15 modular JS files. This is the active project. |
10
+ | **cycleCAD** | `~/cyclecad` | `vvlars-cmd/cyclecad` | `cyclecad` v0.1.3 | Parametric 3D CAD **modeler**. 19 modular JS files, 18,800+ lines. This is the active project. |
11
11
 
12
12
  **IMPORTANT**: These are SEPARATE repos. When Sachin says "cyclecad" he means `~/cyclecad`, NOT `~/explodeview/docs/cyclecad/`. I made this mistake once and was corrected.
13
13
 
@@ -58,11 +58,15 @@ SACHIN (vvlars@googlemail.com, GitHub: vvlars-cmd). Building cycleCAD — open-s
58
58
  | File | Lines | What |
59
59
  |------|-------|------|
60
60
  | `index.html` | 14K | Landing page for cyclecad.com |
61
- | `app/index.html` | 1,852 | Main CAD app — HTML + inline script wiring all modules |
61
+ | `app/index.html` | 3,156 | Main CAD app — HTML + inline script wiring all 17 modules |
62
62
  | `app/js/app.js` | 794 | App state, mode management, history, save/load |
63
- | `app/js/viewport.js` | 667 | Three.js r170 scene, camera, lights, grid, OrbitControls, views |
63
+ | `app/js/viewport.js` | 751 | Three.js r170 scene, camera, lights, shadows, grid, selection highlight, OrbitControls, views |
64
64
  | `app/js/sketch.js` | 899 | 2D canvas overlay, line/rect/circle/arc, grid snapping, constraints |
65
65
  | `app/js/operations.js` | 1,078 | Extrude, revolve, fillet, chamfer, boolean, shell, pattern |
66
+ | `app/js/constraint-solver.js` | 1,047 | 2D constraint solver: 12 types, iterative relaxation, DOF analysis |
67
+ | `app/js/advanced-ops.js` | 763 | Sweep, loft, sheet metal (bend/flange/tab/slot/unfold), spring, thread |
68
+ | `app/js/assembly.js` | 1,103 | Assembly workspace: components, mate constraints, joints, explode/collapse |
69
+ | `app/js/dxf-export.js` | 1,174 | DXF export: 2D sketch, 3D projection, multi-view engineering drawing |
66
70
  | `app/js/params.js` | 523 | Parameter editor, material selector (Steel/Al/ABS/Brass/Ti/Nylon) |
67
71
  | `app/js/tree.js` | 479 | Feature tree panel with rename, suppress, delete, context menus |
68
72
  | `app/js/inventor-parser.js` | 1,138 | OLE2/CFB binary parser for .ipt/.iam, 26 feature types, assembly constraints |
@@ -137,6 +141,11 @@ Located at `~/cyclecad/example/DUO Durchgehend Inventor/` (gitignored — too la
137
141
  - 3D viewport (Three.js r170, OrbitControls, preset views, grid, wireframe, fit-to-all)
138
142
  - 2D sketch engine (line, rect, circle, arc, polyline, grid snap, constraint detection)
139
143
  - Parametric operations (extrude, revolve, fillet, chamfer, boolean, shell, pattern)
144
+ - **Constraint solver** (12 types: coincident, horizontal, vertical, parallel, perpendicular, tangent, equal, fixed, concentric, symmetric, distance, angle)
145
+ - **Sweep** (profile along path with twist, scale interpolation, helix/line/arc paths)
146
+ - **Loft** (between profiles with automatic resampling, circle/rect/hexagon)
147
+ - **Sheet metal** (bend with k-factor, flange, tab, slot, unfold flat pattern)
148
+ - **Spring & thread generators** (helical sweep, screw thread geometry)
140
149
  - Feature tree (rename, suppress, delete, context menus)
141
150
  - Parameter editor (real-time updates, 6 materials with density data)
142
151
  - Export (STL ASCII+binary, OBJ, glTF 2.0, cycleCAD JSON)
@@ -144,12 +153,14 @@ Located at `~/cyclecad/example/DUO Durchgehend Inventor/` (gitignored — too la
144
153
  - Inventor parsing (OLE2/CFB, 26 feature types, constraints, metadata)
145
154
  - Assembly resolver (reference extraction, path resolution, BOM)
146
155
  - Project loader (.ipj parsing, folder indexing, file categorization)
147
- - Project browser (tree UI, search, categories, stats)
156
+ - Project browser (tree UI + inline left panel, search, categories, stats)
157
+ - DUO manifest loader (474 files, instant load without File System Access API)
148
158
  - Rebuild guides (cycleCAD + Fusion 360, per-step time estimates, HTML export)
149
159
  - Reverse engineering (STL import, geometry analysis, feature inference)
150
160
  - Keyboard shortcuts (25+)
151
161
  - Undo/redo (history snapshots, Ctrl+Z/Y)
152
162
  - Welcome splash with quick actions
163
+ - Left panel tabs (Model Tree / Project Browser)
153
164
  - Dark theme UI (VS Code-style CSS variables)
154
165
 
155
166
  ### STUBS/APPROXIMATIONS
@@ -158,15 +169,14 @@ Located at `~/cyclecad/example/DUO Durchgehend Inventor/` (gitignored — too la
158
169
  - STEP export shows error (needs OpenCascade.js)
159
170
  - Revolve doesn't fully rebuild on param change
160
171
  - History restore is basic (feature list only, no geometry serialization)
172
+ - Bend/flange apply to selected mesh with default bend line (not edge-selected)
161
173
 
162
174
  ### NOT YET BUILT
163
- - Constraint solver for sketches
164
- - Sweep, loft operations
165
- - Sheet metal tools
166
- - Assembly workspace (joint placement, motion)
167
175
  - Real-time collaboration
168
- - DXF/DWG export
169
176
  - Plugin API
177
+ - STEP import via OpenCascade.js
178
+ - DWG export (DXF is done)
179
+ - Assembly workspace joint editing UI (module is built, needs UI panel)
170
180
 
171
181
  ## Competitive Landscape
172
182
  | Competitor | What | Our Edge |
package/app/index.html CHANGED
@@ -1364,6 +1364,42 @@
1364
1364
  </button>
1365
1365
  </div>
1366
1366
 
1367
+ <!-- Advanced Operations -->
1368
+ <div class="toolbar-group">
1369
+ <button class="toolbar-button" id="tool-sweep" title="Sweep (Profile Along Path)">
1370
+ <span class="toolbar-icon">&#8634;</span>
1371
+ <span class="toolbar-label">Sweep</span>
1372
+ </button>
1373
+ <button class="toolbar-button" id="tool-loft" title="Loft (Between Profiles)">
1374
+ <span class="toolbar-icon">&#9651;</span>
1375
+ <span class="toolbar-label">Loft</span>
1376
+ </button>
1377
+ <button class="toolbar-button" id="tool-spring" title="Generate Spring" style="background:rgba(63,185,80,0.08);">
1378
+ <span class="toolbar-icon">&#8635;</span>
1379
+ <span class="toolbar-label">Spring</span>
1380
+ </button>
1381
+ <button class="toolbar-button" id="tool-thread" title="Generate Thread" style="background:rgba(63,185,80,0.08);">
1382
+ <span class="toolbar-icon">&#10038;</span>
1383
+ <span class="toolbar-label">Thread</span>
1384
+ </button>
1385
+ </div>
1386
+
1387
+ <!-- Sheet Metal -->
1388
+ <div class="toolbar-group">
1389
+ <button class="toolbar-button" id="tool-bend" title="Sheet Metal Bend" style="background:rgba(210,153,34,0.08);">
1390
+ <span class="toolbar-icon">&#9484;</span>
1391
+ <span class="toolbar-label">Bend</span>
1392
+ </button>
1393
+ <button class="toolbar-button" id="tool-flange" title="Sheet Metal Flange" style="background:rgba(210,153,34,0.08);">
1394
+ <span class="toolbar-icon">&#9492;</span>
1395
+ <span class="toolbar-label">Flange</span>
1396
+ </button>
1397
+ <button class="toolbar-button" id="tool-unfold" title="Unfold Sheet Metal" style="background:rgba(210,153,34,0.08);">
1398
+ <span class="toolbar-icon">&#9645;</span>
1399
+ <span class="toolbar-label">Unfold</span>
1400
+ </button>
1401
+ </div>
1402
+
1367
1403
  <!-- Export Tools -->
1368
1404
  <div class="toolbar-group">
1369
1405
  <button class="toolbar-button" id="export-stl" title="Export STL">
@@ -1374,6 +1410,26 @@
1374
1410
  <span class="toolbar-icon">💾</span>
1375
1411
  <span class="toolbar-label">STEP</span>
1376
1412
  </button>
1413
+ <button class="toolbar-button" id="export-dxf" title="Export DXF (2D Drawing)">
1414
+ <span class="toolbar-icon">📐</span>
1415
+ <span class="toolbar-label">DXF</span>
1416
+ </button>
1417
+ <button class="toolbar-button" id="export-multiview" title="Multi-View Engineering Drawing (DXF)">
1418
+ <span class="toolbar-icon">📄</span>
1419
+ <span class="toolbar-label">Drawing</span>
1420
+ </button>
1421
+ </div>
1422
+
1423
+ <!-- Assembly Tools -->
1424
+ <div class="toolbar-group">
1425
+ <button class="toolbar-button" id="tool-assembly" title="Assembly Workspace" style="background:rgba(139,92,246,0.1);border:1px solid rgba(139,92,246,0.3);">
1426
+ <span class="toolbar-icon">&#9881;</span>
1427
+ <span class="toolbar-label">Assembly</span>
1428
+ </button>
1429
+ <button class="toolbar-button" id="tool-explode" title="Explode/Collapse Assembly">
1430
+ <span class="toolbar-icon">&#11043;</span>
1431
+ <span class="toolbar-label">Explode</span>
1432
+ </button>
1377
1433
  </div>
1378
1434
 
1379
1435
  <!-- Reverse Engineer -->
@@ -1549,6 +1605,10 @@
1549
1605
  import { loadProject, showFolderPicker, parseIPJ } from './js/project-loader.js';
1550
1606
  import { initProjectBrowser, showBrowser, hideBrowser, setProject, onFileSelect } from './js/project-browser.js';
1551
1607
  import { generateGuide, renderGuide, exportGuideHTML } from './js/rebuild-guide.js';
1608
+ import { solveConstraints, addConstraint, removeConstraint, autoDetectConstraints, isFullyConstrained, getAllConstraints, clearAllConstraints } from './js/constraint-solver.js';
1609
+ import { createSweep, createLoft, createBend, createFlange, createTab, createSlot, unfoldSheetMetal, createSpring, createThread } from './js/advanced-ops.js';
1610
+ import Assembly from './js/assembly.js';
1611
+ import { exportSketchToDXF, exportProjectionToDXF, exportMultiViewDXF, export3DDXF, downloadDXF } from './js/dxf-export.js';
1552
1612
 
1553
1613
  // ========== Application State ==========
1554
1614
  const APP = {
@@ -1559,6 +1619,7 @@
1559
1619
  history: [],
1560
1620
  historyIndex: -1,
1561
1621
  project: null, // Current Inventor project
1622
+ assembly: null, // Assembly workspace instance
1562
1623
  };
1563
1624
 
1564
1625
  // ========== Initialization ==========
@@ -1571,6 +1632,9 @@
1571
1632
  document.getElementById('kernel-status').classList.add('ready');
1572
1633
  document.getElementById('kernel-status-text').textContent = 'Ready';
1573
1634
 
1635
+ // 1b. Initialize assembly workspace
1636
+ APP.assembly = new Assembly(getScene());
1637
+
1574
1638
  // 2. Initialize feature tree
1575
1639
  const treeContainer = document.getElementById('feature-tree');
1576
1640
  if (treeContainer) initTree(treeContainer);
@@ -1956,7 +2020,27 @@
1956
2020
 
1957
2021
  // Export
1958
2022
  bind('export-stl', () => doExportSTL());
1959
- bind('export-step', () => updateStatus('STEP export: coming soon'));
2023
+ bind('export-step', () => updateStatus('STEP export requires OpenCascade.js — coming soon'));
2024
+ bind('export-dxf', () => {
2025
+ if (APP.features.length === 0) { updateStatus('No geometry to export'); return; }
2026
+ try {
2027
+ const mesh = APP.features[APP.features.length - 1].mesh;
2028
+ if (!mesh) { updateStatus('Select a feature with geometry'); return; }
2029
+ const dxf = exportProjectionToDXF(mesh, 'front', { hiddenLines: true });
2030
+ downloadDXF(dxf, 'cyclecad-export.dxf');
2031
+ updateStatus('DXF exported: front view projection');
2032
+ } catch (err) { updateStatus('DXF export failed: ' + err.message); console.error(err); }
2033
+ });
2034
+ bind('export-multiview', () => {
2035
+ if (APP.features.length === 0) { updateStatus('No geometry to export'); return; }
2036
+ try {
2037
+ const mesh = APP.features[APP.features.length - 1].mesh;
2038
+ if (!mesh) { updateStatus('Select a feature with geometry'); return; }
2039
+ const dxf = exportMultiViewDXF(mesh, { titleBlock: true, title: 'cycleCAD Part', author: 'cycleCAD', company: 'cycleWASH' });
2040
+ downloadDXF(dxf, 'cyclecad-drawing.dxf');
2041
+ updateStatus('Multi-view engineering drawing exported');
2042
+ } catch (err) { updateStatus('Drawing export failed: ' + err.message); console.error(err); }
2043
+ });
1960
2044
 
1961
2045
  // Edit
1962
2046
  bind('btn-undo', () => { undo(); });
@@ -2017,6 +2101,80 @@
2017
2101
  updateStatus(`Inventor file loaded: ${parsedData.metadata?.fileName || 'unknown'} — ${parsedData.features?.length || 0} features found`);
2018
2102
  });
2019
2103
  });
2104
+
2105
+ // Advanced Operations
2106
+ bind('tool-sweep', () => openDialog('sweep'));
2107
+ bind('tool-loft', () => openDialog('loft'));
2108
+ bind('tool-spring', () => {
2109
+ const splash = document.getElementById('welcome-splash');
2110
+ if (splash) splash.classList.add('hidden');
2111
+ const radius = parseFloat(prompt('Spring outer radius (mm):', '10') || '0');
2112
+ const wireR = parseFloat(prompt('Wire radius (mm):', '1.5') || '0');
2113
+ const height = parseFloat(prompt('Spring height (mm):', '40') || '0');
2114
+ const turns = parseFloat(prompt('Number of turns:', '8') || '0');
2115
+ if (!radius || !wireR || !height || !turns) { updateStatus('Cancelled'); return; }
2116
+ try {
2117
+ const mesh = createSpring(radius, wireR, height, turns);
2118
+ addToScene(mesh);
2119
+ const feature = { id: 'feature_' + Date.now(), name: `Spring (R${radius} H${height})`, type: 'spring', mesh, params: { radius, wireR, height, turns } };
2120
+ APP.features.push(feature);
2121
+ addFeature(feature);
2122
+ pushHistory();
2123
+ updateStatus(`Created spring: R${radius}mm, ${turns} turns, H${height}mm`);
2124
+ } catch (err) { updateStatus('Spring failed: ' + err.message); }
2125
+ });
2126
+ bind('tool-thread', () => {
2127
+ const splash = document.getElementById('welcome-splash');
2128
+ if (splash) splash.classList.add('hidden');
2129
+ const outerR = parseFloat(prompt('Thread outer radius (mm):', '5') || '0');
2130
+ const innerR = parseFloat(prompt('Thread inner radius (mm):', '4') || '0');
2131
+ const pitch = parseFloat(prompt('Thread pitch (mm):', '1') || '0');
2132
+ const length = parseFloat(prompt('Thread length (mm):', '20') || '0');
2133
+ if (!outerR || !innerR || !pitch || !length) { updateStatus('Cancelled'); return; }
2134
+ try {
2135
+ const mesh = createThread(outerR, innerR, pitch, length);
2136
+ addToScene(mesh);
2137
+ const feature = { id: 'feature_' + Date.now(), name: `Thread M${outerR*2}x${pitch}`, type: 'thread', mesh, params: { outerR, innerR, pitch, length } };
2138
+ APP.features.push(feature);
2139
+ addFeature(feature);
2140
+ pushHistory();
2141
+ updateStatus(`Created thread: M${outerR*2}x${pitch}, L${length}mm`);
2142
+ } catch (err) { updateStatus('Thread failed: ' + err.message); }
2143
+ });
2144
+
2145
+ // Sheet Metal
2146
+ bind('tool-bend', () => openDialog('bend'));
2147
+ bind('tool-flange', () => openDialog('flange'));
2148
+ bind('tool-unfold', () => {
2149
+ if (APP.features.length === 0) { updateStatus('No features to unfold'); return; }
2150
+ updateStatus('Sheet metal unfold: select a bent part, then define bend lines');
2151
+ });
2152
+
2153
+ // Assembly
2154
+ let assemblyMode = false;
2155
+ bind('tool-assembly', () => {
2156
+ assemblyMode = !assemblyMode;
2157
+ if (assemblyMode) {
2158
+ updateStatus('Assembly mode ON — click parts to add to assembly. Right-click for mate constraints.');
2159
+ document.getElementById('tool-assembly').style.background = 'rgba(139,92,246,0.3)';
2160
+ } else {
2161
+ updateStatus('Assembly mode OFF');
2162
+ document.getElementById('tool-assembly').style.background = 'rgba(139,92,246,0.1)';
2163
+ }
2164
+ });
2165
+
2166
+ let exploded = false;
2167
+ bind('tool-explode', () => {
2168
+ if (!APP.assembly) return;
2169
+ exploded = !exploded;
2170
+ if (exploded) {
2171
+ APP.assembly.explodeAssembly(2.0);
2172
+ updateStatus('Assembly exploded — click again to collapse');
2173
+ } else {
2174
+ APP.assembly.collapseAssembly();
2175
+ updateStatus('Assembly collapsed');
2176
+ }
2177
+ });
2020
2178
  }
2021
2179
 
2022
2180
  // ========== Tab Switching ==========
@@ -2094,9 +2252,26 @@
2094
2252
  if (spinner) spinner.classList.add('active');
2095
2253
  updateStatus('Loading DUO project manifest...');
2096
2254
 
2097
- // Fetch pre-built manifest (no File System Access API needed)
2255
+ // Fetch pre-built manifest (local dev only not in public repo)
2098
2256
  const resp = await fetch('duo-manifest.json');
2099
- if (!resp.ok) throw new Error('Failed to load manifest: ' + resp.status);
2257
+ if (!resp.ok) {
2258
+ // Manifest not available (e.g. on public site) — fall back to folder picker
2259
+ if (spinner) spinner.classList.remove('active');
2260
+ updateStatus('DUO manifest not found — use File System Access to open a project folder');
2261
+ try {
2262
+ const handle = await showFolderPicker();
2263
+ if (handle) {
2264
+ const project = await loadProject(handle);
2265
+ APP.project = project;
2266
+ setProject(project);
2267
+ populateInlineBrowser(project);
2268
+ showBrowser();
2269
+ switchLeftTab('browser');
2270
+ updateStatus(`Project loaded: ${project.stats?.parts || 0} parts`);
2271
+ }
2272
+ } catch (e) { updateStatus('Folder selection cancelled'); }
2273
+ return;
2274
+ }
2100
2275
  const manifest = await resp.json();
2101
2276
 
2102
2277
  // Transform file types: manifest uses type:"file" + ext:".ipt"
@@ -2556,6 +2731,137 @@
2556
2731
  }
2557
2732
  }
2558
2733
 
2734
+ // ========== Advanced Operation Apply Functions ==========
2735
+ function generateProfilePoints(shape, size, segments = 32) {
2736
+ const pts = [];
2737
+ if (shape === 'circle') {
2738
+ for (let i = 0; i < segments; i++) {
2739
+ const a = (i / segments) * Math.PI * 2;
2740
+ pts.push({ x: Math.cos(a) * size, y: Math.sin(a) * size });
2741
+ }
2742
+ } else if (shape === 'rectangle') {
2743
+ const h = size * 0.6;
2744
+ pts.push({ x: -size, y: -h }, { x: size, y: -h }, { x: size, y: h }, { x: -size, y: h });
2745
+ } else if (shape === 'hexagon') {
2746
+ for (let i = 0; i < 6; i++) {
2747
+ const a = (i / 6) * Math.PI * 2;
2748
+ pts.push({ x: Math.cos(a) * size, y: Math.sin(a) * size });
2749
+ }
2750
+ }
2751
+ return pts;
2752
+ }
2753
+
2754
+ function applySweep() {
2755
+ const profileShape = document.getElementById('sweep-profile').value;
2756
+ const profileSize = parseFloat(document.getElementById('sweep-size').value);
2757
+ const pathType = document.getElementById('sweep-path').value;
2758
+ const length = parseFloat(document.getElementById('sweep-length').value);
2759
+ const segments = parseInt(document.getElementById('sweep-segments').value);
2760
+ const twist = parseFloat(document.getElementById('sweep-twist').value);
2761
+
2762
+ const profile = generateProfilePoints(profileShape, profileSize);
2763
+ let path = [];
2764
+
2765
+ if (pathType === 'helix') {
2766
+ const turns = 3;
2767
+ const radius = length / 4;
2768
+ for (let i = 0; i <= segments; i++) {
2769
+ const t = i / segments;
2770
+ const a = t * Math.PI * 2 * turns;
2771
+ path.push({ x: Math.cos(a) * radius, y: Math.sin(a) * radius, z: t * length });
2772
+ }
2773
+ } else if (pathType === 'line') {
2774
+ path = [{ x: 0, y: 0, z: 0 }, { x: 0, y: 0, z: length }];
2775
+ } else if (pathType === 'arc') {
2776
+ const arcRadius = length / 2;
2777
+ for (let i = 0; i <= segments; i++) {
2778
+ const a = (i / segments) * Math.PI;
2779
+ path.push({ x: Math.cos(a) * arcRadius - arcRadius, y: 0, z: Math.sin(a) * arcRadius });
2780
+ }
2781
+ }
2782
+
2783
+ try {
2784
+ const mesh = createSweep(profile, path, { segments, twist });
2785
+ addToScene(mesh);
2786
+ const feature = { id: 'feature_' + Date.now(), name: `Sweep (${profileShape})`, type: 'sweep', mesh, params: { profileShape, profileSize, pathType, length, segments, twist } };
2787
+ APP.features.push(feature);
2788
+ addFeature(feature);
2789
+ pushHistory();
2790
+ updateStatus(`Created sweep: ${profileShape} profile along ${pathType} path`);
2791
+ closeDialog('sweep');
2792
+ } catch (err) { updateStatus('Sweep failed: ' + err.message); console.error(err); }
2793
+ }
2794
+
2795
+ function applyLoft() {
2796
+ const startShape = document.getElementById('loft-start').value;
2797
+ const startSize = parseFloat(document.getElementById('loft-start-size').value);
2798
+ const endShape = document.getElementById('loft-end').value;
2799
+ const endSize = parseFloat(document.getElementById('loft-end-size').value);
2800
+ const height = parseFloat(document.getElementById('loft-height').value);
2801
+ const segments = parseInt(document.getElementById('loft-segments').value);
2802
+
2803
+ const profiles = [
2804
+ { points: generateProfilePoints(startShape, startSize), position: { x: 0, y: 0, z: 0 } },
2805
+ { points: generateProfilePoints(endShape, endSize), position: { x: 0, y: 0, z: height } }
2806
+ ];
2807
+
2808
+ try {
2809
+ const mesh = createLoft(profiles, { segments });
2810
+ addToScene(mesh);
2811
+ const feature = { id: 'feature_' + Date.now(), name: `Loft (${startShape}→${endShape})`, type: 'loft', mesh, params: { startShape, startSize, endShape, endSize, height, segments } };
2812
+ APP.features.push(feature);
2813
+ addFeature(feature);
2814
+ pushHistory();
2815
+ updateStatus(`Created loft: ${startShape} to ${endShape}, H${height}mm`);
2816
+ closeDialog('loft');
2817
+ } catch (err) { updateStatus('Loft failed: ' + err.message); console.error(err); }
2818
+ }
2819
+
2820
+ function applyBend() {
2821
+ const angle = parseFloat(document.getElementById('bend-angle').value);
2822
+ const radius = parseFloat(document.getElementById('bend-radius').value);
2823
+ const kFactor = parseFloat(document.getElementById('bend-kfactor').value);
2824
+
2825
+ if (!APP.selectedFeature || !APP.selectedFeature.mesh) {
2826
+ updateStatus('Select a flat plate first, then apply bend');
2827
+ return;
2828
+ }
2829
+
2830
+ try {
2831
+ const bendLine = { start: { x: 0, y: 0, z: 0 }, end: { x: 0, y: 20, z: 0 } };
2832
+ const mesh = createBend(APP.selectedFeature.mesh, bendLine, angle, radius, { kFactor });
2833
+ addToScene(mesh);
2834
+ const feature = { id: 'feature_' + Date.now(), name: `Bend (${angle}°, R${radius})`, type: 'bend', mesh, params: { angle, radius, kFactor } };
2835
+ APP.features.push(feature);
2836
+ addFeature(feature);
2837
+ pushHistory();
2838
+ updateStatus(`Applied bend: ${angle}° with R${radius}mm inner radius`);
2839
+ closeDialog('bend');
2840
+ } catch (err) { updateStatus('Bend failed: ' + err.message); console.error(err); }
2841
+ }
2842
+
2843
+ function applyFlange() {
2844
+ const length = parseFloat(document.getElementById('flange-length').value);
2845
+ const angle = parseFloat(document.getElementById('flange-angle').value);
2846
+
2847
+ if (!APP.selectedFeature || !APP.selectedFeature.mesh) {
2848
+ updateStatus('Select a sheet metal part first, then apply flange');
2849
+ return;
2850
+ }
2851
+
2852
+ try {
2853
+ const edge = { start: { x: -10, y: 0, z: 0 }, end: { x: 10, y: 0, z: 0 } };
2854
+ const mesh = createFlange(APP.selectedFeature.mesh, edge, length, angle);
2855
+ addToScene(mesh);
2856
+ const feature = { id: 'feature_' + Date.now(), name: `Flange (L${length}, ${angle}°)`, type: 'flange', mesh, params: { length, angle } };
2857
+ APP.features.push(feature);
2858
+ addFeature(feature);
2859
+ pushHistory();
2860
+ updateStatus(`Applied flange: L${length}mm at ${angle}°`);
2861
+ closeDialog('flange');
2862
+ } catch (err) { updateStatus('Flange failed: ' + err.message); console.error(err); }
2863
+ }
2864
+
2559
2865
  // Close dialog when backdrop is clicked
2560
2866
  document.getElementById('dialog-backdrop').addEventListener('click', () => {
2561
2867
  if (currentDialogId) closeDialog(currentDialogId);
@@ -2775,5 +3081,147 @@
2775
3081
  </div>
2776
3082
  </div>
2777
3083
 
3084
+ <!-- Sweep Dialog -->
3085
+ <div class="operation-dialog" id="dialog-sweep">
3086
+ <div class="dialog-header">
3087
+ <div class="dialog-title">Sweep (Profile Along Path)</div>
3088
+ <div class="dialog-close-btn" onclick="closeDialog('sweep')">&#10005;</div>
3089
+ </div>
3090
+ <div class="dialog-content">
3091
+ <div class="dialog-form-group">
3092
+ <label class="dialog-label">Profile Shape</label>
3093
+ <select class="dialog-select" id="sweep-profile">
3094
+ <option value="circle">Circle</option>
3095
+ <option value="rectangle">Rectangle</option>
3096
+ <option value="sketch">From Sketch</option>
3097
+ </select>
3098
+ </div>
3099
+ <div class="dialog-form-group">
3100
+ <label class="dialog-label">Profile Radius / Width (mm)</label>
3101
+ <input type="number" class="dialog-input" id="sweep-size" value="3" min="0.1" step="0.1">
3102
+ </div>
3103
+ <div class="dialog-form-group">
3104
+ <label class="dialog-label">Path Shape</label>
3105
+ <select class="dialog-select" id="sweep-path">
3106
+ <option value="helix">Helix</option>
3107
+ <option value="line">Straight Line</option>
3108
+ <option value="arc">Arc</option>
3109
+ </select>
3110
+ </div>
3111
+ <div class="dialog-form-group">
3112
+ <label class="dialog-label">Path Length (mm)</label>
3113
+ <input type="number" class="dialog-input" id="sweep-length" value="40" min="1" step="1">
3114
+ </div>
3115
+ <div class="dialog-form-group">
3116
+ <label class="dialog-label">Segments</label>
3117
+ <input type="range" class="dialog-range" id="sweep-segments" min="8" max="128" value="64">
3118
+ </div>
3119
+ <div class="dialog-form-group">
3120
+ <label class="dialog-label">Twist (deg/mm)</label>
3121
+ <input type="number" class="dialog-input" id="sweep-twist" value="0" step="0.1">
3122
+ </div>
3123
+ </div>
3124
+ <div class="dialog-footer">
3125
+ <button class="dialog-button secondary" onclick="closeDialog('sweep')">Cancel</button>
3126
+ <button class="dialog-button primary" onclick="applySweep()">OK</button>
3127
+ </div>
3128
+ </div>
3129
+
3130
+ <!-- Loft Dialog -->
3131
+ <div class="operation-dialog" id="dialog-loft">
3132
+ <div class="dialog-header">
3133
+ <div class="dialog-title">Loft (Between Profiles)</div>
3134
+ <div class="dialog-close-btn" onclick="closeDialog('loft')">&#10005;</div>
3135
+ </div>
3136
+ <div class="dialog-content">
3137
+ <div class="dialog-form-group">
3138
+ <label class="dialog-label">Start Profile</label>
3139
+ <select class="dialog-select" id="loft-start">
3140
+ <option value="circle">Circle</option>
3141
+ <option value="rectangle">Rectangle</option>
3142
+ <option value="hexagon">Hexagon</option>
3143
+ </select>
3144
+ </div>
3145
+ <div class="dialog-form-group">
3146
+ <label class="dialog-label">Start Size (mm)</label>
3147
+ <input type="number" class="dialog-input" id="loft-start-size" value="10" min="0.1" step="0.1">
3148
+ </div>
3149
+ <div class="dialog-form-group">
3150
+ <label class="dialog-label">End Profile</label>
3151
+ <select class="dialog-select" id="loft-end">
3152
+ <option value="circle">Circle</option>
3153
+ <option value="rectangle">Rectangle</option>
3154
+ <option value="hexagon">Hexagon</option>
3155
+ </select>
3156
+ </div>
3157
+ <div class="dialog-form-group">
3158
+ <label class="dialog-label">End Size (mm)</label>
3159
+ <input type="number" class="dialog-input" id="loft-end-size" value="5" min="0.1" step="0.1">
3160
+ </div>
3161
+ <div class="dialog-form-group">
3162
+ <label class="dialog-label">Height (mm)</label>
3163
+ <input type="number" class="dialog-input" id="loft-height" value="30" min="1" step="1">
3164
+ </div>
3165
+ <div class="dialog-form-group">
3166
+ <label class="dialog-label">Segments</label>
3167
+ <input type="range" class="dialog-range" id="loft-segments" min="4" max="64" value="32">
3168
+ </div>
3169
+ </div>
3170
+ <div class="dialog-footer">
3171
+ <button class="dialog-button secondary" onclick="closeDialog('loft')">Cancel</button>
3172
+ <button class="dialog-button primary" onclick="applyLoft()">OK</button>
3173
+ </div>
3174
+ </div>
3175
+
3176
+ <!-- Bend Dialog -->
3177
+ <div class="operation-dialog" id="dialog-bend">
3178
+ <div class="dialog-header">
3179
+ <div class="dialog-title">Sheet Metal Bend</div>
3180
+ <div class="dialog-close-btn" onclick="closeDialog('bend')">&#10005;</div>
3181
+ </div>
3182
+ <div class="dialog-content">
3183
+ <div class="dialog-form-group">
3184
+ <label class="dialog-label">Bend Angle (degrees)</label>
3185
+ <input type="number" class="dialog-input" id="bend-angle" value="90" min="1" max="180" step="1">
3186
+ </div>
3187
+ <div class="dialog-form-group">
3188
+ <label class="dialog-label">Inner Bend Radius (mm)</label>
3189
+ <input type="number" class="dialog-input" id="bend-radius" value="2" min="0.1" step="0.1">
3190
+ </div>
3191
+ <div class="dialog-form-group">
3192
+ <label class="dialog-label">K-Factor</label>
3193
+ <input type="number" class="dialog-input" id="bend-kfactor" value="0.44" min="0.1" max="0.9" step="0.01">
3194
+ </div>
3195
+ <p style="font-size:10px;color:var(--text-muted);margin-top:4px;">Select a flat plate first, then define the bend line by clicking two points.</p>
3196
+ </div>
3197
+ <div class="dialog-footer">
3198
+ <button class="dialog-button secondary" onclick="closeDialog('bend')">Cancel</button>
3199
+ <button class="dialog-button primary" onclick="applyBend()">OK</button>
3200
+ </div>
3201
+ </div>
3202
+
3203
+ <!-- Flange Dialog -->
3204
+ <div class="operation-dialog" id="dialog-flange">
3205
+ <div class="dialog-header">
3206
+ <div class="dialog-title">Sheet Metal Flange</div>
3207
+ <div class="dialog-close-btn" onclick="closeDialog('flange')">&#10005;</div>
3208
+ </div>
3209
+ <div class="dialog-content">
3210
+ <div class="dialog-form-group">
3211
+ <label class="dialog-label">Flange Length (mm)</label>
3212
+ <input type="number" class="dialog-input" id="flange-length" value="15" min="0.1" step="0.1">
3213
+ </div>
3214
+ <div class="dialog-form-group">
3215
+ <label class="dialog-label">Flange Angle (degrees)</label>
3216
+ <input type="number" class="dialog-input" id="flange-angle" value="90" min="1" max="180" step="1">
3217
+ </div>
3218
+ <p style="font-size:10px;color:var(--text-muted);margin-top:4px;">Select an edge on a sheet metal part, then apply the flange.</p>
3219
+ </div>
3220
+ <div class="dialog-footer">
3221
+ <button class="dialog-button secondary" onclick="closeDialog('flange')">Cancel</button>
3222
+ <button class="dialog-button primary" onclick="applyFlange()">OK</button>
3223
+ </div>
3224
+ </div>
3225
+
2778
3226
  </body>
2779
3227
  </html>