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 +19 -9
- package/app/index.html +451 -3
- package/app/js/advanced-ops.js +762 -0
- package/app/js/assembly.js +1102 -0
- package/app/js/constraint-solver.js +1046 -0
- package/app/js/dxf-export.js +1173 -0
- package/app/js/viewport.js +83 -0
- package/package.json +1 -1
- package/DUO-MANIFEST-README.md +0 -233
- package/app/duo-manifest-demo.html +0 -337
- package/app/duo-manifest.json +0 -7375
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**.
|
|
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` |
|
|
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` |
|
|
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">↺</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">△</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">↻</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">✶</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">┌</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">└</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">▭</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">⚙</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">⬣</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
|
|
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 (
|
|
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)
|
|
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')">✕</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')">✕</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')">✕</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')">✕</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>
|