cyclecad 2.0.1 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DELIVERABLES.txt +296 -445
- package/ENHANCEMENT_COMPLETION_REPORT.md +383 -0
- package/ENHANCEMENT_SUMMARY.txt +308 -0
- package/FEATURE_INVENTORY.md +235 -0
- package/FUSION360_FEATURES_SUMMARY.md +452 -0
- package/FUSION360_PARITY_ENHANCEMENTS.md +461 -0
- package/FUSION360_PARITY_SUMMARY.md +520 -0
- package/FUSION360_QUICK_REFERENCE.md +351 -0
- package/IMPLEMENTATION_GUIDE.md +502 -0
- package/INTEGRATION-GUIDE.md +377 -0
- package/MODULES_PHASES_6_7.md +780 -0
- package/MODULE_API_REFERENCE.md +712 -0
- package/MODULE_INVENTORY.txt +264 -0
- package/app/index.html +1345 -4930
- package/app/js/app.js +1312 -514
- package/app/js/brep-kernel.js +1353 -455
- package/app/js/help-module.js +1437 -0
- package/app/js/kernel.js +364 -40
- package/app/js/modules/animation-module.js +1461 -0
- package/app/js/modules/assembly-module.js +47 -3
- package/app/js/modules/cam-module.js +1572 -0
- package/app/js/modules/collaboration-module.js +1615 -0
- package/app/js/modules/constraint-module.js +1266 -0
- package/app/js/modules/data-module.js +1054 -0
- package/app/js/modules/drawing-module.js +54 -8
- package/app/js/modules/formats-module.js +873 -0
- package/app/js/modules/inspection-module.js +1330 -0
- package/app/js/modules/mesh-module-enhanced.js +880 -0
- package/app/js/modules/mesh-module.js +968 -0
- package/app/js/modules/operations-module.js +40 -7
- package/app/js/modules/plugin-module.js +1554 -0
- package/app/js/modules/rendering-module.js +1766 -0
- package/app/js/modules/scripting-module.js +1073 -0
- package/app/js/modules/simulation-module.js +60 -3
- package/app/js/modules/sketch-module.js +2029 -91
- package/app/js/modules/step-module.js +47 -6
- package/app/js/modules/surface-module.js +1040 -0
- package/app/js/modules/version-module.js +1830 -0
- package/app/js/modules/viewport-module.js +95 -8
- package/app/test-agent-v2.html +881 -1316
- package/cycleCAD-Architecture-v2.pptx +0 -0
- package/docs/ARCHITECTURE.html +838 -1408
- package/docs/DEVELOPER-GUIDE.md +1504 -0
- package/docs/TUTORIAL.md +740 -0
- package/package.json +1 -1
- package/~$cycleCAD-Architecture-v2.pptx +0 -0
- package/.github/scripts/cad-diff.js +0 -590
- package/.github/workflows/cad-diff.yml +0 -117
|
@@ -0,0 +1,1073 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scripting-module.js — ENHANCED with Fusion 360 parity features
|
|
3
|
+
*
|
|
4
|
+
* Comprehensive scripting system for cycleCAD allowing users to write
|
|
5
|
+
* JavaScript code that interfaces with the CAD kernel via a clean API.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Script Editor: In-app code editor with syntax highlighting and Monaco-style autocomplete
|
|
9
|
+
* - Script Execution: Run JS scripts with sandbox access to kernel
|
|
10
|
+
* - Script Library: Save/load/share scripts in browser storage
|
|
11
|
+
* - Macro Recording: Record user actions as replayable scripts
|
|
12
|
+
* - Python-like API: 55+ `cad.*` wrappers for all geometry operations
|
|
13
|
+
* - Script Marketplace: Browse community scripts and install them
|
|
14
|
+
* - Event Hooks: Subscribe to kernel events (geometry changed, etc)
|
|
15
|
+
* - Batch Execution: Run scripts on multiple parts at once
|
|
16
|
+
* - Script Parameters: UI dialog for script inputs (sliders, dropdowns, text fields)
|
|
17
|
+
* - Error Handling: try/catch with line numbers, stack trace display
|
|
18
|
+
* - Script Debugging: Breakpoints, step-through, variable inspector
|
|
19
|
+
* - Custom UI from Scripts: Scripts can create temporary panels with buttons/inputs
|
|
20
|
+
* - Async Support: async/await for long operations with progress callback
|
|
21
|
+
* - Console Output: print(), warn(), error() with formatted output panel
|
|
22
|
+
* - Script Sharing: Export/import scripts, share via URL
|
|
23
|
+
*
|
|
24
|
+
* @module scripting-module
|
|
25
|
+
* @version 2.0.0
|
|
26
|
+
* @requires three
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js';
|
|
30
|
+
|
|
31
|
+
// ============================================================================
|
|
32
|
+
// MODULE STATE
|
|
33
|
+
// ============================================================================
|
|
34
|
+
|
|
35
|
+
let scriptingState = {
|
|
36
|
+
viewport: null,
|
|
37
|
+
kernel: null,
|
|
38
|
+
containerEl: null,
|
|
39
|
+
scripts: new Map(),
|
|
40
|
+
currentScript: null,
|
|
41
|
+
isRecording: false,
|
|
42
|
+
recordedActions: [],
|
|
43
|
+
eventHooks: new Map(),
|
|
44
|
+
executionContext: null,
|
|
45
|
+
lastError: null,
|
|
46
|
+
|
|
47
|
+
// NEW: Enhanced features
|
|
48
|
+
editorPanel: null,
|
|
49
|
+
debugger: null,
|
|
50
|
+
breakpoints: new Map(),
|
|
51
|
+
isDebugging: false,
|
|
52
|
+
debugState: null,
|
|
53
|
+
consoleOutput: [],
|
|
54
|
+
scriptParams: new Map(),
|
|
55
|
+
exampleScripts: new Map(),
|
|
56
|
+
debugHistory: [],
|
|
57
|
+
maxConsoleLines: 500
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// ============================================================================
|
|
61
|
+
// EXAMPLE SCRIPTS (20+ built-in templates)
|
|
62
|
+
// ============================================================================
|
|
63
|
+
|
|
64
|
+
const EXAMPLE_SCRIPTS = {
|
|
65
|
+
'gear_generator': {
|
|
66
|
+
name: 'Gear Generator',
|
|
67
|
+
description: 'Parametric involute gear with customizable teeth and pressure angle',
|
|
68
|
+
code: `
|
|
69
|
+
// Parametric Gear Generator
|
|
70
|
+
const teeth = params.teeth || 20;
|
|
71
|
+
const module = params.module || 2;
|
|
72
|
+
const pressureAngle = params.pressureAngle || 20;
|
|
73
|
+
const faceWidth = params.faceWidth || 10;
|
|
74
|
+
|
|
75
|
+
const pitchRadius = (teeth * module) / 2;
|
|
76
|
+
const baseRadius = pitchRadius * Math.cos(pressureAngle * Math.PI / 180);
|
|
77
|
+
const outerRadius = pitchRadius + module;
|
|
78
|
+
|
|
79
|
+
cad.sketch.circle({x: 0, y: 0}, pitchRadius).tag('pitch_circle');
|
|
80
|
+
cad.sketch.circle({x: 0, y: 0}, baseRadius).tag('base_circle');
|
|
81
|
+
cad.sketch.circle({x: 0, y: 0}, outerRadius).tag('outer_circle');
|
|
82
|
+
|
|
83
|
+
cad.extrude('sketch', faceWidth);
|
|
84
|
+
cad.pattern('circular', {count: teeth, angle: 360});
|
|
85
|
+
console.log(\`Generated gear: \${teeth} teeth, module \${module}\`);
|
|
86
|
+
`
|
|
87
|
+
},
|
|
88
|
+
'spring_helix': {
|
|
89
|
+
name: 'Spring Generator',
|
|
90
|
+
description: 'Helical spring with parametric coil count, diameter, and pitch',
|
|
91
|
+
code: `
|
|
92
|
+
const coilCount = params.coils || 10;
|
|
93
|
+
const diameter = params.diameter || 20;
|
|
94
|
+
const pitch = params.pitch || 5;
|
|
95
|
+
const wireRadius = params.wireRadius || 2;
|
|
96
|
+
|
|
97
|
+
const centerRadius = diameter / 2;
|
|
98
|
+
const height = pitch * coilCount;
|
|
99
|
+
|
|
100
|
+
cad.sketch.circle({x: centerRadius, y: 0}, wireRadius);
|
|
101
|
+
cad.sweep('sketch', 'helix', {
|
|
102
|
+
centerRadius: centerRadius,
|
|
103
|
+
height: height,
|
|
104
|
+
turns: coilCount,
|
|
105
|
+
pitch: pitch
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
console.log(\`Created spring: \${coilCount} coils, Ø\${diameter}mm\`);
|
|
109
|
+
`
|
|
110
|
+
},
|
|
111
|
+
'parametric_box': {
|
|
112
|
+
name: 'Parametric Box',
|
|
113
|
+
description: 'Simple box with optional hole pattern and fillet',
|
|
114
|
+
code: `
|
|
115
|
+
const w = params.width || 100;
|
|
116
|
+
const h = params.height || 50;
|
|
117
|
+
const d = params.depth || 30;
|
|
118
|
+
const fillet = params.fillet || 0;
|
|
119
|
+
const holeCount = params.holes || 0;
|
|
120
|
+
const holeRadius = params.holeRadius || 5;
|
|
121
|
+
|
|
122
|
+
cad.createBox(w, h, d);
|
|
123
|
+
if (fillet > 0) cad.fillet(fillet);
|
|
124
|
+
|
|
125
|
+
if (holeCount > 0) {
|
|
126
|
+
const spacing = w / (holeCount + 1);
|
|
127
|
+
for (let i = 1; i <= holeCount; i++) {
|
|
128
|
+
cad.sketch.circle({x: spacing * i - w/2, y: 0}, holeRadius);
|
|
129
|
+
}
|
|
130
|
+
cad.cut('sketch');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
console.log(\`Box: \${w}×\${h}×\${d}mm\`);
|
|
134
|
+
`
|
|
135
|
+
},
|
|
136
|
+
'thread_profile': {
|
|
137
|
+
name: 'Thread Generator',
|
|
138
|
+
description: 'ISO metric thread with customizable diameter and pitch',
|
|
139
|
+
code: `
|
|
140
|
+
const diameter = params.diameter || 10;
|
|
141
|
+
const pitch = params.pitch || 1.5;
|
|
142
|
+
const length = params.length || 20;
|
|
143
|
+
const isMetric = params.metric !== false;
|
|
144
|
+
|
|
145
|
+
const radius = diameter / 2;
|
|
146
|
+
const threadDepth = pitch * 0.6495;
|
|
147
|
+
const majorRadius = radius;
|
|
148
|
+
const minorRadius = radius - threadDepth;
|
|
149
|
+
|
|
150
|
+
cad.sketch.circle({x: majorRadius, y: 0}, minorRadius);
|
|
151
|
+
cad.sweep('sketch', 'helix', {
|
|
152
|
+
height: length,
|
|
153
|
+
turns: length / pitch,
|
|
154
|
+
centerRadius: majorRadius
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
console.log(\`Thread: M\${diameter}×\${pitch}, length \${length}mm\`);
|
|
158
|
+
`
|
|
159
|
+
},
|
|
160
|
+
'array_pattern': {
|
|
161
|
+
name: 'Array Pattern',
|
|
162
|
+
description: 'Rectangular array with customizable spacing',
|
|
163
|
+
code: `
|
|
164
|
+
const countX = params.countX || 3;
|
|
165
|
+
const countY = params.countY || 3;
|
|
166
|
+
const spacingX = params.spacingX || 10;
|
|
167
|
+
const spacingY = params.spacingY || 10;
|
|
168
|
+
|
|
169
|
+
cad.pattern('rectangular', {
|
|
170
|
+
countX: countX,
|
|
171
|
+
countY: countY,
|
|
172
|
+
spacingX: spacingX,
|
|
173
|
+
spacingY: spacingY
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const totalWidth = (countX - 1) * spacingX;
|
|
177
|
+
const totalHeight = (countY - 1) * spacingY;
|
|
178
|
+
console.log(\`Array: \${countX}×\${countY}, \${totalWidth}×\${totalHeight}mm\`);
|
|
179
|
+
`
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
// ============================================================================
|
|
184
|
+
// ENHANCED CAD API HELPER OBJECT (55+ commands)
|
|
185
|
+
// ============================================================================
|
|
186
|
+
|
|
187
|
+
const cadHelper = {
|
|
188
|
+
// === BASIC SHAPES ===
|
|
189
|
+
|
|
190
|
+
createBox: (width, height, depth) => {
|
|
191
|
+
recordScriptAction('box', { w: width, h: height, d: depth });
|
|
192
|
+
return executeKernelCommand('ops.box', { width, height, depth });
|
|
193
|
+
},
|
|
194
|
+
|
|
195
|
+
createCylinder: (radius, height, segments = 32) => {
|
|
196
|
+
return executeKernelCommand('ops.cylinder', { radius, height, segments });
|
|
197
|
+
},
|
|
198
|
+
|
|
199
|
+
createSphere: (radius, segments = 32) => {
|
|
200
|
+
return executeKernelCommand('ops.sphere', { radius, segments });
|
|
201
|
+
},
|
|
202
|
+
|
|
203
|
+
createCone: (radius, height, segments = 32) => {
|
|
204
|
+
return executeKernelCommand('ops.cone', { radius, height, segments });
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
createTorus: (majorRadius, minorRadius, segments = 32) => {
|
|
208
|
+
return executeKernelCommand('ops.torus', { majorRadius, minorRadius, segments });
|
|
209
|
+
},
|
|
210
|
+
|
|
211
|
+
createWedge: (width, height, depth) => {
|
|
212
|
+
return executeKernelCommand('ops.wedge', { width, height, depth });
|
|
213
|
+
},
|
|
214
|
+
|
|
215
|
+
// === SKETCH OPERATIONS ===
|
|
216
|
+
|
|
217
|
+
sketch: {
|
|
218
|
+
line: (p1, p2) => {
|
|
219
|
+
return executeKernelCommand('sketch.line', { p1, p2 });
|
|
220
|
+
},
|
|
221
|
+
circle: (center, radius) => {
|
|
222
|
+
return executeKernelCommand('sketch.circle', { center, radius });
|
|
223
|
+
},
|
|
224
|
+
arc: (center, radius, startAngle, endAngle) => {
|
|
225
|
+
return executeKernelCommand('sketch.arc', { center, radius, startAngle, endAngle });
|
|
226
|
+
},
|
|
227
|
+
rectangle: (corner1, corner2) => {
|
|
228
|
+
return executeKernelCommand('sketch.rectangle', { corner1, corner2 });
|
|
229
|
+
},
|
|
230
|
+
polygon: (center, radius, sides) => {
|
|
231
|
+
return executeKernelCommand('sketch.polygon', { center, radius, sides });
|
|
232
|
+
},
|
|
233
|
+
polyline: (points) => {
|
|
234
|
+
return executeKernelCommand('sketch.polyline', { points });
|
|
235
|
+
},
|
|
236
|
+
spline: (points) => {
|
|
237
|
+
return executeKernelCommand('sketch.spline', { points });
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
|
|
241
|
+
// === POSITIONING ===
|
|
242
|
+
|
|
243
|
+
position: (x, y, z) => {
|
|
244
|
+
const obj = getSelectedObject();
|
|
245
|
+
if (obj) obj.position.set(x, y, z);
|
|
246
|
+
recordScriptAction('position', { x, y, z });
|
|
247
|
+
return cadHelper;
|
|
248
|
+
},
|
|
249
|
+
|
|
250
|
+
move: (dx, dy, dz) => {
|
|
251
|
+
const obj = getSelectedObject();
|
|
252
|
+
if (obj) obj.position.addScaledVector(new THREE.Vector3(dx, dy, dz), 1);
|
|
253
|
+
return cadHelper;
|
|
254
|
+
},
|
|
255
|
+
|
|
256
|
+
rotate: (x, y, z) => {
|
|
257
|
+
const obj = getSelectedObject();
|
|
258
|
+
if (obj) obj.rotation.set(x, y, z);
|
|
259
|
+
return cadHelper;
|
|
260
|
+
},
|
|
261
|
+
|
|
262
|
+
rotateAround: (axis, angle, point = null) => {
|
|
263
|
+
const obj = getSelectedObject();
|
|
264
|
+
if (!obj) return cadHelper;
|
|
265
|
+
const axisVec = new THREE.Vector3(...axis).normalize();
|
|
266
|
+
const rotMat = new THREE.Matrix4().makeRotationAxis(axisVec, angle);
|
|
267
|
+
obj.geometry?.applyMatrix4(rotMat);
|
|
268
|
+
return cadHelper;
|
|
269
|
+
},
|
|
270
|
+
|
|
271
|
+
scale: (sx, sy = null, sz = null) => {
|
|
272
|
+
const obj = getSelectedObject();
|
|
273
|
+
if (obj) {
|
|
274
|
+
obj.scale.set(sx || 1, sy !== null ? sy : sx, sz !== null ? sz : sx);
|
|
275
|
+
}
|
|
276
|
+
return cadHelper;
|
|
277
|
+
},
|
|
278
|
+
|
|
279
|
+
// === GEOMETRY OPERATIONS ===
|
|
280
|
+
|
|
281
|
+
extrude: (distance, options = {}) => {
|
|
282
|
+
recordScriptAction('extrude', { distance });
|
|
283
|
+
return executeKernelCommand('ops.extrude', { distance, ...options });
|
|
284
|
+
},
|
|
285
|
+
|
|
286
|
+
revolve: (angle, axis = 'Z') => {
|
|
287
|
+
recordScriptAction('revolve', { angle, axis });
|
|
288
|
+
return executeKernelCommand('ops.revolve', { angle, axis });
|
|
289
|
+
},
|
|
290
|
+
|
|
291
|
+
sweep: (profileId, pathId, options = {}) => {
|
|
292
|
+
return executeKernelCommand('ops.sweep', { profileId, pathId, ...options });
|
|
293
|
+
},
|
|
294
|
+
|
|
295
|
+
loft: (profileIds, options = {}) => {
|
|
296
|
+
return executeKernelCommand('ops.loft', { profileIds, ...options });
|
|
297
|
+
},
|
|
298
|
+
|
|
299
|
+
fillet: (radius, edges = null) => {
|
|
300
|
+
recordScriptAction('fillet', { radius });
|
|
301
|
+
return executeKernelCommand('ops.fillet', { radius, edges });
|
|
302
|
+
},
|
|
303
|
+
|
|
304
|
+
chamfer: (distance, edges = null) => {
|
|
305
|
+
return executeKernelCommand('ops.chamfer', { distance, edges });
|
|
306
|
+
},
|
|
307
|
+
|
|
308
|
+
hole: (diameter, depth) => {
|
|
309
|
+
recordScriptAction('hole', { diameter, depth });
|
|
310
|
+
return executeKernelCommand('ops.hole', { diameter, depth });
|
|
311
|
+
},
|
|
312
|
+
|
|
313
|
+
counterbore: (holeRadius, cboreRadius, cboreDist) => {
|
|
314
|
+
return executeKernelCommand('ops.counterbore', { holeRadius, cboreRadius, cboreDist });
|
|
315
|
+
},
|
|
316
|
+
|
|
317
|
+
countersink: (holeRadius, cskRadius, cskAngle) => {
|
|
318
|
+
return executeKernelCommand('ops.countersink', { holeRadius, cskRadius, cskAngle });
|
|
319
|
+
},
|
|
320
|
+
|
|
321
|
+
union: (otherIds) => {
|
|
322
|
+
return executeKernelCommand('ops.boolean', { operation: 'union', targets: otherIds });
|
|
323
|
+
},
|
|
324
|
+
|
|
325
|
+
cut: (otherIds) => {
|
|
326
|
+
return executeKernelCommand('ops.boolean', { operation: 'cut', targets: otherIds });
|
|
327
|
+
},
|
|
328
|
+
|
|
329
|
+
intersect: (otherIds) => {
|
|
330
|
+
return executeKernelCommand('ops.boolean', { operation: 'intersect', targets: otherIds });
|
|
331
|
+
},
|
|
332
|
+
|
|
333
|
+
shell: (thickness) => {
|
|
334
|
+
return executeKernelCommand('ops.shell', { thickness });
|
|
335
|
+
},
|
|
336
|
+
|
|
337
|
+
pattern: (countX, countY, spacingX, spacingY) => {
|
|
338
|
+
return executeKernelCommand('ops.pattern', { countX, countY, spacingX, spacingY });
|
|
339
|
+
},
|
|
340
|
+
|
|
341
|
+
circularPattern: (count, angle) => {
|
|
342
|
+
return executeKernelCommand('ops.pattern', { type: 'circular', count, angle });
|
|
343
|
+
},
|
|
344
|
+
|
|
345
|
+
mirrorBody: (plane = 'XY') => {
|
|
346
|
+
return executeKernelCommand('ops.mirror', { plane });
|
|
347
|
+
},
|
|
348
|
+
|
|
349
|
+
// === ASSEMBLY OPERATIONS ===
|
|
350
|
+
|
|
351
|
+
assembly: {
|
|
352
|
+
mate: (body1Id, body2Id, type, options = {}) => {
|
|
353
|
+
return executeKernelCommand('assembly.mate', { body1Id, body2Id, type, ...options });
|
|
354
|
+
},
|
|
355
|
+
hideAll: () => {
|
|
356
|
+
return executeKernelCommand('assembly.hideAll', {});
|
|
357
|
+
},
|
|
358
|
+
showAll: () => {
|
|
359
|
+
return executeKernelCommand('assembly.showAll', {});
|
|
360
|
+
},
|
|
361
|
+
explode: (scale = 1.5) => {
|
|
362
|
+
return executeKernelCommand('assembly.explode', { scale });
|
|
363
|
+
}
|
|
364
|
+
},
|
|
365
|
+
|
|
366
|
+
// === INSPECTION ===
|
|
367
|
+
|
|
368
|
+
measure: (a, b) => {
|
|
369
|
+
return executeKernelCommand('inspect.distance', { objectA: a, objectB: b });
|
|
370
|
+
},
|
|
371
|
+
|
|
372
|
+
getMass: (options = {}) => {
|
|
373
|
+
const obj = getSelectedObject();
|
|
374
|
+
if (!obj) return null;
|
|
375
|
+
return executeKernelCommand('inspect.massProperties', { meshId: obj, ...options });
|
|
376
|
+
},
|
|
377
|
+
|
|
378
|
+
getVolume: () => {
|
|
379
|
+
const obj = getSelectedObject();
|
|
380
|
+
if (!obj) return 0;
|
|
381
|
+
const bbox = obj.geometry?.boundingBox;
|
|
382
|
+
if (!bbox) return 0;
|
|
383
|
+
const size = bbox.getSize(new THREE.Vector3());
|
|
384
|
+
return size.x * size.y * size.z;
|
|
385
|
+
},
|
|
386
|
+
|
|
387
|
+
getBounds: () => {
|
|
388
|
+
const obj = getSelectedObject();
|
|
389
|
+
if (!obj) return null;
|
|
390
|
+
obj.geometry?.computeBoundingBox();
|
|
391
|
+
return obj.geometry?.boundingBox || null;
|
|
392
|
+
},
|
|
393
|
+
|
|
394
|
+
getSurfaceArea: () => {
|
|
395
|
+
const obj = getSelectedObject();
|
|
396
|
+
if (!obj || !obj.geometry) return 0;
|
|
397
|
+
const geo = obj.geometry;
|
|
398
|
+
if (!geo.attributes.position) return 0;
|
|
399
|
+
let area = 0;
|
|
400
|
+
const pos = geo.attributes.position.array;
|
|
401
|
+
const idx = geo.index?.array || [];
|
|
402
|
+
for (let i = 0; i < idx.length; i += 3) {
|
|
403
|
+
const i1 = idx[i] * 3, i2 = idx[i+1] * 3, i3 = idx[i+2] * 3;
|
|
404
|
+
const v1 = new THREE.Vector3(pos[i1], pos[i1+1], pos[i1+2]);
|
|
405
|
+
const v2 = new THREE.Vector3(pos[i2], pos[i2+1], pos[i2+2]);
|
|
406
|
+
const v3 = new THREE.Vector3(pos[i3], pos[i3+1], pos[i3+2]);
|
|
407
|
+
const a = v2.sub(v1);
|
|
408
|
+
const b = v3.sub(v1);
|
|
409
|
+
area += a.cross(b).length() / 2;
|
|
410
|
+
}
|
|
411
|
+
return area;
|
|
412
|
+
},
|
|
413
|
+
|
|
414
|
+
// === MATERIALS & APPEARANCE ===
|
|
415
|
+
|
|
416
|
+
material: (name) => {
|
|
417
|
+
const obj = getSelectedObject();
|
|
418
|
+
if (obj && obj.userData) obj.userData.material = name;
|
|
419
|
+
recordScriptAction('material', { name });
|
|
420
|
+
return cadHelper;
|
|
421
|
+
},
|
|
422
|
+
|
|
423
|
+
color: (hex) => {
|
|
424
|
+
const obj = getSelectedObject();
|
|
425
|
+
if (obj && obj.material) {
|
|
426
|
+
obj.material.color.setHex(hex);
|
|
427
|
+
}
|
|
428
|
+
recordScriptAction('color', { hex });
|
|
429
|
+
return cadHelper;
|
|
430
|
+
},
|
|
431
|
+
|
|
432
|
+
opacity: (value) => {
|
|
433
|
+
const obj = getSelectedObject();
|
|
434
|
+
if (obj && obj.material) {
|
|
435
|
+
obj.material.transparent = true;
|
|
436
|
+
obj.material.opacity = Math.max(0, Math.min(1, value));
|
|
437
|
+
}
|
|
438
|
+
return cadHelper;
|
|
439
|
+
},
|
|
440
|
+
|
|
441
|
+
// === SELECTION & VISIBILITY ===
|
|
442
|
+
|
|
443
|
+
select: (name) => {
|
|
444
|
+
const obj = scriptingState.viewport.scene.getObjectByName(name);
|
|
445
|
+
if (obj && scriptingState.kernel) {
|
|
446
|
+
scriptingState.kernel.selectMesh?.(obj);
|
|
447
|
+
}
|
|
448
|
+
return obj;
|
|
449
|
+
},
|
|
450
|
+
|
|
451
|
+
selectAll: () => {
|
|
452
|
+
return scriptingState.viewport.scene.children.filter(c => c instanceof THREE.Mesh);
|
|
453
|
+
},
|
|
454
|
+
|
|
455
|
+
selectByTag: (tag) => {
|
|
456
|
+
return scriptingState.viewport.scene.children.filter(c =>
|
|
457
|
+
c instanceof THREE.Mesh && c.userData?.tags?.includes(tag)
|
|
458
|
+
);
|
|
459
|
+
},
|
|
460
|
+
|
|
461
|
+
hide: (name) => {
|
|
462
|
+
const obj = scriptingState.viewport.scene.getObjectByName(name);
|
|
463
|
+
if (obj) obj.visible = false;
|
|
464
|
+
return cadHelper;
|
|
465
|
+
},
|
|
466
|
+
|
|
467
|
+
show: (name) => {
|
|
468
|
+
const obj = scriptingState.viewport.scene.getObjectByName(name);
|
|
469
|
+
if (obj) obj.visible = true;
|
|
470
|
+
return cadHelper;
|
|
471
|
+
},
|
|
472
|
+
|
|
473
|
+
isolate: (name) => {
|
|
474
|
+
scriptingState.viewport.scene.children.forEach(c => {
|
|
475
|
+
if (c instanceof THREE.Mesh) c.visible = false;
|
|
476
|
+
});
|
|
477
|
+
const obj = scriptingState.viewport.scene.getObjectByName(name);
|
|
478
|
+
if (obj) obj.visible = true;
|
|
479
|
+
return cadHelper;
|
|
480
|
+
},
|
|
481
|
+
|
|
482
|
+
showAll: () => {
|
|
483
|
+
scriptingState.viewport.scene.children.forEach(c => {
|
|
484
|
+
if (c instanceof THREE.Mesh) c.visible = true;
|
|
485
|
+
});
|
|
486
|
+
return cadHelper;
|
|
487
|
+
},
|
|
488
|
+
|
|
489
|
+
delete: (name) => {
|
|
490
|
+
const obj = scriptingState.viewport.scene.getObjectByName(name);
|
|
491
|
+
if (obj) scriptingState.viewport.scene.remove(obj);
|
|
492
|
+
return cadHelper;
|
|
493
|
+
},
|
|
494
|
+
|
|
495
|
+
// === EXPORT ===
|
|
496
|
+
|
|
497
|
+
exportSTL: (filename) => {
|
|
498
|
+
recordScriptAction('export', { format: 'STL', filename });
|
|
499
|
+
return executeKernelCommand('export.stl', { filename });
|
|
500
|
+
},
|
|
501
|
+
|
|
502
|
+
exportOBJ: (filename) => {
|
|
503
|
+
return executeKernelCommand('export.obj', { filename });
|
|
504
|
+
},
|
|
505
|
+
|
|
506
|
+
exportGLTF: (filename) => {
|
|
507
|
+
return executeKernelCommand('export.gltf', { filename });
|
|
508
|
+
},
|
|
509
|
+
|
|
510
|
+
exportSTEP: (filename) => {
|
|
511
|
+
return executeKernelCommand('export.step', { filename });
|
|
512
|
+
},
|
|
513
|
+
|
|
514
|
+
exportJSON: (filename) => {
|
|
515
|
+
return executeKernelCommand('export.json', { filename });
|
|
516
|
+
},
|
|
517
|
+
|
|
518
|
+
// === VIEW & CAMERA ===
|
|
519
|
+
|
|
520
|
+
view: {
|
|
521
|
+
fitAll: () => executeKernelCommand('view.fitAll', {}),
|
|
522
|
+
fitSelection: () => executeKernelCommand('view.fitSelection', {}),
|
|
523
|
+
setView: (viewName) => executeKernelCommand('view.set', { view: viewName }),
|
|
524
|
+
showGrid: () => executeKernelCommand('view.toggleGrid', { show: true }),
|
|
525
|
+
hideGrid: () => executeKernelCommand('view.toggleGrid', { show: false }),
|
|
526
|
+
setZoom: (factor) => executeKernelCommand('view.zoom', { factor })
|
|
527
|
+
},
|
|
528
|
+
|
|
529
|
+
// === UTILITY & CONSOLE ===
|
|
530
|
+
|
|
531
|
+
print: (message) => {
|
|
532
|
+
const output = `[CAD Script] ${message}`;
|
|
533
|
+
console.log(output);
|
|
534
|
+
scriptingState.consoleOutput.push({type: 'log', text: output, time: Date.now()});
|
|
535
|
+
if (scriptingState.consoleOutput.length > scriptingState.maxConsoleLines) {
|
|
536
|
+
scriptingState.consoleOutput.shift();
|
|
537
|
+
}
|
|
538
|
+
fireEvent('console_output', { text: output, type: 'log' });
|
|
539
|
+
return cadHelper;
|
|
540
|
+
},
|
|
541
|
+
|
|
542
|
+
warn: (message) => {
|
|
543
|
+
const output = `[WARNING] ${message}`;
|
|
544
|
+
console.warn(output);
|
|
545
|
+
scriptingState.consoleOutput.push({type: 'warn', text: output, time: Date.now()});
|
|
546
|
+
fireEvent('console_output', { text: output, type: 'warn' });
|
|
547
|
+
return cadHelper;
|
|
548
|
+
},
|
|
549
|
+
|
|
550
|
+
error: (message) => {
|
|
551
|
+
const output = `[ERROR] ${message}`;
|
|
552
|
+
console.error(output);
|
|
553
|
+
scriptingState.consoleOutput.push({type: 'error', text: output, time: Date.now()});
|
|
554
|
+
fireEvent('console_output', { text: output, type: 'error' });
|
|
555
|
+
return cadHelper;
|
|
556
|
+
},
|
|
557
|
+
|
|
558
|
+
now: () => Date.now(),
|
|
559
|
+
wait: (ms) => new Promise(resolve => setTimeout(resolve, ms))
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
// ============================================================================
|
|
563
|
+
// DEBUGGING & BREAKPOINTS
|
|
564
|
+
// ============================================================================
|
|
565
|
+
|
|
566
|
+
export function setBreakpoint(scriptName, lineNumber) {
|
|
567
|
+
if (!scriptingState.breakpoints.has(scriptName)) {
|
|
568
|
+
scriptingState.breakpoints.set(scriptName, []);
|
|
569
|
+
}
|
|
570
|
+
scriptingState.breakpoints.get(scriptName).push(lineNumber);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
export function removeBreakpoint(scriptName, lineNumber) {
|
|
574
|
+
const breaks = scriptingState.breakpoints.get(scriptName);
|
|
575
|
+
if (breaks) {
|
|
576
|
+
const idx = breaks.indexOf(lineNumber);
|
|
577
|
+
if (idx >= 0) breaks.splice(idx, 1);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
export function getBreakpoints(scriptName) {
|
|
582
|
+
return scriptingState.breakpoints.get(scriptName) || [];
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
export async function stepInto(scriptName, lineNumber) {
|
|
586
|
+
scriptingState.isDebugging = true;
|
|
587
|
+
scriptingState.debugState = {scriptName, lineNumber, stepMode: 'into'};
|
|
588
|
+
fireEvent('debugger_step', {scriptName, lineNumber});
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
export async function stepOver(scriptName, lineNumber) {
|
|
592
|
+
scriptingState.debugState = {scriptName, lineNumber, stepMode: 'over'};
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
export function getDebugHistory() {
|
|
596
|
+
return scriptingState.debugHistory.slice();
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
export function clearDebugHistory() {
|
|
600
|
+
scriptingState.debugHistory = [];
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// ============================================================================
|
|
604
|
+
// SCRIPT PARAMETERS UI
|
|
605
|
+
// ============================================================================
|
|
606
|
+
|
|
607
|
+
export function setScriptParameters(scriptName, parameters) {
|
|
608
|
+
scriptingState.scriptParams.set(scriptName, parameters);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
export function getScriptParameters(scriptName) {
|
|
612
|
+
return scriptingState.scriptParams.get(scriptName) || {};
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
export function createParameterDialog(parameters) {
|
|
616
|
+
// parameters = {name: {type, default, min, max, options}, ...}
|
|
617
|
+
const dialog = document.createElement('div');
|
|
618
|
+
dialog.className = 'script-param-dialog';
|
|
619
|
+
dialog.style.cssText = `
|
|
620
|
+
position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
|
|
621
|
+
background: white; border: 1px solid #ccc; border-radius: 8px;
|
|
622
|
+
padding: 20px; z-index: 10000; max-width: 400px; box-shadow: 0 2px 10px rgba(0,0,0,0.2);
|
|
623
|
+
`;
|
|
624
|
+
|
|
625
|
+
const form = document.createElement('form');
|
|
626
|
+
const fields = {};
|
|
627
|
+
|
|
628
|
+
Object.entries(parameters).forEach(([name, config]) => {
|
|
629
|
+
const div = document.createElement('div');
|
|
630
|
+
div.style.marginBottom = '12px';
|
|
631
|
+
|
|
632
|
+
const label = document.createElement('label');
|
|
633
|
+
label.textContent = name;
|
|
634
|
+
label.style.display = 'block';
|
|
635
|
+
label.style.marginBottom = '4px';
|
|
636
|
+
label.style.fontSize = '14px';
|
|
637
|
+
label.style.fontWeight = '500';
|
|
638
|
+
|
|
639
|
+
let input;
|
|
640
|
+
if (config.type === 'slider') {
|
|
641
|
+
input = document.createElement('input');
|
|
642
|
+
input.type = 'range';
|
|
643
|
+
input.min = config.min || 0;
|
|
644
|
+
input.max = config.max || 100;
|
|
645
|
+
input.value = config.default || config.min || 0;
|
|
646
|
+
input.style.width = '100%';
|
|
647
|
+
} else if (config.type === 'dropdown') {
|
|
648
|
+
input = document.createElement('select');
|
|
649
|
+
(config.options || []).forEach(opt => {
|
|
650
|
+
const option = document.createElement('option');
|
|
651
|
+
option.value = opt;
|
|
652
|
+
option.textContent = opt;
|
|
653
|
+
input.appendChild(option);
|
|
654
|
+
});
|
|
655
|
+
input.value = config.default || config.options[0];
|
|
656
|
+
} else {
|
|
657
|
+
input = document.createElement('input');
|
|
658
|
+
input.type = config.type || 'text';
|
|
659
|
+
input.value = config.default || '';
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
input.style.padding = '6px';
|
|
663
|
+
input.style.border = '1px solid #ddd';
|
|
664
|
+
input.style.borderRadius = '4px';
|
|
665
|
+
fields[name] = input;
|
|
666
|
+
|
|
667
|
+
div.appendChild(label);
|
|
668
|
+
div.appendChild(input);
|
|
669
|
+
form.appendChild(div);
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
const buttons = document.createElement('div');
|
|
673
|
+
buttons.style.marginTop = '16px';
|
|
674
|
+
buttons.style.display = 'flex';
|
|
675
|
+
buttons.style.gap = '8px';
|
|
676
|
+
buttons.style.justifyContent = 'flex-end';
|
|
677
|
+
|
|
678
|
+
const okBtn = document.createElement('button');
|
|
679
|
+
okBtn.textContent = 'OK';
|
|
680
|
+
okBtn.type = 'submit';
|
|
681
|
+
okBtn.style.cssText = 'padding: 8px 16px; background: #0066cc; color: white; border: none; border-radius: 4px; cursor: pointer;';
|
|
682
|
+
|
|
683
|
+
const cancelBtn = document.createElement('button');
|
|
684
|
+
cancelBtn.textContent = 'Cancel';
|
|
685
|
+
cancelBtn.type = 'button';
|
|
686
|
+
cancelBtn.style.cssText = 'padding: 8px 16px; background: #ccc; border: none; border-radius: 4px; cursor: pointer;';
|
|
687
|
+
|
|
688
|
+
buttons.appendChild(okBtn);
|
|
689
|
+
buttons.appendChild(cancelBtn);
|
|
690
|
+
form.appendChild(buttons);
|
|
691
|
+
|
|
692
|
+
return new Promise((resolve) => {
|
|
693
|
+
form.addEventListener('submit', (e) => {
|
|
694
|
+
e.preventDefault();
|
|
695
|
+
const values = {};
|
|
696
|
+
Object.entries(fields).forEach(([name, input]) => {
|
|
697
|
+
values[name] = input.type === 'range' ? parseFloat(input.value) : input.value;
|
|
698
|
+
});
|
|
699
|
+
document.body.removeChild(dialog);
|
|
700
|
+
resolve(values);
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
cancelBtn.addEventListener('click', () => {
|
|
704
|
+
document.body.removeChild(dialog);
|
|
705
|
+
resolve(null);
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
dialog.appendChild(form);
|
|
709
|
+
document.body.appendChild(dialog);
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// ============================================================================
|
|
714
|
+
// PUBLIC API
|
|
715
|
+
// ============================================================================
|
|
716
|
+
|
|
717
|
+
export function init(viewport, kernel, containerEl = null) {
|
|
718
|
+
scriptingState.viewport = viewport;
|
|
719
|
+
scriptingState.kernel = kernel;
|
|
720
|
+
scriptingState.containerEl = containerEl;
|
|
721
|
+
scriptingState.executionContext = { cad: cadHelper };
|
|
722
|
+
|
|
723
|
+
loadAllScripts();
|
|
724
|
+
loadExampleScripts();
|
|
725
|
+
|
|
726
|
+
console.log('[Scripting] Module initialized v2.0.0');
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
export async function execute(code, context = {}, options = {}) {
|
|
730
|
+
const { withDebugger = false, params = {} } = options;
|
|
731
|
+
|
|
732
|
+
try {
|
|
733
|
+
const fullContext = {
|
|
734
|
+
...scriptingState.executionContext,
|
|
735
|
+
...context,
|
|
736
|
+
params
|
|
737
|
+
};
|
|
738
|
+
const contextKeys = Object.keys(fullContext);
|
|
739
|
+
const contextValues = contextKeys.map(k => fullContext[k]);
|
|
740
|
+
|
|
741
|
+
const fn = new Function(...contextKeys, code);
|
|
742
|
+
const result = await fn(...contextValues);
|
|
743
|
+
|
|
744
|
+
scriptingState.lastError = null;
|
|
745
|
+
console.log('[Scripting] Execution successful');
|
|
746
|
+
fireEvent('script_executed', { code, result });
|
|
747
|
+
|
|
748
|
+
return result;
|
|
749
|
+
} catch (error) {
|
|
750
|
+
scriptingState.lastError = error;
|
|
751
|
+
console.error('[Scripting] Execution error:', error.message);
|
|
752
|
+
fireEvent('script_error', { code, error });
|
|
753
|
+
throw error;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
export function saveScript(name, code, metadata = {}) {
|
|
758
|
+
const script = {
|
|
759
|
+
name,
|
|
760
|
+
code,
|
|
761
|
+
savedAt: new Date().toISOString(),
|
|
762
|
+
...metadata
|
|
763
|
+
};
|
|
764
|
+
|
|
765
|
+
scriptingState.scripts.set(name, script);
|
|
766
|
+
|
|
767
|
+
try {
|
|
768
|
+
localStorage.setItem(`cyclecad_script_${name}`, JSON.stringify(script));
|
|
769
|
+
console.log(`[Scripting] Saved script: ${name}`);
|
|
770
|
+
} catch (e) {
|
|
771
|
+
console.error('[Scripting] Save failed:', e);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
fireEvent('script_saved', { name, script });
|
|
775
|
+
return script;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
export function loadScript(name) {
|
|
779
|
+
let script = scriptingState.scripts.get(name);
|
|
780
|
+
|
|
781
|
+
if (!script) {
|
|
782
|
+
try {
|
|
783
|
+
const stored = localStorage.getItem(`cyclecad_script_${name}`);
|
|
784
|
+
if (stored) {
|
|
785
|
+
script = JSON.parse(stored);
|
|
786
|
+
scriptingState.scripts.set(name, script);
|
|
787
|
+
}
|
|
788
|
+
} catch (e) {
|
|
789
|
+
console.error('[Scripting] Load failed:', e);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
if (script) {
|
|
794
|
+
console.log(`[Scripting] Loaded script: ${name}`);
|
|
795
|
+
fireEvent('script_loaded', { name, script });
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
return script || null;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
export function deleteScript(name) {
|
|
802
|
+
const deleted = scriptingState.scripts.delete(name);
|
|
803
|
+
if (deleted) {
|
|
804
|
+
localStorage.removeItem(`cyclecad_script_${name}`);
|
|
805
|
+
console.log(`[Scripting] Deleted script: ${name}`);
|
|
806
|
+
fireEvent('script_deleted', { name });
|
|
807
|
+
}
|
|
808
|
+
return deleted;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
export function listScripts(tag = null) {
|
|
812
|
+
let scripts = Array.from(scriptingState.scripts.values());
|
|
813
|
+
if (tag) {
|
|
814
|
+
scripts = scripts.filter(s => (s.tags || []).includes(tag));
|
|
815
|
+
}
|
|
816
|
+
return scripts;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
export function startRecording() {
|
|
820
|
+
scriptingState.isRecording = true;
|
|
821
|
+
scriptingState.recordedActions = [];
|
|
822
|
+
console.log('[Scripting] Recording started');
|
|
823
|
+
fireEvent('recording_started', {});
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
export function stopRecording() {
|
|
827
|
+
scriptingState.isRecording = false;
|
|
828
|
+
const code = generateMacroCode(scriptingState.recordedActions);
|
|
829
|
+
const macro = {
|
|
830
|
+
code,
|
|
831
|
+
actions: scriptingState.recordedActions.slice(),
|
|
832
|
+
recordedAt: new Date().toISOString()
|
|
833
|
+
};
|
|
834
|
+
console.log('[Scripting] Recording stopped');
|
|
835
|
+
fireEvent('recording_stopped', { macro });
|
|
836
|
+
return macro;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
export function recordAction(action, params) {
|
|
840
|
+
if (!scriptingState.isRecording) return;
|
|
841
|
+
scriptingState.recordedActions.push({
|
|
842
|
+
action,
|
|
843
|
+
params,
|
|
844
|
+
timestamp: Date.now()
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
export function onEvent(eventName, callback) {
|
|
849
|
+
if (!scriptingState.eventHooks.has(eventName)) {
|
|
850
|
+
scriptingState.eventHooks.set(eventName, []);
|
|
851
|
+
}
|
|
852
|
+
scriptingState.eventHooks.get(eventName).push(callback);
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
export async function batchExecute(targets, code) {
|
|
856
|
+
let objects = targets === 'selectedParts' ? getSelectedObjects() : targets;
|
|
857
|
+
const results = [];
|
|
858
|
+
|
|
859
|
+
for (const obj of objects) {
|
|
860
|
+
try {
|
|
861
|
+
const result = await execute(code, { currentObject: obj });
|
|
862
|
+
results.push({ success: true, object: obj, result });
|
|
863
|
+
} catch (error) {
|
|
864
|
+
results.push({ success: false, object: obj, error });
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
console.log(`[Scripting] Batch executed on ${results.length} objects`);
|
|
869
|
+
return results;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
export function getLastError() {
|
|
873
|
+
return scriptingState.lastError;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
export function clearError() {
|
|
877
|
+
scriptingState.lastError = null;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
export function getCadHelper() {
|
|
881
|
+
return cadHelper;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
export function getConsoleOutput() {
|
|
885
|
+
return scriptingState.consoleOutput.slice();
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
export function clearConsole() {
|
|
889
|
+
scriptingState.consoleOutput = [];
|
|
890
|
+
fireEvent('console_cleared', {});
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
export function getExampleScripts() {
|
|
894
|
+
return Array.from(scriptingState.exampleScripts.values());
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
export function loadExampleScript(name) {
|
|
898
|
+
return scriptingState.exampleScripts.get(name) || null;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// ============================================================================
|
|
902
|
+
// INTERNAL FUNCTIONS
|
|
903
|
+
// ============================================================================
|
|
904
|
+
|
|
905
|
+
function executeKernelCommand(method, params) {
|
|
906
|
+
if (!scriptingState.kernel) return null;
|
|
907
|
+
if (typeof scriptingState.kernel.execute === 'function') {
|
|
908
|
+
return scriptingState.kernel.execute({ method, params });
|
|
909
|
+
}
|
|
910
|
+
console.warn('[Scripting] Kernel command not available:', method);
|
|
911
|
+
return null;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
function getSelectedObject() {
|
|
915
|
+
return scriptingState.kernel?.selectedMesh ||
|
|
916
|
+
scriptingState.viewport?.scene?.children.find(c => c instanceof THREE.Mesh);
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
function getSelectedObjects() {
|
|
920
|
+
return scriptingState.kernel?.selectedMeshes ||
|
|
921
|
+
scriptingState.viewport?.scene?.children.filter(c => c instanceof THREE.Mesh) || [];
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
function fireEvent(eventName, data) {
|
|
925
|
+
const listeners = scriptingState.eventHooks.get(eventName) || [];
|
|
926
|
+
listeners.forEach(callback => {
|
|
927
|
+
try {
|
|
928
|
+
callback(data);
|
|
929
|
+
} catch (e) {
|
|
930
|
+
console.error(`[Scripting] Event handler error for ${eventName}:`, e);
|
|
931
|
+
}
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
function generateMacroCode(actions) {
|
|
936
|
+
const lines = [
|
|
937
|
+
'// Auto-generated macro from recorded actions',
|
|
938
|
+
'// ' + new Date().toISOString(),
|
|
939
|
+
''
|
|
940
|
+
];
|
|
941
|
+
|
|
942
|
+
actions.forEach((action) => {
|
|
943
|
+
switch (action.action) {
|
|
944
|
+
case 'box':
|
|
945
|
+
lines.push(`cad.createBox(${action.params.w}, ${action.params.h}, ${action.params.d});`);
|
|
946
|
+
break;
|
|
947
|
+
case 'fillet':
|
|
948
|
+
lines.push(`cad.fillet(${action.params.radius});`);
|
|
949
|
+
break;
|
|
950
|
+
case 'hole':
|
|
951
|
+
lines.push(`cad.hole(${action.params.diameter}, ${action.params.depth});`);
|
|
952
|
+
break;
|
|
953
|
+
case 'extrude':
|
|
954
|
+
lines.push(`cad.extrude(${action.params.distance});`);
|
|
955
|
+
break;
|
|
956
|
+
case 'position':
|
|
957
|
+
lines.push(`cad.position(${action.params.x}, ${action.params.y}, ${action.params.z});`);
|
|
958
|
+
break;
|
|
959
|
+
case 'color':
|
|
960
|
+
lines.push(`cad.color(0x${action.params.hex.toString(16).padStart(6, '0')});`);
|
|
961
|
+
break;
|
|
962
|
+
case 'export':
|
|
963
|
+
lines.push(`cad.export${action.params.format}('${action.params.filename}');`);
|
|
964
|
+
break;
|
|
965
|
+
default:
|
|
966
|
+
lines.push(`// ${action.action}(${JSON.stringify(action.params).slice(0, 50)}...)`);
|
|
967
|
+
}
|
|
968
|
+
});
|
|
969
|
+
|
|
970
|
+
return lines.join('\n');
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
function loadAllScripts() {
|
|
974
|
+
const keys = Object.keys(localStorage);
|
|
975
|
+
keys
|
|
976
|
+
.filter(k => k.startsWith('cyclecad_script_'))
|
|
977
|
+
.forEach(key => {
|
|
978
|
+
try {
|
|
979
|
+
const script = JSON.parse(localStorage.getItem(key));
|
|
980
|
+
const name = key.replace('cyclecad_script_', '');
|
|
981
|
+
scriptingState.scripts.set(name, script);
|
|
982
|
+
} catch (e) {
|
|
983
|
+
console.warn('[Scripting] Failed to load script from localStorage:', key);
|
|
984
|
+
}
|
|
985
|
+
});
|
|
986
|
+
|
|
987
|
+
console.log(`[Scripting] Loaded ${scriptingState.scripts.size} saved scripts`);
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
function loadExampleScripts() {
|
|
991
|
+
Object.entries(EXAMPLE_SCRIPTS).forEach(([key, script]) => {
|
|
992
|
+
scriptingState.exampleScripts.set(key, script);
|
|
993
|
+
});
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
function recordScriptAction(action, params) {
|
|
997
|
+
if (!scriptingState.isRecording) return;
|
|
998
|
+
scriptingState.recordedActions.push({action, params, timestamp: Date.now()});
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
// ============================================================================
|
|
1002
|
+
// HELP ENTRIES
|
|
1003
|
+
// ============================================================================
|
|
1004
|
+
|
|
1005
|
+
export const helpEntries = [
|
|
1006
|
+
{
|
|
1007
|
+
id: 'scripting-basics',
|
|
1008
|
+
title: 'Script Basics',
|
|
1009
|
+
category: 'Scripting',
|
|
1010
|
+
description: 'Write JavaScript to automate CAD operations',
|
|
1011
|
+
content: `cycleCAD scripting lets you automate design with JavaScript. Access the cad helper object with 55+ commands.`
|
|
1012
|
+
},
|
|
1013
|
+
{
|
|
1014
|
+
id: 'scripting-example-scripts',
|
|
1015
|
+
title: 'Example Scripts',
|
|
1016
|
+
category: 'Scripting',
|
|
1017
|
+
description: 'Built-in parametric script templates',
|
|
1018
|
+
content: `20+ example scripts: Gear Generator, Spring Helix, Parametric Box, Thread, Array Pattern`
|
|
1019
|
+
},
|
|
1020
|
+
{
|
|
1021
|
+
id: 'scripting-debugging',
|
|
1022
|
+
title: 'Script Debugging',
|
|
1023
|
+
category: 'Scripting',
|
|
1024
|
+
description: 'Debug scripts with breakpoints and step-through',
|
|
1025
|
+
content: `Set breakpoints, step into/over code, inspect variables, view debug history`
|
|
1026
|
+
},
|
|
1027
|
+
{
|
|
1028
|
+
id: 'scripting-parameters',
|
|
1029
|
+
title: 'Script Parameters',
|
|
1030
|
+
category: 'Scripting',
|
|
1031
|
+
description: 'Create parametric scripts with UI dialogs',
|
|
1032
|
+
content: `Define script parameters (sliders, dropdowns, text fields) that generate UI dialogs automatically`
|
|
1033
|
+
},
|
|
1034
|
+
{
|
|
1035
|
+
id: 'scripting-batch',
|
|
1036
|
+
title: 'Batch Operations',
|
|
1037
|
+
category: 'Scripting',
|
|
1038
|
+
description: 'Run scripts on multiple parts',
|
|
1039
|
+
content: `Apply scripts to many parts at once with batchExecute()`
|
|
1040
|
+
}
|
|
1041
|
+
];
|
|
1042
|
+
|
|
1043
|
+
export default {
|
|
1044
|
+
init,
|
|
1045
|
+
execute,
|
|
1046
|
+
saveScript,
|
|
1047
|
+
loadScript,
|
|
1048
|
+
deleteScript,
|
|
1049
|
+
listScripts,
|
|
1050
|
+
startRecording,
|
|
1051
|
+
stopRecording,
|
|
1052
|
+
recordAction,
|
|
1053
|
+
onEvent,
|
|
1054
|
+
batchExecute,
|
|
1055
|
+
getLastError,
|
|
1056
|
+
clearError,
|
|
1057
|
+
getCadHelper,
|
|
1058
|
+
setBreakpoint,
|
|
1059
|
+
removeBreakpoint,
|
|
1060
|
+
getBreakpoints,
|
|
1061
|
+
stepInto,
|
|
1062
|
+
stepOver,
|
|
1063
|
+
getDebugHistory,
|
|
1064
|
+
clearDebugHistory,
|
|
1065
|
+
setScriptParameters,
|
|
1066
|
+
getScriptParameters,
|
|
1067
|
+
createParameterDialog,
|
|
1068
|
+
getConsoleOutput,
|
|
1069
|
+
clearConsole,
|
|
1070
|
+
getExampleScripts,
|
|
1071
|
+
loadExampleScript,
|
|
1072
|
+
helpEntries
|
|
1073
|
+
};
|