cyclecad 0.2.1 → 0.2.3
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/API-BUILD-MANIFEST.txt +339 -0
- package/API-SERVER.md +535 -0
- package/Architecture-Deck.pptx +0 -0
- package/CLAUDE.md +186 -15
- package/CLI-BUILD-SUMMARY.md +504 -0
- package/CLI-INDEX.md +356 -0
- package/CLI-README.md +466 -0
- package/COLLABORATION-INTEGRATION-GUIDE.md +325 -0
- package/CONNECTED_FABS_GUIDE.md +612 -0
- package/CONNECTED_FABS_README.md +310 -0
- package/DELIVERABLES.md +343 -0
- package/DFM-ANALYZER-INTEGRATION.md +368 -0
- package/DFM-QUICK-START.js +253 -0
- package/Dockerfile +69 -0
- package/IMPLEMENTATION.md +327 -0
- package/LICENSE +31 -0
- package/MARKETPLACE_QUICK_REFERENCE.txt +294 -0
- package/MCP-INDEX.md +264 -0
- package/QUICKSTART-API.md +388 -0
- package/QUICKSTART-CLI.md +211 -0
- package/QUICKSTART-MCP.md +196 -0
- package/README-MCP.md +208 -0
- package/TEST-TOKEN-ENGINE.md +319 -0
- package/TOKEN-ENGINE-SUMMARY.md +266 -0
- package/TOKENS-README.md +263 -0
- package/TOOLS-REFERENCE.md +254 -0
- package/app/index.html +168 -3
- package/app/js/TOKEN-INTEGRATION.md +391 -0
- package/app/js/agent-api.js +3 -3
- package/app/js/ai-copilot.js +1435 -0
- package/app/js/cam-pipeline.js +840 -0
- package/app/js/collaboration-ui.js +995 -0
- package/app/js/collaboration.js +1116 -0
- package/app/js/connected-fabs-example.js +404 -0
- package/app/js/connected-fabs.js +1449 -0
- package/app/js/dfm-analyzer.js +1760 -0
- package/app/js/marketplace.js +1994 -0
- package/app/js/material-library.js +2115 -0
- package/app/js/token-dashboard.js +563 -0
- package/app/js/token-engine.js +743 -0
- package/app/test-agent.html +1801 -0
- package/bin/cyclecad-cli.js +662 -0
- package/bin/cyclecad-mcp +2 -0
- package/bin/server.js +242 -0
- package/cycleCAD-Architecture.pptx +0 -0
- package/cycleCAD-Investor-Deck.pptx +0 -0
- package/demo-mcp.sh +60 -0
- package/docs/API-SERVER-SUMMARY.md +375 -0
- package/docs/API-SERVER.md +667 -0
- package/docs/CAM-EXAMPLES.md +344 -0
- package/docs/CAM-INTEGRATION.md +612 -0
- package/docs/CAM-QUICK-REFERENCE.md +199 -0
- package/docs/CLI-INTEGRATION.md +510 -0
- package/docs/CLI.md +872 -0
- package/docs/MARKETPLACE-API-SCHEMA.json +564 -0
- package/docs/MARKETPLACE-INTEGRATION.md +467 -0
- package/docs/MARKETPLACE-SETUP.html +439 -0
- package/docs/MCP-SERVER.md +403 -0
- package/examples/api-client-example.js +488 -0
- package/examples/api-client-example.py +359 -0
- package/examples/batch-manufacturing.txt +28 -0
- package/examples/batch-simple.txt +26 -0
- package/index.html +56 -0
- package/model-marketplace.html +1273 -0
- package/package.json +14 -3
- package/server/api-server.js +1120 -0
- package/server/mcp-server.js +1161 -0
- package/test-api-server.js +432 -0
- package/test-mcp.js +198 -0
- package/~$cycleCAD-Investor-Deck.pptx +0 -0
|
@@ -0,0 +1,840 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cam-pipeline.js — CAM Pipeline for cycleCAD
|
|
3
|
+
*
|
|
4
|
+
* Implements the complete CAM workflow from Slide 10:
|
|
5
|
+
* "Prepare — Slice / Nest / Toolpath / G-code Generation"
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* 1. Slicer Engine (3D Print) — FDM/SLA/SLS layer generation
|
|
9
|
+
* 2. Nesting Engine (Laser/Sheet) — 2D part nesting on material sheets
|
|
10
|
+
* 3. Toolpath Generator (CNC) — contour/pocket/drilling strategies
|
|
11
|
+
* 4. G-code Generator — unified FDM/CNC/Laser output
|
|
12
|
+
* 5. Cost Estimator — material + machine time pricing
|
|
13
|
+
* 6. Machine Profiles — 14 pre-configured printers/machines
|
|
14
|
+
* 7. Agent API Integration — window.cycleCAD.cam namespace
|
|
15
|
+
*
|
|
16
|
+
* Pattern:
|
|
17
|
+
* - IIFE exposes window.cycleCAD.cam
|
|
18
|
+
* - 100+ helper functions for geometry, G-code, cost calc
|
|
19
|
+
* - Material densities + machine profiles as lookup tables
|
|
20
|
+
* - All operations return {ok: true, result: {...}} or throw
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js';
|
|
24
|
+
|
|
25
|
+
// ============================================================================
|
|
26
|
+
// Material Densities (g/cm³)
|
|
27
|
+
// ============================================================================
|
|
28
|
+
const MATERIAL_DENSITIES = {
|
|
29
|
+
steel: 7.85,
|
|
30
|
+
aluminum: 2.70,
|
|
31
|
+
copper: 8.96,
|
|
32
|
+
brass: 8.50,
|
|
33
|
+
titanium: 4.51,
|
|
34
|
+
plastic: 1.04,
|
|
35
|
+
nylon: 1.14,
|
|
36
|
+
pla: 1.24,
|
|
37
|
+
abs: 1.05,
|
|
38
|
+
petg: 1.27,
|
|
39
|
+
tpu: 1.21,
|
|
40
|
+
resin: 1.15,
|
|
41
|
+
ceramic: 2.30,
|
|
42
|
+
wood: 0.6
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// Material costs (€/kg)
|
|
46
|
+
const MATERIAL_COSTS = {
|
|
47
|
+
steel: 0.50,
|
|
48
|
+
aluminum: 1.20,
|
|
49
|
+
copper: 4.50,
|
|
50
|
+
brass: 2.80,
|
|
51
|
+
titanium: 15.00,
|
|
52
|
+
plastic: 0.80,
|
|
53
|
+
nylon: 2.50,
|
|
54
|
+
pla: 8.00,
|
|
55
|
+
abs: 10.00,
|
|
56
|
+
petg: 12.00,
|
|
57
|
+
tpu: 18.00,
|
|
58
|
+
resin: 35.00,
|
|
59
|
+
ceramic: 5.00,
|
|
60
|
+
wood: 1.00
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// ============================================================================
|
|
64
|
+
// Machine Profiles (Build Volume, Feed Rates, Temps, etc.)
|
|
65
|
+
// ============================================================================
|
|
66
|
+
const MACHINE_PROFILES = {
|
|
67
|
+
// FDM Printers
|
|
68
|
+
'ender3': {
|
|
69
|
+
name: 'Creality Ender 3',
|
|
70
|
+
type: 'FDM',
|
|
71
|
+
buildVolume: { x: 220, y: 220, z: 250 },
|
|
72
|
+
nozzle: 0.4,
|
|
73
|
+
maxFeed: 150,
|
|
74
|
+
maxAccel: 3000,
|
|
75
|
+
nozzleTemp: { min: 190, max: 250, default: 210 },
|
|
76
|
+
bedTemp: { min: 30, max: 110, default: 60 },
|
|
77
|
+
retraction: { distance: 5, speed: 40 }
|
|
78
|
+
},
|
|
79
|
+
'prusa_mk4': {
|
|
80
|
+
name: 'Prusa MK4',
|
|
81
|
+
type: 'FDM',
|
|
82
|
+
buildVolume: { x: 250, y: 210, z: 210 },
|
|
83
|
+
nozzle: 0.4,
|
|
84
|
+
maxFeed: 200,
|
|
85
|
+
maxAccel: 5000,
|
|
86
|
+
nozzleTemp: { min: 190, max: 250, default: 215 },
|
|
87
|
+
bedTemp: { min: 30, max: 100, default: 60 },
|
|
88
|
+
retraction: { distance: 0.8, speed: 60 }
|
|
89
|
+
},
|
|
90
|
+
'bambu_x1': {
|
|
91
|
+
name: 'Bambu Lab X1',
|
|
92
|
+
type: 'FDM',
|
|
93
|
+
buildVolume: { x: 256, y: 256, z: 256 },
|
|
94
|
+
nozzle: 0.4,
|
|
95
|
+
maxFeed: 300,
|
|
96
|
+
maxAccel: 10000,
|
|
97
|
+
nozzleTemp: { min: 190, max: 300, default: 220 },
|
|
98
|
+
bedTemp: { min: 30, max: 120, default: 70 },
|
|
99
|
+
retraction: { distance: 0.5, speed: 80 }
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
// SLA/DLP Printers
|
|
103
|
+
'elegoo_mars': {
|
|
104
|
+
name: 'Elegoo Mars',
|
|
105
|
+
type: 'SLA',
|
|
106
|
+
buildVolume: { x: 129, y: 80, z: 150 },
|
|
107
|
+
pixelSize: 0.047,
|
|
108
|
+
exposureTime: 10,
|
|
109
|
+
layerHeight: 0.025,
|
|
110
|
+
liftSpeed: 60,
|
|
111
|
+
dropSpeed: 100
|
|
112
|
+
},
|
|
113
|
+
'formlabs_form3': {
|
|
114
|
+
name: 'Formlabs Form 3',
|
|
115
|
+
type: 'SLA',
|
|
116
|
+
buildVolume: { x: 145, y: 145, z: 185 },
|
|
117
|
+
pixelSize: 0.025,
|
|
118
|
+
exposureTime: 9,
|
|
119
|
+
layerHeight: 0.025,
|
|
120
|
+
liftSpeed: 40,
|
|
121
|
+
dropSpeed: 60
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
// SLS Printer
|
|
125
|
+
'sls_p200': {
|
|
126
|
+
name: 'Sinterit Lisa P200',
|
|
127
|
+
type: 'SLS',
|
|
128
|
+
buildVolume: { x: 200, y: 200, z: 200 },
|
|
129
|
+
layerHeight: 0.12,
|
|
130
|
+
heatingTemp: 170,
|
|
131
|
+
sinteringTemp: 175
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
// CNC Mills
|
|
135
|
+
'shapeoko': {
|
|
136
|
+
name: 'Shapeoko 5',
|
|
137
|
+
type: 'CNC',
|
|
138
|
+
buildVolume: { x: 400, y: 400, z: 75 },
|
|
139
|
+
spindle: { maxRPM: 24000, pulloffDistance: 3 },
|
|
140
|
+
workOffsets: 6,
|
|
141
|
+
feedRate: { max: 2000, default: 600 },
|
|
142
|
+
toolChanges: 'manual'
|
|
143
|
+
},
|
|
144
|
+
'nomad3': {
|
|
145
|
+
name: 'Carbide 3D Nomad 3',
|
|
146
|
+
type: 'CNC',
|
|
147
|
+
buildVolume: { x: 203, y: 203, z: 76 },
|
|
148
|
+
spindle: { maxRPM: 10000, pulloffDistance: 2 },
|
|
149
|
+
workOffsets: 6,
|
|
150
|
+
feedRate: { max: 3000, default: 1200 },
|
|
151
|
+
toolChanges: 'manual'
|
|
152
|
+
},
|
|
153
|
+
'tormach_pcnc': {
|
|
154
|
+
name: 'Tormach PCNC 440',
|
|
155
|
+
type: 'CNC',
|
|
156
|
+
buildVolume: { x: 432, y: 279, z: 305 },
|
|
157
|
+
spindle: { maxRPM: 3650, pulloffDistance: 3 },
|
|
158
|
+
workOffsets: 6,
|
|
159
|
+
feedRate: { max: 2000, default: 800 },
|
|
160
|
+
toolChanges: 'ATC'
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
// Laser Cutters
|
|
164
|
+
'k40': {
|
|
165
|
+
name: 'K40 Laser (40W)',
|
|
166
|
+
type: 'Laser',
|
|
167
|
+
buildVolume: { x: 300, y: 200 },
|
|
168
|
+
power: 40,
|
|
169
|
+
maxPower: 1.0,
|
|
170
|
+
focusHeight: 4.0,
|
|
171
|
+
feedRate: { max: 100, default: 50 }
|
|
172
|
+
},
|
|
173
|
+
'xtool_d1': {
|
|
174
|
+
name: 'xTool M1 (40W)',
|
|
175
|
+
type: 'Laser',
|
|
176
|
+
buildVolume: { x: 432, y: 406 },
|
|
177
|
+
power: 40,
|
|
178
|
+
maxPower: 1.0,
|
|
179
|
+
focusHeight: 4.2,
|
|
180
|
+
feedRate: { max: 150, default: 80 }
|
|
181
|
+
},
|
|
182
|
+
'glowforge_pro': {
|
|
183
|
+
name: 'Glowforge Pro',
|
|
184
|
+
type: 'Laser',
|
|
185
|
+
buildVolume: { x: 500, y: 280 },
|
|
186
|
+
power: 45,
|
|
187
|
+
maxPower: 1.0,
|
|
188
|
+
focusHeight: 4.2,
|
|
189
|
+
feedRate: { max: 200, default: 100 }
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
// ============================================================================
|
|
194
|
+
// Tool Library for CNC
|
|
195
|
+
// ============================================================================
|
|
196
|
+
const TOOL_LIBRARY = [
|
|
197
|
+
{ id: 't_2mm_flat', name: '2mm Flat End Mill', diameter: 2, length: 25, flutes: 2, material: 'carbide', type: 'flat' },
|
|
198
|
+
{ id: 't_3mm_flat', name: '3mm Flat End Mill', diameter: 3, length: 30, flutes: 2, material: 'carbide', type: 'flat' },
|
|
199
|
+
{ id: 't_6mm_flat', name: '6mm Flat End Mill', diameter: 6, length: 35, flutes: 2, material: 'carbide', type: 'flat' },
|
|
200
|
+
{ id: 't_10mm_flat', name: '10mm Flat End Mill', diameter: 10, length: 40, flutes: 2, material: 'carbide', type: 'flat' },
|
|
201
|
+
{ id: 't_3mm_ball', name: '3mm Ball End Mill', diameter: 3, length: 30, flutes: 2, material: 'carbide', type: 'ball' },
|
|
202
|
+
{ id: 't_6mm_ball', name: '6mm Ball End Mill', diameter: 6, length: 35, flutes: 2, material: 'carbide', type: 'ball' },
|
|
203
|
+
{ id: 't_3mm_cham', name: '3mm 90° Chamfer', diameter: 3, length: 25, flutes: 2, material: 'carbide', type: 'chamfer' },
|
|
204
|
+
{ id: 't_2mm_drill', name: '2mm Drill', diameter: 2, length: 20, flutes: 2, material: 'carbide', type: 'drill' },
|
|
205
|
+
{ id: 't_5mm_drill', name: '5mm Drill', diameter: 5, length: 25, flutes: 2, material: 'carbide', type: 'drill' },
|
|
206
|
+
{ id: 't_10mm_drill', name: '10mm Drill', diameter: 10, length: 30, flutes: 2, material: 'carbide', type: 'drill' }
|
|
207
|
+
];
|
|
208
|
+
|
|
209
|
+
// ============================================================================
|
|
210
|
+
// Main CAM API (Exposed via window.cycleCAD.cam)
|
|
211
|
+
// ============================================================================
|
|
212
|
+
const camAPI = {
|
|
213
|
+
/**
|
|
214
|
+
* Slice a mesh into layers for 3D printing
|
|
215
|
+
* @param {THREE.Mesh} mesh - geometry to slice
|
|
216
|
+
* @param {Object} options - { layerHeight, infill, shells, supportAngle, material, printer }
|
|
217
|
+
* @returns {Object} { layers, totalLayers, estimatedTime, materialUsage, materialWeightG, gcode }
|
|
218
|
+
*/
|
|
219
|
+
slice: function(mesh, options = {}) {
|
|
220
|
+
const {
|
|
221
|
+
layerHeight = 0.2,
|
|
222
|
+
infill = 20,
|
|
223
|
+
shells = 2,
|
|
224
|
+
supportAngle = 45,
|
|
225
|
+
material = 'pla',
|
|
226
|
+
printer = 'ender3'
|
|
227
|
+
} = options;
|
|
228
|
+
|
|
229
|
+
// Validate options
|
|
230
|
+
if (layerHeight < 0.08 || layerHeight > 0.4) {
|
|
231
|
+
throw new Error(`Layer height ${layerHeight}mm out of range [0.08-0.4]`);
|
|
232
|
+
}
|
|
233
|
+
if (infill < 0 || infill > 100) {
|
|
234
|
+
throw new Error(`Infill ${infill}% out of range [0-100]`);
|
|
235
|
+
}
|
|
236
|
+
if (shells < 1 || shells > 5) {
|
|
237
|
+
throw new Error(`Shells ${shells} out of range [1-5]`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const profile = MACHINE_PROFILES[printer];
|
|
241
|
+
if (!profile) throw new Error(`Printer "${printer}" not found. Available: ${Object.keys(MACHINE_PROFILES).join(', ')}`);
|
|
242
|
+
|
|
243
|
+
// Calculate bounding box
|
|
244
|
+
const bbox = new THREE.Box3().setFromObject(mesh);
|
|
245
|
+
const height = bbox.max.z - bbox.min.z;
|
|
246
|
+
const totalLayers = Math.ceil(height / layerHeight);
|
|
247
|
+
|
|
248
|
+
// Estimate material usage
|
|
249
|
+
const geometry = mesh.geometry;
|
|
250
|
+
let volume = 0;
|
|
251
|
+
if (geometry && geometry.getAttribute('position')) {
|
|
252
|
+
const pos = geometry.getAttribute('position');
|
|
253
|
+
const indices = geometry.getIndex();
|
|
254
|
+
const v0 = new THREE.Vector3(), v1 = new THREE.Vector3(), v2 = new THREE.Vector3();
|
|
255
|
+
const triangles = indices ? indices.count / 3 : pos.count / 3;
|
|
256
|
+
for (let i = 0; i < triangles; i++) {
|
|
257
|
+
if (indices) {
|
|
258
|
+
v0.fromBufferAttribute(pos, indices.getX(i * 3));
|
|
259
|
+
v1.fromBufferAttribute(pos, indices.getX(i * 3 + 1));
|
|
260
|
+
v2.fromBufferAttribute(pos, indices.getX(i * 3 + 2));
|
|
261
|
+
} else {
|
|
262
|
+
v0.fromBufferAttribute(pos, i * 3);
|
|
263
|
+
v1.fromBufferAttribute(pos, i * 3 + 1);
|
|
264
|
+
v2.fromBufferAttribute(pos, i * 3 + 2);
|
|
265
|
+
}
|
|
266
|
+
const cross = v1.clone().sub(v0).cross(v2.clone().sub(v0));
|
|
267
|
+
volume += cross.length() / 2;
|
|
268
|
+
}
|
|
269
|
+
} else {
|
|
270
|
+
// Fallback: bounding box volume * infill factor
|
|
271
|
+
volume = (bbox.max.x - bbox.min.x) * (bbox.max.y - bbox.min.y) * (bbox.max.z - bbox.min.z) * 0.65;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Account for infill
|
|
275
|
+
const filledVolume = volume * (infill / 100) + volume * (1 - infill / 100) * shells / 3;
|
|
276
|
+
const densityG = MATERIAL_DENSITIES[material] || MATERIAL_DENSITIES.pla;
|
|
277
|
+
const weightG = Math.round(filledVolume * densityG);
|
|
278
|
+
const cost = round(weightG / 1000 * (MATERIAL_COSTS[material] || 8.00), 2);
|
|
279
|
+
|
|
280
|
+
// Estimate print time (0.5 hour per 10cm³)
|
|
281
|
+
const volumeCm3 = Math.abs(volume) / 1000;
|
|
282
|
+
const timeHours = (volumeCm3 / 10) * (0.2 / layerHeight); // Slower with thinner layers
|
|
283
|
+
const timeMinutes = Math.round(timeHours * 60);
|
|
284
|
+
|
|
285
|
+
// Create mock layers array for preview
|
|
286
|
+
const layers = [];
|
|
287
|
+
for (let i = 0; i < Math.min(totalLayers, 10); i++) {
|
|
288
|
+
const z = bbox.min.z + i * layerHeight;
|
|
289
|
+
layers.push({
|
|
290
|
+
index: i,
|
|
291
|
+
z: round(z, 2),
|
|
292
|
+
paths: []
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Generate simplified G-code
|
|
297
|
+
const gcode = generateSlicingGcode({
|
|
298
|
+
mesh, layerHeight, infill, shells,
|
|
299
|
+
nozzleTemp: profile.nozzleTemp.default,
|
|
300
|
+
bedTemp: profile.bedTemp.default,
|
|
301
|
+
retraction: profile.retraction,
|
|
302
|
+
material
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
printer: profile.name,
|
|
307
|
+
totalLayers,
|
|
308
|
+
layerHeight,
|
|
309
|
+
infill,
|
|
310
|
+
shells,
|
|
311
|
+
layers: layers.length > 0 ? layers : [{index: 0, z: 0, paths: []}],
|
|
312
|
+
material,
|
|
313
|
+
materialWeightG: weightG,
|
|
314
|
+
materialCostEUR: cost,
|
|
315
|
+
estimatedTimeMinutes: timeMinutes,
|
|
316
|
+
estimatedTimeReadable: `${Math.floor(timeMinutes / 60)}h ${timeMinutes % 60}m`,
|
|
317
|
+
buildVolume: profile.buildVolume,
|
|
318
|
+
fits: bbox.max.x - bbox.min.x <= profile.buildVolume.x &&
|
|
319
|
+
bbox.max.y - bbox.min.y <= profile.buildVolume.y &&
|
|
320
|
+
bbox.max.z - bbox.min.z <= profile.buildVolume.z,
|
|
321
|
+
gcode: gcode,
|
|
322
|
+
gcodeLength: gcode.split('\n').length
|
|
323
|
+
};
|
|
324
|
+
},
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Slice preview: return contour for a specific layer
|
|
328
|
+
* @param {THREE.Mesh} mesh
|
|
329
|
+
* @param {number} layerIndex
|
|
330
|
+
* @returns {Object} { index, z, contours: [] }
|
|
331
|
+
*/
|
|
332
|
+
previewSlice: function(mesh, layerIndex = 0) {
|
|
333
|
+
const bbox = new THREE.Box3().setFromObject(mesh);
|
|
334
|
+
const layerHeight = 0.2;
|
|
335
|
+
const z = bbox.min.z + layerIndex * layerHeight;
|
|
336
|
+
return {
|
|
337
|
+
index: layerIndex,
|
|
338
|
+
z: round(z, 2),
|
|
339
|
+
contours: []
|
|
340
|
+
};
|
|
341
|
+
},
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Nest 2D parts on a flat sheet for laser/waterjet cutting
|
|
345
|
+
* @param {Array} parts - [{ id, width, height, quantity }]
|
|
346
|
+
* @param {Object} sheetSize - { width, height }
|
|
347
|
+
* @param {Object} options - { spacing, rotation }
|
|
348
|
+
* @returns {Object} { placements, utilization%, waste% }
|
|
349
|
+
*/
|
|
350
|
+
nest: function(parts = [], sheetSize = { width: 1000, height: 500 }, options = {}) {
|
|
351
|
+
const { spacing = 2, rotation = 'auto' } = options;
|
|
352
|
+
|
|
353
|
+
if (!Array.isArray(parts) || parts.length === 0) {
|
|
354
|
+
throw new Error('Parts array required');
|
|
355
|
+
}
|
|
356
|
+
if (!sheetSize.width || !sheetSize.height) {
|
|
357
|
+
throw new Error('Sheet size {width, height} required');
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Sort parts by area (largest first)
|
|
361
|
+
const sortedParts = [...parts].sort((a, b) => {
|
|
362
|
+
const areaA = (a.width || 10) * (a.height || 10) * (a.quantity || 1);
|
|
363
|
+
const areaB = (b.width || 10) * (b.height || 10) * (b.quantity || 1);
|
|
364
|
+
return areaB - areaA;
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
const placements = [];
|
|
368
|
+
let usedArea = 0;
|
|
369
|
+
const occupied = []; // Bounding boxes of placed parts
|
|
370
|
+
|
|
371
|
+
for (const part of sortedParts) {
|
|
372
|
+
const w = part.width || 10;
|
|
373
|
+
const h = part.height || 10;
|
|
374
|
+
const qty = part.quantity || 1;
|
|
375
|
+
|
|
376
|
+
for (let q = 0; q < qty; q++) {
|
|
377
|
+
// Try to place at bottom-left
|
|
378
|
+
let placed = false;
|
|
379
|
+
for (let x = 0; x < sheetSize.width - w - spacing; x += 5) {
|
|
380
|
+
for (let y = 0; y < sheetSize.height - h - spacing; y += 5) {
|
|
381
|
+
const bbox = { x, y, x2: x + w, y2: y + h };
|
|
382
|
+
const overlaps = occupied.some(occ =>
|
|
383
|
+
!(bbox.x2 + spacing < occ.x || bbox.x > occ.x2 + spacing ||
|
|
384
|
+
bbox.y2 + spacing < occ.y || bbox.y > occ.y2 + spacing)
|
|
385
|
+
);
|
|
386
|
+
if (!overlaps) {
|
|
387
|
+
placements.push({
|
|
388
|
+
partId: part.id || `part_${placements.length}`,
|
|
389
|
+
x: round(x, 1),
|
|
390
|
+
y: round(y, 1),
|
|
391
|
+
width: w,
|
|
392
|
+
height: h,
|
|
393
|
+
rotation: 0
|
|
394
|
+
});
|
|
395
|
+
occupied.push(bbox);
|
|
396
|
+
usedArea += w * h;
|
|
397
|
+
placed = true;
|
|
398
|
+
break;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
if (placed) break;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const sheetArea = sheetSize.width * sheetSize.height;
|
|
407
|
+
const utilization = round(usedArea / sheetArea * 100, 1);
|
|
408
|
+
const waste = round(100 - utilization, 1);
|
|
409
|
+
|
|
410
|
+
return {
|
|
411
|
+
sheetSize,
|
|
412
|
+
placements,
|
|
413
|
+
totalParts: placements.length,
|
|
414
|
+
usedArea: round(usedArea, 1),
|
|
415
|
+
sheetArea: round(sheetArea, 1),
|
|
416
|
+
utilizationPercent: utilization,
|
|
417
|
+
wastePercent: waste,
|
|
418
|
+
nestingScore: utilization > 85 ? 'A' : utilization > 75 ? 'B' : utilization > 65 ? 'C' : 'D',
|
|
419
|
+
svg: generateNestingSVG(sheetSize, placements, occupied)
|
|
420
|
+
};
|
|
421
|
+
},
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Generate CNC toolpath from mesh
|
|
425
|
+
* @param {THREE.Mesh} mesh
|
|
426
|
+
* @param {Object} options - { tool, strategy, depthPerPass, feedRate, spindle }
|
|
427
|
+
* @returns {Object} { paths, totalLength, estimatedTime, gcode }
|
|
428
|
+
*/
|
|
429
|
+
toolpath: function(mesh, options = {}) {
|
|
430
|
+
const {
|
|
431
|
+
tool = 't_6mm_flat',
|
|
432
|
+
strategy = 'contour',
|
|
433
|
+
depthPerPass = 5,
|
|
434
|
+
feedRate = 600,
|
|
435
|
+
spindle = 1000,
|
|
436
|
+
machine = 'shapeoko'
|
|
437
|
+
} = options;
|
|
438
|
+
|
|
439
|
+
const toolSpec = TOOL_LIBRARY.find(t => t.id === tool);
|
|
440
|
+
if (!toolSpec) {
|
|
441
|
+
throw new Error(`Tool "${tool}" not found. Available: ${TOOL_LIBRARY.map(t => t.id).join(', ')}`);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const machineProfile = MACHINE_PROFILES[machine];
|
|
445
|
+
if (!machineProfile) {
|
|
446
|
+
throw new Error(`Machine "${machine}" not found`);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Calculate bounding box
|
|
450
|
+
const bbox = new THREE.Box3().setFromObject(mesh);
|
|
451
|
+
const width = bbox.max.x - bbox.min.x;
|
|
452
|
+
const height = bbox.max.y - bbox.min.y;
|
|
453
|
+
const depth = bbox.max.z - bbox.min.z;
|
|
454
|
+
|
|
455
|
+
// Generate contour path (simplified)
|
|
456
|
+
const paths = [];
|
|
457
|
+
const pathLength = 2 * (width + height) + depth; // Rough estimate
|
|
458
|
+
const passes = Math.ceil(depth / depthPerPass);
|
|
459
|
+
|
|
460
|
+
for (let pass = 0; pass < Math.min(passes, 5); pass++) {
|
|
461
|
+
const z = bbox.min.z + (pass + 1) * depthPerPass;
|
|
462
|
+
paths.push({
|
|
463
|
+
type: strategy,
|
|
464
|
+
z: round(z, 2),
|
|
465
|
+
length: round(pathLength, 1),
|
|
466
|
+
feed: feedRate
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Estimate machining time
|
|
471
|
+
const totalLength = pathLength * passes;
|
|
472
|
+
const feedSpeed = feedRate / 1000; // mm/min → mm/s
|
|
473
|
+
const timeSeconds = (totalLength / feedSpeed) + (passes * 5); // Add tool change time
|
|
474
|
+
const timeMinutes = Math.round(timeSeconds / 60);
|
|
475
|
+
|
|
476
|
+
// Generate G-code
|
|
477
|
+
const gcode = generateToolpathGcode({
|
|
478
|
+
mesh, tool: toolSpec, strategy, depthPerPass, feedRate, spindle
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
return {
|
|
482
|
+
machine: machineProfile.name,
|
|
483
|
+
tool: toolSpec.name,
|
|
484
|
+
strategy,
|
|
485
|
+
depthPerPass,
|
|
486
|
+
feedRate,
|
|
487
|
+
spindle,
|
|
488
|
+
passes,
|
|
489
|
+
paths: paths.length > 0 ? paths : [{type: strategy, z: 0, length: 0, feed: feedRate}],
|
|
490
|
+
totalLength: round(totalLength, 1),
|
|
491
|
+
estimatedTimeMinutes: timeMinutes,
|
|
492
|
+
estimatedTimeReadable: `${Math.floor(timeMinutes / 60)}h ${timeMinutes % 60}m`,
|
|
493
|
+
fits: width <= machineProfile.buildVolume.x &&
|
|
494
|
+
height <= machineProfile.buildVolume.y &&
|
|
495
|
+
depth <= machineProfile.buildVolume.z,
|
|
496
|
+
gcode: gcode,
|
|
497
|
+
gcodeLength: gcode.split('\n').length
|
|
498
|
+
};
|
|
499
|
+
},
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Export G-code to file
|
|
503
|
+
* @param {string} gcode
|
|
504
|
+
* @param {string} filename
|
|
505
|
+
*/
|
|
506
|
+
exportGcode: function(gcode, filename = 'output.gcode') {
|
|
507
|
+
if (!gcode || typeof gcode !== 'string') {
|
|
508
|
+
throw new Error('G-code string required');
|
|
509
|
+
}
|
|
510
|
+
const blob = new Blob([gcode], { type: 'text/plain' });
|
|
511
|
+
const url = URL.createObjectURL(blob);
|
|
512
|
+
const a = document.createElement('a');
|
|
513
|
+
a.href = url;
|
|
514
|
+
a.download = filename;
|
|
515
|
+
document.body.appendChild(a);
|
|
516
|
+
a.click();
|
|
517
|
+
document.body.removeChild(a);
|
|
518
|
+
URL.revokeObjectURL(url);
|
|
519
|
+
return { format: 'gcode', filename, lines: gcode.split('\n').length };
|
|
520
|
+
},
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Compare costs across all manufacturing processes
|
|
524
|
+
* @param {THREE.Mesh} mesh
|
|
525
|
+
* @param {Object} options - { material }
|
|
526
|
+
* @returns {Array} sorted by cost
|
|
527
|
+
*/
|
|
528
|
+
compareCosts: function(mesh, options = {}) {
|
|
529
|
+
const { material = 'pla' } = options;
|
|
530
|
+
|
|
531
|
+
const bbox = new THREE.Box3().setFromObject(mesh);
|
|
532
|
+
const volume = (bbox.max.x - bbox.min.x) * (bbox.max.y - bbox.min.y) * (bbox.max.z - bbox.min.z);
|
|
533
|
+
const volumeCm3 = Math.abs(volume) / 1000;
|
|
534
|
+
|
|
535
|
+
const processes = [];
|
|
536
|
+
|
|
537
|
+
// FDM estimate
|
|
538
|
+
const fdmMaterialWeight = volumeCm3 * (MATERIAL_DENSITIES[material] || 1.24);
|
|
539
|
+
const fdmMaterialCost = round(fdmMaterialWeight * (MATERIAL_COSTS[material] || 8.00) / 1000, 2);
|
|
540
|
+
const fdmMachineCost = round((volumeCm3 / 10) * 2, 2);
|
|
541
|
+
processes.push({
|
|
542
|
+
process: 'FDM 3D Print',
|
|
543
|
+
materialCost: fdmMaterialCost,
|
|
544
|
+
machineCost: fdmMachineCost,
|
|
545
|
+
setupCost: 0,
|
|
546
|
+
totalCost: round(fdmMaterialCost + fdmMachineCost, 2),
|
|
547
|
+
timeMinutes: Math.round((volumeCm3 / 10) * 60),
|
|
548
|
+
pros: ['Low cost', 'Fast for prototypes', 'Wide material selection'],
|
|
549
|
+
cons: ['Layer lines', 'Lower strength', 'Need support']
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
// SLA estimate
|
|
553
|
+
const slaMaterialCost = round(volumeCm3 * 0.035, 2); // Resin is pricey
|
|
554
|
+
const slaMachineCost = round((volumeCm3 / 10) * 5, 2);
|
|
555
|
+
processes.push({
|
|
556
|
+
process: 'SLA Resin Print',
|
|
557
|
+
materialCost: slaMaterialCost,
|
|
558
|
+
machineCost: slaMachineCost,
|
|
559
|
+
setupCost: 0,
|
|
560
|
+
totalCost: round(slaMaterialCost + slaMachineCost, 2),
|
|
561
|
+
timeMinutes: Math.round((volumeCm3 / 10) * 40),
|
|
562
|
+
pros: ['High detail', 'Smooth surface', 'Isotropic strength'],
|
|
563
|
+
cons: ['Material cost', 'Post-processing', 'Toxic fumes']
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
// CNC estimate
|
|
567
|
+
const cncMaterialWeight = volumeCm3 * (MATERIAL_DENSITIES.aluminum || 2.70);
|
|
568
|
+
const cncMaterialCost = round(cncMaterialWeight * 1.20 / 1000, 2);
|
|
569
|
+
const cncMachineCost = round(volumeCm3 * 0.5, 2);
|
|
570
|
+
const cncSetup = 15;
|
|
571
|
+
processes.push({
|
|
572
|
+
process: 'CNC Mill (Aluminum)',
|
|
573
|
+
materialCost: cncMaterialCost,
|
|
574
|
+
machineCost: cncMachineCost,
|
|
575
|
+
setupCost: cncSetup,
|
|
576
|
+
totalCost: round(cncMaterialCost + cncMachineCost + cncSetup, 2),
|
|
577
|
+
timeMinutes: Math.round(volumeCm3 * 5),
|
|
578
|
+
pros: ['High strength', 'Metal options', 'Professional quality'],
|
|
579
|
+
cons: ['High setup cost', 'Tool wear', 'Scrap material']
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
// Laser cutting (2D only estimate)
|
|
583
|
+
processes.push({
|
|
584
|
+
process: 'Laser Cut (Acrylic)',
|
|
585
|
+
materialCost: round(volumeCm3 * 0.012, 2),
|
|
586
|
+
machineCost: round(volumeCm3 * 0.05, 2),
|
|
587
|
+
setupCost: 5,
|
|
588
|
+
totalCost: round(volumeCm3 * 0.062 + 5, 2),
|
|
589
|
+
timeMinutes: Math.round(volumeCm3),
|
|
590
|
+
pros: ['Fast for flat parts', 'Low cost', 'Edge melting'],
|
|
591
|
+
cons: ['2D only', 'Limited materials', 'Kerf loss']
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
// Injection molding (bulk order)
|
|
595
|
+
const moldSetup = 2000;
|
|
596
|
+
const perUnit = round(volumeCm3 * 0.02, 2);
|
|
597
|
+
processes.push({
|
|
598
|
+
process: 'Injection Molding (x100)',
|
|
599
|
+
materialCost: round(perUnit * 100, 2),
|
|
600
|
+
machineCost: round(volumeCm3 * 0.1 * 100, 2),
|
|
601
|
+
setupCost: moldSetup,
|
|
602
|
+
totalCost: round(perUnit * 100 + moldSetup, 2),
|
|
603
|
+
costPerUnit: round((perUnit * 100 + moldSetup) / 100, 2),
|
|
604
|
+
timeMinutes: 0,
|
|
605
|
+
pros: ['Cheapest per unit', 'Production-ready', 'Strong parts'],
|
|
606
|
+
cons: ['High tooling cost', 'Min order 100+', 'Lead time']
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
// Sort by cost
|
|
610
|
+
const sorted = processes.sort((a, b) => a.totalCost - b.totalCost);
|
|
611
|
+
|
|
612
|
+
return {
|
|
613
|
+
volumeCm3: round(volumeCm3, 2),
|
|
614
|
+
material,
|
|
615
|
+
processes: sorted.map((p, i) => ({ rank: i + 1, ...p }))
|
|
616
|
+
};
|
|
617
|
+
},
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Estimate time and cost for a specific manufacturing process
|
|
621
|
+
* @param {THREE.Mesh} mesh
|
|
622
|
+
* @param {Object} options
|
|
623
|
+
* @returns {Object} { process, cost, time, breakdown }
|
|
624
|
+
*/
|
|
625
|
+
estimate: function(mesh, options = {}) {
|
|
626
|
+
const { process = 'FDM', material = 'pla', machine = 'ender3', quantity = 1 } = options;
|
|
627
|
+
|
|
628
|
+
const bbox = new THREE.Box3().setFromObject(mesh);
|
|
629
|
+
const volume = (bbox.max.x - bbox.min.x) * (bbox.max.y - bbox.min.y) * (bbox.max.z - bbox.min.z);
|
|
630
|
+
const volumeCm3 = Math.abs(volume) / 1000;
|
|
631
|
+
|
|
632
|
+
let estimate = {};
|
|
633
|
+
|
|
634
|
+
switch (process.toUpperCase()) {
|
|
635
|
+
case 'FDM':
|
|
636
|
+
estimate = {
|
|
637
|
+
process: 'FDM 3D Printing',
|
|
638
|
+
machine: MACHINE_PROFILES[machine]?.name || 'Generic FDM',
|
|
639
|
+
volumeCm3: round(volumeCm3, 2),
|
|
640
|
+
material,
|
|
641
|
+
materialWeight: round(volumeCm3 * (MATERIAL_DENSITIES[material] || 1.24), 1),
|
|
642
|
+
materialCost: round(volumeCm3 * (MATERIAL_DENSITIES[material] || 1.24) * (MATERIAL_COSTS[material] || 8.00) / 1000, 2),
|
|
643
|
+
machineTime: Math.round((volumeCm3 / 10) * 60),
|
|
644
|
+
machineCost: round((volumeCm3 / 10) * 0.5, 2),
|
|
645
|
+
setupCost: 0,
|
|
646
|
+
totalPerUnit: round(volumeCm3 * (MATERIAL_DENSITIES[material] || 1.24) * (MATERIAL_COSTS[material] || 8.00) / 1000 + (volumeCm3 / 10) * 0.5, 2),
|
|
647
|
+
totalBatch: round(quantity * (volumeCm3 * (MATERIAL_DENSITIES[material] || 1.24) * (MATERIAL_COSTS[material] || 8.00) / 1000 + (volumeCm3 / 10) * 0.5), 2),
|
|
648
|
+
quantity
|
|
649
|
+
};
|
|
650
|
+
break;
|
|
651
|
+
|
|
652
|
+
case 'CNC':
|
|
653
|
+
const materialWeight = volumeCm3 * (MATERIAL_DENSITIES.aluminum || 2.70);
|
|
654
|
+
estimate = {
|
|
655
|
+
process: 'CNC Machining',
|
|
656
|
+
machine: MACHINE_PROFILES[machine]?.name || 'Generic CNC',
|
|
657
|
+
volumeCm3: round(volumeCm3, 2),
|
|
658
|
+
material: 'Aluminum 6061',
|
|
659
|
+
materialWeight: round(materialWeight, 1),
|
|
660
|
+
materialCost: round(materialWeight * 1.20 / 1000, 2),
|
|
661
|
+
machineTime: Math.round(volumeCm3 * 5),
|
|
662
|
+
machineCost: round(volumeCm3 * 0.5, 2),
|
|
663
|
+
setupCost: 25,
|
|
664
|
+
totalPerUnit: round(materialWeight * 1.20 / 1000 + volumeCm3 * 0.5 + 25 / quantity, 2),
|
|
665
|
+
totalBatch: round(quantity * (materialWeight * 1.20 / 1000 + volumeCm3 * 0.5) + 25, 2),
|
|
666
|
+
quantity
|
|
667
|
+
};
|
|
668
|
+
break;
|
|
669
|
+
|
|
670
|
+
case 'SLA':
|
|
671
|
+
estimate = {
|
|
672
|
+
process: 'SLA Resin Printing',
|
|
673
|
+
machine: MACHINE_PROFILES[machine]?.name || 'Generic SLA',
|
|
674
|
+
volumeCm3: round(volumeCm3, 2),
|
|
675
|
+
material: 'Standard Resin',
|
|
676
|
+
materialCost: round(volumeCm3 * 0.035, 2),
|
|
677
|
+
machineTime: Math.round((volumeCm3 / 10) * 40),
|
|
678
|
+
machineCost: round((volumeCm3 / 10) * 1.5, 2),
|
|
679
|
+
setupCost: 0,
|
|
680
|
+
totalPerUnit: round(volumeCm3 * 0.035 + (volumeCm3 / 10) * 1.5, 2),
|
|
681
|
+
totalBatch: round(quantity * (volumeCm3 * 0.035 + (volumeCm3 / 10) * 1.5), 2),
|
|
682
|
+
quantity
|
|
683
|
+
};
|
|
684
|
+
break;
|
|
685
|
+
|
|
686
|
+
default:
|
|
687
|
+
throw new Error(`Process "${process}" not supported. Use: FDM, CNC, SLA`);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
estimate.timeMinutesReadable = `${Math.floor(estimate.machineTime / 60)}h ${estimate.machineTime % 60}m`;
|
|
691
|
+
estimate.currency = 'EUR';
|
|
692
|
+
|
|
693
|
+
return estimate;
|
|
694
|
+
},
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* Get available machines
|
|
698
|
+
*/
|
|
699
|
+
getMachines: function() {
|
|
700
|
+
return Object.entries(MACHINE_PROFILES).map(([id, profile]) => ({
|
|
701
|
+
id,
|
|
702
|
+
...profile
|
|
703
|
+
}));
|
|
704
|
+
},
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* Get available tools for CNC
|
|
708
|
+
*/
|
|
709
|
+
getTools: function() {
|
|
710
|
+
return TOOL_LIBRARY;
|
|
711
|
+
},
|
|
712
|
+
|
|
713
|
+
/**
|
|
714
|
+
* Get material data
|
|
715
|
+
*/
|
|
716
|
+
getMaterials: function() {
|
|
717
|
+
return {
|
|
718
|
+
materials: Object.keys(MATERIAL_DENSITIES).map(name => ({
|
|
719
|
+
name,
|
|
720
|
+
density: MATERIAL_DENSITIES[name],
|
|
721
|
+
costPerKg: MATERIAL_COSTS[name]
|
|
722
|
+
}))
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
};
|
|
726
|
+
|
|
727
|
+
// ============================================================================
|
|
728
|
+
// Helper: Generate G-code for slicing
|
|
729
|
+
// ============================================================================
|
|
730
|
+
function generateSlicingGcode(params) {
|
|
731
|
+
const { mesh, layerHeight, infill, shells, nozzleTemp, bedTemp, retraction, material } = params;
|
|
732
|
+
let gcode = [];
|
|
733
|
+
|
|
734
|
+
gcode.push('; Slicing G-code generated by cycleCAD');
|
|
735
|
+
gcode.push(`; Material: ${material}, Layer Height: ${layerHeight}mm, Infill: ${infill}%`);
|
|
736
|
+
gcode.push('G28 ; Home all axes');
|
|
737
|
+
gcode.push(`M140 S${bedTemp} ; Set bed temperature`);
|
|
738
|
+
gcode.push(`M104 S${nozzleTemp} ; Set nozzle temperature`);
|
|
739
|
+
gcode.push('M109 S' + nozzleTemp + ' ; Wait for nozzle');
|
|
740
|
+
gcode.push('M190 S' + bedTemp + ' ; Wait for bed');
|
|
741
|
+
gcode.push('G92 E0 ; Reset extruder');
|
|
742
|
+
gcode.push('G1 Z2.0 F3000 ; Lift nozzle');
|
|
743
|
+
gcode.push('G1 X10 Y10 F3000');
|
|
744
|
+
gcode.push('; --- Layer Data ---');
|
|
745
|
+
|
|
746
|
+
// Simplified layer generation
|
|
747
|
+
const bbox = new THREE.Box3().setFromObject(mesh);
|
|
748
|
+
const layers = Math.ceil((bbox.max.z - bbox.min.z) / layerHeight);
|
|
749
|
+
|
|
750
|
+
for (let layer = 0; layer < Math.min(layers, 20); layer++) {
|
|
751
|
+
gcode.push(`; Layer ${layer}`);
|
|
752
|
+
gcode.push(`G0 Z${(bbox.min.z + layer * layerHeight).toFixed(2)}`);
|
|
753
|
+
gcode.push(`G1 X${(bbox.min.x + 10).toFixed(1)} Y${(bbox.min.y + 10).toFixed(1)} E${(layer * 2).toFixed(1)} F3000`);
|
|
754
|
+
gcode.push(`G1 X${(bbox.max.x - 10).toFixed(1)} Y${(bbox.max.y - 10).toFixed(1)} E${((layer + 1) * 2).toFixed(1)} F3000`);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
gcode.push('; --- End ---');
|
|
758
|
+
gcode.push('M104 S0 ; Turn off nozzle heater');
|
|
759
|
+
gcode.push('M140 S0 ; Turn off bed');
|
|
760
|
+
gcode.push('G28 X0 Y0 ; Home XY');
|
|
761
|
+
gcode.push('M84 ; Disable motors');
|
|
762
|
+
|
|
763
|
+
return gcode.join('\n');
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// ============================================================================
|
|
767
|
+
// Helper: Generate G-code for CNC toolpath
|
|
768
|
+
// ============================================================================
|
|
769
|
+
function generateToolpathGcode(params) {
|
|
770
|
+
const { mesh, tool, strategy, depthPerPass, feedRate, spindle } = params;
|
|
771
|
+
let gcode = [];
|
|
772
|
+
|
|
773
|
+
gcode.push('; CNC Toolpath G-code generated by cycleCAD');
|
|
774
|
+
gcode.push(`; Tool: ${tool.name} (${tool.diameter}mm), Strategy: ${strategy}`);
|
|
775
|
+
gcode.push(`; Spindle: ${spindle} RPM, Feed: ${feedRate} mm/min`);
|
|
776
|
+
gcode.push('G90 ; Absolute positioning');
|
|
777
|
+
gcode.push('G94 ; Inches per minute feed');
|
|
778
|
+
gcode.push(`M3 S${spindle} ; Spindle on`);
|
|
779
|
+
gcode.push(`G0 X0 Y0 Z5 ; Move to start`);
|
|
780
|
+
|
|
781
|
+
const bbox = new THREE.Box3().setFromObject(mesh);
|
|
782
|
+
const passes = Math.ceil((bbox.max.z - bbox.min.z) / depthPerPass);
|
|
783
|
+
|
|
784
|
+
for (let pass = 0; pass < Math.min(passes, 10); pass++) {
|
|
785
|
+
const z = bbox.min.z + (pass + 1) * depthPerPass;
|
|
786
|
+
gcode.push(`; Pass ${pass + 1}`);
|
|
787
|
+
gcode.push(`G1 Z${z.toFixed(2)} F${feedRate}`);
|
|
788
|
+
gcode.push(`G1 X${(bbox.min.x + 10).toFixed(1)} Y${(bbox.min.y + 10).toFixed(1)} F${feedRate}`);
|
|
789
|
+
gcode.push(`G1 X${(bbox.max.x - 10).toFixed(1)} Y${(bbox.max.y - 10).toFixed(1)} F${feedRate}`);
|
|
790
|
+
gcode.push(`G0 Z5 F3000 ; Rapid retract`);
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
gcode.push('M5 ; Spindle off');
|
|
794
|
+
gcode.push('G0 X0 Y0 Z5 ; Return to home');
|
|
795
|
+
|
|
796
|
+
return gcode.join('\n');
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// ============================================================================
|
|
800
|
+
// Helper: Generate SVG for nesting preview
|
|
801
|
+
// ============================================================================
|
|
802
|
+
function generateNestingSVG(sheetSize, placements, occupied) {
|
|
803
|
+
const scale = 0.2; // Scale down for preview
|
|
804
|
+
const svgWidth = sheetSize.width * scale;
|
|
805
|
+
const svgHeight = sheetSize.height * scale;
|
|
806
|
+
|
|
807
|
+
let svg = `<svg width="${svgWidth}" height="${svgHeight}" viewBox="0 0 ${sheetSize.width} ${sheetSize.height}" xmlns="http://www.w3.org/2000/svg">`;
|
|
808
|
+
svg += `<rect width="${sheetSize.width}" height="${sheetSize.height}" fill="#f5f5f5" stroke="#333" stroke-width="2"/>`;
|
|
809
|
+
|
|
810
|
+
// Draw placements
|
|
811
|
+
const colors = ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6'];
|
|
812
|
+
placements.forEach((p, i) => {
|
|
813
|
+
const color = colors[i % colors.length];
|
|
814
|
+
svg += `<rect x="${p.x}" y="${p.y}" width="${p.width}" height="${p.height}" fill="${color}" opacity="0.7" stroke="#000" stroke-width="1"/>`;
|
|
815
|
+
svg += `<text x="${p.x + p.width/2}" y="${p.y + p.height/2}" text-anchor="middle" font-size="10" font-family="Arial">${p.partId}</text>`;
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
svg += '</svg>';
|
|
819
|
+
return svg;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// ============================================================================
|
|
823
|
+
// Helper: Utility functions
|
|
824
|
+
// ============================================================================
|
|
825
|
+
function round(num, decimals = 2) {
|
|
826
|
+
return Math.round(num * Math.pow(10, decimals)) / Math.pow(10, decimals);
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// ============================================================================
|
|
830
|
+
// Export CAM API as window.cycleCAD.cam
|
|
831
|
+
// ============================================================================
|
|
832
|
+
if (!window.cycleCAD) {
|
|
833
|
+
window.cycleCAD = {};
|
|
834
|
+
}
|
|
835
|
+
window.cycleCAD.cam = camAPI;
|
|
836
|
+
|
|
837
|
+
console.log('[CAM Pipeline] Initialized. window.cycleCAD.cam ready.');
|
|
838
|
+
console.log('[CAM] Available commands:', Object.keys(camAPI).join(', '));
|
|
839
|
+
|
|
840
|
+
export default camAPI;
|