cyclecad 2.0.1 → 2.1.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/IMPLEMENTATION_GUIDE.md +502 -0
- package/INTEGRATION-GUIDE.md +377 -0
- package/MODULES_PHASES_6_7.md +780 -0
- package/app/index.html +106 -2
- 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 +967 -0
- package/app/js/modules/assembly-module.js +47 -3
- package/app/js/modules/cam-module.js +1067 -0
- package/app/js/modules/collaboration-module.js +1102 -0
- package/app/js/modules/data-module.js +1656 -0
- package/app/js/modules/drawing-module.js +54 -8
- package/app/js/modules/formats-module.js +1173 -0
- package/app/js/modules/inspection-module.js +937 -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 +957 -0
- package/app/js/modules/rendering-module.js +1306 -0
- package/app/js/modules/scripting-module.js +955 -0
- package/app/js/modules/simulation-module.js +60 -3
- package/app/js/modules/sketch-module.js +1032 -90
- package/app/js/modules/step-module.js +47 -6
- package/app/js/modules/surface-module.js +728 -0
- package/app/js/modules/version-module.js +1410 -0
- package/app/js/modules/viewport-module.js +95 -8
- package/app/test-agent-v2.html +881 -1316
- 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/.github/scripts/cad-diff.js +0 -590
- package/.github/workflows/cad-diff.yml +0 -117
|
@@ -0,0 +1,955 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scripting-module.js
|
|
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
|
|
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: Simple `cad.*` wrappers for 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
|
+
*
|
|
17
|
+
* @module scripting-module
|
|
18
|
+
* @version 1.0.0
|
|
19
|
+
* @requires three
|
|
20
|
+
*
|
|
21
|
+
* @tutorial
|
|
22
|
+
* // Initialize scripting module
|
|
23
|
+
* const scripting = await import('./modules/scripting-module.js');
|
|
24
|
+
* scripting.init(viewport, kernel, containerEl);
|
|
25
|
+
*
|
|
26
|
+
* // Execute script code
|
|
27
|
+
* scripting.execute(`
|
|
28
|
+
* cad.createBox(100, 50, 30);
|
|
29
|
+
* cad.position(50, 0, 0);
|
|
30
|
+
* cad.fillet(5);
|
|
31
|
+
* cad.exportSTL('my_box.stl');
|
|
32
|
+
* `);
|
|
33
|
+
*
|
|
34
|
+
* // Save script to library
|
|
35
|
+
* scripting.saveScript('box_maker', `
|
|
36
|
+
* cad.createBox(100, 50, 30);
|
|
37
|
+
* cad.fillet(5);
|
|
38
|
+
* `);
|
|
39
|
+
*
|
|
40
|
+
* // Load and run script
|
|
41
|
+
* scripting.loadScript('box_maker').then(script => {
|
|
42
|
+
* scripting.execute(script.code);
|
|
43
|
+
* });
|
|
44
|
+
*
|
|
45
|
+
* // Record macro
|
|
46
|
+
* scripting.startRecording();
|
|
47
|
+
* // ... user performs actions in UI ...
|
|
48
|
+
* const macro = scripting.stopRecording();
|
|
49
|
+
* console.log(macro.code); // auto-generated script
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* // Parametric part generation
|
|
53
|
+
* const width = prompt('Width:', '100');
|
|
54
|
+
* const height = prompt('Height:', '50');
|
|
55
|
+
* const depth = prompt('Depth:', '30');
|
|
56
|
+
*
|
|
57
|
+
* cad.createBox(parseFloat(width), parseFloat(height), parseFloat(depth));
|
|
58
|
+
* cad.fillet(5);
|
|
59
|
+
* cad.material('steel');
|
|
60
|
+
* cad.color(0x8899aa);
|
|
61
|
+
*/
|
|
62
|
+
|
|
63
|
+
import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js';
|
|
64
|
+
|
|
65
|
+
// ============================================================================
|
|
66
|
+
// MODULE STATE
|
|
67
|
+
// ============================================================================
|
|
68
|
+
|
|
69
|
+
let scriptingState = {
|
|
70
|
+
viewport: null,
|
|
71
|
+
kernel: null,
|
|
72
|
+
containerEl: null,
|
|
73
|
+
scripts: new Map(),
|
|
74
|
+
currentScript: null,
|
|
75
|
+
isRecording: false,
|
|
76
|
+
recordedActions: [],
|
|
77
|
+
eventHooks: new Map(),
|
|
78
|
+
executionContext: null,
|
|
79
|
+
lastError: null
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// ============================================================================
|
|
83
|
+
// CAD API HELPER OBJECT
|
|
84
|
+
// ============================================================================
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* User-facing CAD helper object with shorthand methods
|
|
88
|
+
* Exposed as `cad` in script execution context
|
|
89
|
+
*/
|
|
90
|
+
const cadHelper = {
|
|
91
|
+
// === BASIC SHAPES ===
|
|
92
|
+
|
|
93
|
+
/** Create a rectangular box */
|
|
94
|
+
createBox: (width, height, depth) => {
|
|
95
|
+
return executeKernelCommand('ops.box', { width, height, depth });
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
/** Create a cylinder */
|
|
99
|
+
createCylinder: (radius, height, segments = 32) => {
|
|
100
|
+
return executeKernelCommand('ops.cylinder', { radius, height, segments });
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
/** Create a sphere */
|
|
104
|
+
createSphere: (radius, segments = 32) => {
|
|
105
|
+
return executeKernelCommand('ops.sphere', { radius, segments });
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
/** Create a cone */
|
|
109
|
+
createCone: (radius, height, segments = 32) => {
|
|
110
|
+
return executeKernelCommand('ops.cone', { radius, height, segments });
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
/** Create a torus */
|
|
114
|
+
createTorus: (majorRadius, minorRadius, segments = 32) => {
|
|
115
|
+
return executeKernelCommand('ops.torus', { majorRadius, minorRadius, segments });
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
// === POSITIONING ===
|
|
119
|
+
|
|
120
|
+
/** Set position */
|
|
121
|
+
position: (x, y, z) => {
|
|
122
|
+
const obj = getSelectedObject();
|
|
123
|
+
if (obj) obj.position.set(x, y, z);
|
|
124
|
+
return cadHelper;
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
/** Move by delta */
|
|
128
|
+
move: (dx, dy, dz) => {
|
|
129
|
+
const obj = getSelectedObject();
|
|
130
|
+
if (obj) obj.position.addScaledVector(new THREE.Vector3(dx, dy, dz), 1);
|
|
131
|
+
return cadHelper;
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
/** Set rotation (radians) */
|
|
135
|
+
rotate: (x, y, z) => {
|
|
136
|
+
const obj = getSelectedObject();
|
|
137
|
+
if (obj) obj.rotation.set(x, y, z);
|
|
138
|
+
return cadHelper;
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
/** Set scale */
|
|
142
|
+
scale: (sx, sy, sz) => {
|
|
143
|
+
const obj = getSelectedObject();
|
|
144
|
+
if (obj) obj.scale.set(sx || 1, sy || 1, sz || 1);
|
|
145
|
+
return cadHelper;
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
// === OPERATIONS ===
|
|
149
|
+
|
|
150
|
+
/** Fillet edges */
|
|
151
|
+
fillet: (radius, edges = null) => {
|
|
152
|
+
return executeKernelCommand('ops.fillet', { radius, edges });
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
/** Chamfer edges */
|
|
156
|
+
chamfer: (distance, edges = null) => {
|
|
157
|
+
return executeKernelCommand('ops.chamfer', { distance, edges });
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
/** Extrude selection */
|
|
161
|
+
extrude: (distance) => {
|
|
162
|
+
return executeKernelCommand('ops.extrude', { distance });
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
/** Create a hole */
|
|
166
|
+
hole: (diameter, depth) => {
|
|
167
|
+
return executeKernelCommand('ops.hole', { diameter, depth });
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
/** Boolean union */
|
|
171
|
+
union: (otherIds) => {
|
|
172
|
+
return executeKernelCommand('ops.boolean', { operation: 'union', targets: otherIds });
|
|
173
|
+
},
|
|
174
|
+
|
|
175
|
+
/** Boolean cut */
|
|
176
|
+
cut: (otherIds) => {
|
|
177
|
+
return executeKernelCommand('ops.boolean', { operation: 'cut', targets: otherIds });
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
/** Boolean intersection */
|
|
181
|
+
intersect: (otherIds) => {
|
|
182
|
+
return executeKernelCommand('ops.boolean', { operation: 'intersect', targets: otherIds });
|
|
183
|
+
},
|
|
184
|
+
|
|
185
|
+
/** Apply shell/hollow */
|
|
186
|
+
shell: (thickness) => {
|
|
187
|
+
return executeKernelCommand('ops.shell', { thickness });
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
/** Create rectangular pattern */
|
|
191
|
+
pattern: (countX, countY, spacingX, spacingY) => {
|
|
192
|
+
return executeKernelCommand('ops.pattern', { countX, countY, spacingX, spacingY });
|
|
193
|
+
},
|
|
194
|
+
|
|
195
|
+
/** Revolve profile around axis */
|
|
196
|
+
revolve: (angle, axis = 'Z') => {
|
|
197
|
+
return executeKernelCommand('ops.revolve', { angle, axis });
|
|
198
|
+
},
|
|
199
|
+
|
|
200
|
+
/** Sweep profile along path */
|
|
201
|
+
sweep: (profileId, pathId, options = {}) => {
|
|
202
|
+
return executeKernelCommand('ops.sweep', { profileId, pathId, ...options });
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
/** Loft between profiles */
|
|
206
|
+
loft: (profileIds) => {
|
|
207
|
+
return executeKernelCommand('ops.loft', { profileIds });
|
|
208
|
+
},
|
|
209
|
+
|
|
210
|
+
// === MATERIALS & APPEARANCE ===
|
|
211
|
+
|
|
212
|
+
/** Set material */
|
|
213
|
+
material: (name) => {
|
|
214
|
+
const obj = getSelectedObject();
|
|
215
|
+
if (obj && obj.material) {
|
|
216
|
+
const densities = {
|
|
217
|
+
'Steel': 7.85, 'Aluminum': 2.7, 'ABS': 1.05,
|
|
218
|
+
'Brass': 8.5, 'Titanium': 4.5, 'Nylon': 1.14
|
|
219
|
+
};
|
|
220
|
+
if (obj.userData) obj.userData.material = name;
|
|
221
|
+
}
|
|
222
|
+
return cadHelper;
|
|
223
|
+
},
|
|
224
|
+
|
|
225
|
+
/** Set color (hex) */
|
|
226
|
+
color: (hex) => {
|
|
227
|
+
const obj = getSelectedObject();
|
|
228
|
+
if (obj && obj.material) {
|
|
229
|
+
obj.material.color.setHex(hex);
|
|
230
|
+
}
|
|
231
|
+
return cadHelper;
|
|
232
|
+
},
|
|
233
|
+
|
|
234
|
+
/** Set opacity */
|
|
235
|
+
opacity: (value) => {
|
|
236
|
+
const obj = getSelectedObject();
|
|
237
|
+
if (obj && obj.material) {
|
|
238
|
+
obj.material.transparent = true;
|
|
239
|
+
obj.material.opacity = Math.max(0, Math.min(1, value));
|
|
240
|
+
}
|
|
241
|
+
return cadHelper;
|
|
242
|
+
},
|
|
243
|
+
|
|
244
|
+
// === INSPECTION ===
|
|
245
|
+
|
|
246
|
+
/** Get mass properties */
|
|
247
|
+
getMass: () => {
|
|
248
|
+
const obj = getSelectedObject();
|
|
249
|
+
if (!obj) return null;
|
|
250
|
+
return executeKernelCommand('inspect.massProperties', { meshId: obj });
|
|
251
|
+
},
|
|
252
|
+
|
|
253
|
+
/** Get bounding box */
|
|
254
|
+
getBounds: () => {
|
|
255
|
+
const obj = getSelectedObject();
|
|
256
|
+
if (!obj) return null;
|
|
257
|
+
obj.geometry?.computeBoundingBox();
|
|
258
|
+
return obj.geometry?.boundingBox || null;
|
|
259
|
+
},
|
|
260
|
+
|
|
261
|
+
/** Get volume */
|
|
262
|
+
getVolume: () => {
|
|
263
|
+
const obj = getSelectedObject();
|
|
264
|
+
if (!obj) return 0;
|
|
265
|
+
// Rough estimation from bounding box
|
|
266
|
+
const bbox = obj.geometry?.boundingBox;
|
|
267
|
+
if (!bbox) return 0;
|
|
268
|
+
const size = bbox.getSize(new THREE.Vector3());
|
|
269
|
+
return size.x * size.y * size.z;
|
|
270
|
+
},
|
|
271
|
+
|
|
272
|
+
// === EXPORT ===
|
|
273
|
+
|
|
274
|
+
/** Export to STL */
|
|
275
|
+
exportSTL: (filename) => {
|
|
276
|
+
return executeKernelCommand('export.stl', { filename });
|
|
277
|
+
},
|
|
278
|
+
|
|
279
|
+
/** Export to OBJ */
|
|
280
|
+
exportOBJ: (filename) => {
|
|
281
|
+
return executeKernelCommand('export.obj', { filename });
|
|
282
|
+
},
|
|
283
|
+
|
|
284
|
+
/** Export to glTF */
|
|
285
|
+
exportGLTF: (filename) => {
|
|
286
|
+
return executeKernelCommand('export.gltf', { filename });
|
|
287
|
+
},
|
|
288
|
+
|
|
289
|
+
// === SCENE ===
|
|
290
|
+
|
|
291
|
+
/** Get all objects */
|
|
292
|
+
getObjects: () => {
|
|
293
|
+
return scriptingState.viewport.scene.children.filter(obj => obj instanceof THREE.Mesh);
|
|
294
|
+
},
|
|
295
|
+
|
|
296
|
+
/** Select object by name */
|
|
297
|
+
select: (name) => {
|
|
298
|
+
const obj = scriptingState.viewport.scene.getObjectByName(name);
|
|
299
|
+
if (obj && scriptingState.kernel) {
|
|
300
|
+
scriptingState.kernel.selectMesh?.(obj);
|
|
301
|
+
}
|
|
302
|
+
return obj;
|
|
303
|
+
},
|
|
304
|
+
|
|
305
|
+
/** Hide object */
|
|
306
|
+
hide: (name) => {
|
|
307
|
+
const obj = scriptingState.viewport.scene.getObjectByName(name);
|
|
308
|
+
if (obj) obj.visible = false;
|
|
309
|
+
return cadHelper;
|
|
310
|
+
},
|
|
311
|
+
|
|
312
|
+
/** Show object */
|
|
313
|
+
show: (name) => {
|
|
314
|
+
const obj = scriptingState.viewport.scene.getObjectByName(name);
|
|
315
|
+
if (obj) obj.visible = true;
|
|
316
|
+
return cadHelper;
|
|
317
|
+
},
|
|
318
|
+
|
|
319
|
+
/** Delete object */
|
|
320
|
+
delete: (name) => {
|
|
321
|
+
const obj = scriptingState.viewport.scene.getObjectByName(name);
|
|
322
|
+
if (obj) scriptingState.viewport.scene.remove(obj);
|
|
323
|
+
return cadHelper;
|
|
324
|
+
},
|
|
325
|
+
|
|
326
|
+
// === UTILITY ===
|
|
327
|
+
|
|
328
|
+
/** Print to console and log */
|
|
329
|
+
print: (message) => {
|
|
330
|
+
console.log('[CAD Script]', message);
|
|
331
|
+
return cadHelper;
|
|
332
|
+
},
|
|
333
|
+
|
|
334
|
+
/** Get timestamp */
|
|
335
|
+
now: () => Date.now(),
|
|
336
|
+
|
|
337
|
+
/** Wait (milliseconds) */
|
|
338
|
+
wait: (ms) => new Promise(resolve => setTimeout(resolve, ms))
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
// ============================================================================
|
|
342
|
+
// PUBLIC API
|
|
343
|
+
// ============================================================================
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Initialize the scripting module
|
|
347
|
+
*
|
|
348
|
+
* @param {object} viewport - Three.js viewport
|
|
349
|
+
* @param {object} kernel - CAD kernel
|
|
350
|
+
* @param {HTMLElement} [containerEl] - Container for UI
|
|
351
|
+
*/
|
|
352
|
+
export function init(viewport, kernel, containerEl = null) {
|
|
353
|
+
scriptingState.viewport = viewport;
|
|
354
|
+
scriptingState.kernel = kernel;
|
|
355
|
+
scriptingState.containerEl = containerEl;
|
|
356
|
+
|
|
357
|
+
// Create execution context with cad helper
|
|
358
|
+
scriptingState.executionContext = { cad: cadHelper };
|
|
359
|
+
|
|
360
|
+
// Load saved scripts from localStorage
|
|
361
|
+
loadAllScripts();
|
|
362
|
+
|
|
363
|
+
console.log('[Scripting] Module initialized');
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Execute a script string
|
|
368
|
+
*
|
|
369
|
+
* @tutorial
|
|
370
|
+
* // Simple execution
|
|
371
|
+
* scripting.execute(`
|
|
372
|
+
* cad.createBox(100, 50, 30);
|
|
373
|
+
* cad.fillet(5);
|
|
374
|
+
* cad.exportSTL('box.stl');
|
|
375
|
+
* `);
|
|
376
|
+
*
|
|
377
|
+
* // With error handling
|
|
378
|
+
* try {
|
|
379
|
+
* await scripting.execute(code);
|
|
380
|
+
* } catch (error) {
|
|
381
|
+
* console.error('Script failed:', error.message);
|
|
382
|
+
* }
|
|
383
|
+
*
|
|
384
|
+
* @param {string} code - JavaScript code to execute
|
|
385
|
+
* @param {object} [context={}] - Additional variables to expose
|
|
386
|
+
* @returns {Promise<*>} Script result (if any)
|
|
387
|
+
*/
|
|
388
|
+
export async function execute(code, context = {}) {
|
|
389
|
+
try {
|
|
390
|
+
// Create function with cad context
|
|
391
|
+
const fullContext = { ...scriptingState.executionContext, ...context };
|
|
392
|
+
const contextKeys = Object.keys(fullContext);
|
|
393
|
+
const contextValues = contextKeys.map(k => fullContext[k]);
|
|
394
|
+
|
|
395
|
+
const fn = new Function(...contextKeys, code);
|
|
396
|
+
const result = await fn(...contextValues);
|
|
397
|
+
|
|
398
|
+
scriptingState.lastError = null;
|
|
399
|
+
console.log('[Scripting] Execution successful');
|
|
400
|
+
|
|
401
|
+
// Fire event
|
|
402
|
+
fireEvent('script_executed', { code, result });
|
|
403
|
+
|
|
404
|
+
return result;
|
|
405
|
+
} catch (error) {
|
|
406
|
+
scriptingState.lastError = error;
|
|
407
|
+
console.error('[Scripting] Execution error:', error.message);
|
|
408
|
+
|
|
409
|
+
// Fire event
|
|
410
|
+
fireEvent('script_error', { code, error });
|
|
411
|
+
|
|
412
|
+
throw error;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Save a script to library
|
|
418
|
+
*
|
|
419
|
+
* @tutorial
|
|
420
|
+
* scripting.saveScript('my_script', `
|
|
421
|
+
* cad.createBox(100, 50, 30);
|
|
422
|
+
* cad.fillet(5);
|
|
423
|
+
* `, {
|
|
424
|
+
* description: 'Creates a filleted box',
|
|
425
|
+
* tags: ['box', 'basic'],
|
|
426
|
+
* version: '1.0'
|
|
427
|
+
* });
|
|
428
|
+
*
|
|
429
|
+
* @param {string} name - Script name (unique identifier)
|
|
430
|
+
* @param {string} code - JavaScript code
|
|
431
|
+
* @param {object} [metadata={}] - Metadata (description, tags, version, etc)
|
|
432
|
+
* @returns {object} Saved script object
|
|
433
|
+
*/
|
|
434
|
+
export function saveScript(name, code, metadata = {}) {
|
|
435
|
+
const script = {
|
|
436
|
+
name,
|
|
437
|
+
code,
|
|
438
|
+
savedAt: new Date().toISOString(),
|
|
439
|
+
...metadata
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
scriptingState.scripts.set(name, script);
|
|
443
|
+
|
|
444
|
+
try {
|
|
445
|
+
localStorage.setItem(`cyclecad_script_${name}`, JSON.stringify(script));
|
|
446
|
+
console.log(`[Scripting] Saved script: ${name}`);
|
|
447
|
+
} catch (e) {
|
|
448
|
+
console.error('[Scripting] Save failed:', e);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
fireEvent('script_saved', { name, script });
|
|
452
|
+
return script;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Load a script from library
|
|
457
|
+
*
|
|
458
|
+
* @tutorial
|
|
459
|
+
* const script = scripting.loadScript('my_script');
|
|
460
|
+
* console.log(script.code);
|
|
461
|
+
* await scripting.execute(script.code);
|
|
462
|
+
*
|
|
463
|
+
* @param {string} name - Script name
|
|
464
|
+
* @returns {object|null} Script object or null if not found
|
|
465
|
+
*/
|
|
466
|
+
export function loadScript(name) {
|
|
467
|
+
let script = scriptingState.scripts.get(name);
|
|
468
|
+
|
|
469
|
+
if (!script) {
|
|
470
|
+
try {
|
|
471
|
+
const stored = localStorage.getItem(`cyclecad_script_${name}`);
|
|
472
|
+
if (stored) {
|
|
473
|
+
script = JSON.parse(stored);
|
|
474
|
+
scriptingState.scripts.set(name, script);
|
|
475
|
+
}
|
|
476
|
+
} catch (e) {
|
|
477
|
+
console.error('[Scripting] Load failed:', e);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (script) {
|
|
482
|
+
console.log(`[Scripting] Loaded script: ${name}`);
|
|
483
|
+
fireEvent('script_loaded', { name, script });
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return script || null;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Delete a script from library
|
|
491
|
+
*
|
|
492
|
+
* @param {string} name - Script name
|
|
493
|
+
* @returns {boolean} Success
|
|
494
|
+
*/
|
|
495
|
+
export function deleteScript(name) {
|
|
496
|
+
const deleted = scriptingState.scripts.delete(name);
|
|
497
|
+
if (deleted) {
|
|
498
|
+
localStorage.removeItem(`cyclecad_script_${name}`);
|
|
499
|
+
console.log(`[Scripting] Deleted script: ${name}`);
|
|
500
|
+
fireEvent('script_deleted', { name });
|
|
501
|
+
}
|
|
502
|
+
return deleted;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* List all saved scripts
|
|
507
|
+
*
|
|
508
|
+
* @tutorial
|
|
509
|
+
* const scripts = scripting.listScripts();
|
|
510
|
+
* scripts.forEach(script => {
|
|
511
|
+
* console.log(`${script.name}: ${script.description || 'No description'}`);
|
|
512
|
+
* });
|
|
513
|
+
*
|
|
514
|
+
* @param {string} [tag] - Optional filter by tag
|
|
515
|
+
* @returns {Array<object>} Array of script objects
|
|
516
|
+
*/
|
|
517
|
+
export function listScripts(tag = null) {
|
|
518
|
+
let scripts = Array.from(scriptingState.scripts.values());
|
|
519
|
+
|
|
520
|
+
if (tag) {
|
|
521
|
+
scripts = scripts.filter(s => (s.tags || []).includes(tag));
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
return scripts;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Start recording user actions as a macro
|
|
529
|
+
*
|
|
530
|
+
* @tutorial
|
|
531
|
+
* scripting.startRecording();
|
|
532
|
+
* // User performs actions: click, extrude, fillet, etc.
|
|
533
|
+
* const macro = scripting.stopRecording();
|
|
534
|
+
* scripting.saveScript('macro_1', macro.code, {
|
|
535
|
+
* description: 'Auto-generated macro'
|
|
536
|
+
* });
|
|
537
|
+
*/
|
|
538
|
+
export function startRecording() {
|
|
539
|
+
scriptingState.isRecording = true;
|
|
540
|
+
scriptingState.recordedActions = [];
|
|
541
|
+
|
|
542
|
+
console.log('[Scripting] Recording started');
|
|
543
|
+
fireEvent('recording_started', {});
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Stop recording and get generated macro
|
|
548
|
+
*
|
|
549
|
+
* @returns {object} {code, actions} Generated script
|
|
550
|
+
*/
|
|
551
|
+
export function stopRecording() {
|
|
552
|
+
scriptingState.isRecording = false;
|
|
553
|
+
|
|
554
|
+
// Generate code from recorded actions
|
|
555
|
+
const code = generateMacroCode(scriptingState.recordedActions);
|
|
556
|
+
|
|
557
|
+
const macro = {
|
|
558
|
+
code,
|
|
559
|
+
actions: scriptingState.recordedActions.slice(),
|
|
560
|
+
recordedAt: new Date().toISOString()
|
|
561
|
+
};
|
|
562
|
+
|
|
563
|
+
console.log('[Scripting] Recording stopped. Generated', scriptingState.recordedActions.length, 'actions');
|
|
564
|
+
fireEvent('recording_stopped', { macro });
|
|
565
|
+
|
|
566
|
+
return macro;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Record an action during macro recording
|
|
571
|
+
* @private
|
|
572
|
+
*/
|
|
573
|
+
export function recordAction(action, params) {
|
|
574
|
+
if (!scriptingState.isRecording) return;
|
|
575
|
+
|
|
576
|
+
scriptingState.recordedActions.push({
|
|
577
|
+
action,
|
|
578
|
+
params,
|
|
579
|
+
timestamp: Date.now()
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Register event hook
|
|
585
|
+
*
|
|
586
|
+
* @tutorial
|
|
587
|
+
* scripting.onEvent('script_executed', (data) => {
|
|
588
|
+
* console.log('Script ran:', data.code);
|
|
589
|
+
* });
|
|
590
|
+
*
|
|
591
|
+
* scripting.onEvent('geometry_changed', (data) => {
|
|
592
|
+
* console.log('Geometry updated');
|
|
593
|
+
* });
|
|
594
|
+
*
|
|
595
|
+
* @param {string} eventName - Event name
|
|
596
|
+
* @param {Function} callback - Handler function
|
|
597
|
+
*/
|
|
598
|
+
export function onEvent(eventName, callback) {
|
|
599
|
+
if (!scriptingState.eventHooks.has(eventName)) {
|
|
600
|
+
scriptingState.eventHooks.set(eventName, []);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
scriptingState.eventHooks.get(eventName).push(callback);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Run script on multiple selected objects
|
|
608
|
+
*
|
|
609
|
+
* @tutorial
|
|
610
|
+
* // Apply fillet to all selected parts
|
|
611
|
+
* scripting.batchExecute('selectedParts', `
|
|
612
|
+
* cad.fillet(5);
|
|
613
|
+
* `);
|
|
614
|
+
*
|
|
615
|
+
* @param {string|Array<object>} targets - 'selectedParts' or array of objects
|
|
616
|
+
* @param {string} code - Script code
|
|
617
|
+
* @returns {Promise<Array>} Array of results
|
|
618
|
+
*/
|
|
619
|
+
export async function batchExecute(targets, code) {
|
|
620
|
+
let objects = targets === 'selectedParts' ?
|
|
621
|
+
getSelectedObjects() : targets;
|
|
622
|
+
|
|
623
|
+
const results = [];
|
|
624
|
+
|
|
625
|
+
for (const obj of objects) {
|
|
626
|
+
try {
|
|
627
|
+
const result = await execute(code, { currentObject: obj });
|
|
628
|
+
results.push({ success: true, object: obj, result });
|
|
629
|
+
} catch (error) {
|
|
630
|
+
results.push({ success: false, object: obj, error });
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
console.log(`[Scripting] Batch executed on ${results.length} objects`);
|
|
635
|
+
return results;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Get last script error
|
|
640
|
+
*
|
|
641
|
+
* @returns {Error|null} Last error or null
|
|
642
|
+
*/
|
|
643
|
+
export function getLastError() {
|
|
644
|
+
return scriptingState.lastError;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* Clear last error
|
|
649
|
+
*/
|
|
650
|
+
export function clearError() {
|
|
651
|
+
scriptingState.lastError = null;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* Get cad helper object (for reference/testing)
|
|
656
|
+
*
|
|
657
|
+
* @returns {object} The cad helper
|
|
658
|
+
*/
|
|
659
|
+
export function getCadHelper() {
|
|
660
|
+
return cadHelper;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// ============================================================================
|
|
664
|
+
// INTERNAL FUNCTIONS
|
|
665
|
+
// ============================================================================
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* Execute a kernel command
|
|
669
|
+
* @private
|
|
670
|
+
*/
|
|
671
|
+
function executeKernelCommand(method, params) {
|
|
672
|
+
if (!scriptingState.kernel) return null;
|
|
673
|
+
|
|
674
|
+
// Dispatch to kernel if available
|
|
675
|
+
if (typeof scriptingState.kernel.execute === 'function') {
|
|
676
|
+
return scriptingState.kernel.execute({ method, params });
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
console.warn('[Scripting] Kernel command not available:', method);
|
|
680
|
+
return null;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Get currently selected object
|
|
685
|
+
* @private
|
|
686
|
+
*/
|
|
687
|
+
function getSelectedObject() {
|
|
688
|
+
return scriptingState.kernel?.selectedMesh ||
|
|
689
|
+
scriptingState.viewport?.scene?.children.find(c => c instanceof THREE.Mesh);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* Get all selected objects
|
|
694
|
+
* @private
|
|
695
|
+
*/
|
|
696
|
+
function getSelectedObjects() {
|
|
697
|
+
return scriptingState.kernel?.selectedMeshes ||
|
|
698
|
+
scriptingState.viewport?.scene?.children.filter(c => c instanceof THREE.Mesh) || [];
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* Fire event to all registered listeners
|
|
703
|
+
* @private
|
|
704
|
+
*/
|
|
705
|
+
function fireEvent(eventName, data) {
|
|
706
|
+
const listeners = scriptingState.eventHooks.get(eventName) || [];
|
|
707
|
+
listeners.forEach(callback => {
|
|
708
|
+
try {
|
|
709
|
+
callback(data);
|
|
710
|
+
} catch (e) {
|
|
711
|
+
console.error(`[Scripting] Event handler error for ${eventName}:`, e);
|
|
712
|
+
}
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* Generate code from recorded actions
|
|
718
|
+
* @private
|
|
719
|
+
*/
|
|
720
|
+
function generateMacroCode(actions) {
|
|
721
|
+
const lines = [
|
|
722
|
+
'// Auto-generated macro from recorded actions',
|
|
723
|
+
'// ' + new Date().toISOString(),
|
|
724
|
+
''
|
|
725
|
+
];
|
|
726
|
+
|
|
727
|
+
actions.forEach((action, i) => {
|
|
728
|
+
switch (action.action) {
|
|
729
|
+
case 'box':
|
|
730
|
+
lines.push(`cad.createBox(${action.params.w}, ${action.params.h}, ${action.params.d});`);
|
|
731
|
+
break;
|
|
732
|
+
case 'fillet':
|
|
733
|
+
lines.push(`cad.fillet(${action.params.radius});`);
|
|
734
|
+
break;
|
|
735
|
+
case 'hole':
|
|
736
|
+
lines.push(`cad.hole(${action.params.diameter}, ${action.params.depth});`);
|
|
737
|
+
break;
|
|
738
|
+
case 'extrude':
|
|
739
|
+
lines.push(`cad.extrude(${action.params.distance});`);
|
|
740
|
+
break;
|
|
741
|
+
case 'position':
|
|
742
|
+
lines.push(`cad.position(${action.params.x}, ${action.params.y}, ${action.params.z});`);
|
|
743
|
+
break;
|
|
744
|
+
case 'color':
|
|
745
|
+
lines.push(`cad.color(0x${action.params.hex.toString(16).padStart(6, '0')});`);
|
|
746
|
+
break;
|
|
747
|
+
case 'export':
|
|
748
|
+
lines.push(`cad.export${action.params.format}('${action.params.filename}');`);
|
|
749
|
+
break;
|
|
750
|
+
default:
|
|
751
|
+
lines.push(`// ${action.action}(${JSON.stringify(action.params).slice(0, 50)}...)`);
|
|
752
|
+
}
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
return lines.join('\n');
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* Load all scripts from localStorage
|
|
760
|
+
* @private
|
|
761
|
+
*/
|
|
762
|
+
function loadAllScripts() {
|
|
763
|
+
const keys = Object.keys(localStorage);
|
|
764
|
+
keys
|
|
765
|
+
.filter(k => k.startsWith('cyclecad_script_'))
|
|
766
|
+
.forEach(key => {
|
|
767
|
+
try {
|
|
768
|
+
const script = JSON.parse(localStorage.getItem(key));
|
|
769
|
+
const name = key.replace('cyclecad_script_', '');
|
|
770
|
+
scriptingState.scripts.set(name, script);
|
|
771
|
+
} catch (e) {
|
|
772
|
+
console.warn('[Scripting] Failed to load script from localStorage:', key);
|
|
773
|
+
}
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
console.log(`[Scripting] Loaded ${scriptingState.scripts.size} saved scripts`);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// ============================================================================
|
|
780
|
+
// HELP ENTRIES
|
|
781
|
+
// ============================================================================
|
|
782
|
+
|
|
783
|
+
export const helpEntries = [
|
|
784
|
+
{
|
|
785
|
+
id: 'scripting-basics',
|
|
786
|
+
title: 'Script Basics',
|
|
787
|
+
category: 'Scripting',
|
|
788
|
+
description: 'Write JavaScript to automate CAD operations',
|
|
789
|
+
shortcut: 'Ctrl+Shift+S',
|
|
790
|
+
content: `
|
|
791
|
+
cycleCAD scripting lets you automate design with JavaScript.
|
|
792
|
+
Access the cad helper object with shortcuts:
|
|
793
|
+
|
|
794
|
+
cad.createBox(w, h, d) - Create box
|
|
795
|
+
cad.createCylinder(r, h) - Create cylinder
|
|
796
|
+
cad.fillet(radius) - Fillet edges
|
|
797
|
+
cad.position(x, y, z) - Move object
|
|
798
|
+
cad.exportSTL(filename) - Export to STL
|
|
799
|
+
|
|
800
|
+
All methods chain: cad.createBox(100, 50, 30).fillet(5).color(0xff0000);
|
|
801
|
+
`
|
|
802
|
+
},
|
|
803
|
+
{
|
|
804
|
+
id: 'scripting-shapes',
|
|
805
|
+
title: 'Creating Shapes',
|
|
806
|
+
category: 'Scripting',
|
|
807
|
+
description: 'Programmatically create 3D geometry',
|
|
808
|
+
shortcut: 'Shift+Ctrl+N',
|
|
809
|
+
content: `
|
|
810
|
+
Create basic shapes with cad helper:
|
|
811
|
+
- createBox(w, h, d) - Rectangular solid
|
|
812
|
+
- createCylinder(r, h) - Cylinder
|
|
813
|
+
- createSphere(r) - Sphere
|
|
814
|
+
- createCone(r, h) - Cone
|
|
815
|
+
- createTorus(majorR, minorR) - Torus
|
|
816
|
+
|
|
817
|
+
Example:
|
|
818
|
+
cad.createBox(100, 50, 30);
|
|
819
|
+
cad.createCylinder(25, 100);
|
|
820
|
+
`
|
|
821
|
+
},
|
|
822
|
+
{
|
|
823
|
+
id: 'scripting-operations',
|
|
824
|
+
title: 'Geometry Operations',
|
|
825
|
+
category: 'Scripting',
|
|
826
|
+
description: 'Modify shapes with fillet, hole, boolean, etc.',
|
|
827
|
+
shortcut: 'Shift+Ctrl+O',
|
|
828
|
+
content: `
|
|
829
|
+
Modify geometry with operations:
|
|
830
|
+
- fillet(radius) - Round edges
|
|
831
|
+
- chamfer(distance) - Bevel edges
|
|
832
|
+
- hole(diameter, depth) - Create hole
|
|
833
|
+
- extrude(distance) - Extrude selection
|
|
834
|
+
- union/cut/intersect(otherIds) - Boolean ops
|
|
835
|
+
- shell(thickness) - Create hollow
|
|
836
|
+
- pattern(countX, countY, spaceX, spaceY) - Array
|
|
837
|
+
|
|
838
|
+
Example:
|
|
839
|
+
cad.createBox(100, 50, 30)
|
|
840
|
+
.fillet(5)
|
|
841
|
+
.hole(10, 15)
|
|
842
|
+
.color(0x8899aa);
|
|
843
|
+
`
|
|
844
|
+
},
|
|
845
|
+
{
|
|
846
|
+
id: 'scripting-library',
|
|
847
|
+
title: 'Script Library',
|
|
848
|
+
category: 'Scripting',
|
|
849
|
+
description: 'Save and load scripts for reuse',
|
|
850
|
+
shortcut: 'Shift+Ctrl+L',
|
|
851
|
+
content: `
|
|
852
|
+
Save scripts to library:
|
|
853
|
+
scripting.saveScript('my_script', code, {
|
|
854
|
+
description: 'Creates a filleted box',
|
|
855
|
+
tags: ['box', 'basic']
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
Load and run:
|
|
859
|
+
const script = scripting.loadScript('my_script');
|
|
860
|
+
scripting.execute(script.code);
|
|
861
|
+
|
|
862
|
+
List all scripts:
|
|
863
|
+
scripting.listScripts().forEach(s => console.log(s.name));
|
|
864
|
+
`
|
|
865
|
+
},
|
|
866
|
+
{
|
|
867
|
+
id: 'scripting-macros',
|
|
868
|
+
title: 'Macro Recording',
|
|
869
|
+
category: 'Scripting',
|
|
870
|
+
description: 'Auto-record user actions as scripts',
|
|
871
|
+
shortcut: 'Shift+Ctrl+R',
|
|
872
|
+
content: `
|
|
873
|
+
Record user actions as replayable macros:
|
|
874
|
+
1. Click "Record"
|
|
875
|
+
2. Perform actions (create box, fillet, etc)
|
|
876
|
+
3. Click "Stop"
|
|
877
|
+
4. Generated script appears
|
|
878
|
+
5. Save to library for later use
|
|
879
|
+
|
|
880
|
+
Useful for repetitive design tasks.
|
|
881
|
+
`
|
|
882
|
+
},
|
|
883
|
+
{
|
|
884
|
+
id: 'scripting-batch',
|
|
885
|
+
title: 'Batch Operations',
|
|
886
|
+
category: 'Scripting',
|
|
887
|
+
description: 'Run scripts on multiple parts',
|
|
888
|
+
shortcut: 'Shift+Ctrl+B',
|
|
889
|
+
content: `
|
|
890
|
+
Apply operations to many parts at once:
|
|
891
|
+
scripting.batchExecute('selectedParts', \`
|
|
892
|
+
cad.fillet(5);
|
|
893
|
+
cad.color(0x8899aa);
|
|
894
|
+
\`);
|
|
895
|
+
|
|
896
|
+
The script runs for each selected part.
|
|
897
|
+
Useful for applying material/color to assemblies.
|
|
898
|
+
`
|
|
899
|
+
},
|
|
900
|
+
{
|
|
901
|
+
id: 'scripting-export',
|
|
902
|
+
title: 'Export from Scripts',
|
|
903
|
+
category: 'Scripting',
|
|
904
|
+
description: 'Save work programmatically',
|
|
905
|
+
shortcut: 'Shift+Ctrl+E',
|
|
906
|
+
content: `
|
|
907
|
+
Export from scripts:
|
|
908
|
+
cad.exportSTL('part.stl');
|
|
909
|
+
cad.exportOBJ('part.obj');
|
|
910
|
+
cad.exportGLTF('model.gltf');
|
|
911
|
+
|
|
912
|
+
Automate file generation for batches of parts.
|
|
913
|
+
`
|
|
914
|
+
},
|
|
915
|
+
{
|
|
916
|
+
id: 'scripting-events',
|
|
917
|
+
title: 'Event Hooks',
|
|
918
|
+
category: 'Scripting',
|
|
919
|
+
description: 'Subscribe to kernel events',
|
|
920
|
+
shortcut: 'Shift+Ctrl+V',
|
|
921
|
+
content: `
|
|
922
|
+
Listen for kernel events:
|
|
923
|
+
scripting.onEvent('script_executed', (data) => {
|
|
924
|
+
console.log('Script ran');
|
|
925
|
+
});
|
|
926
|
+
|
|
927
|
+
scripting.onEvent('geometry_changed', (data) => {
|
|
928
|
+
console.log('Model updated');
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
Available events:
|
|
932
|
+
- script_executed, script_error
|
|
933
|
+
- script_saved, script_loaded
|
|
934
|
+
- recording_started, recording_stopped
|
|
935
|
+
`
|
|
936
|
+
}
|
|
937
|
+
];
|
|
938
|
+
|
|
939
|
+
export default {
|
|
940
|
+
init,
|
|
941
|
+
execute,
|
|
942
|
+
saveScript,
|
|
943
|
+
loadScript,
|
|
944
|
+
deleteScript,
|
|
945
|
+
listScripts,
|
|
946
|
+
startRecording,
|
|
947
|
+
stopRecording,
|
|
948
|
+
recordAction,
|
|
949
|
+
onEvent,
|
|
950
|
+
batchExecute,
|
|
951
|
+
getLastError,
|
|
952
|
+
clearError,
|
|
953
|
+
getCadHelper,
|
|
954
|
+
helpEntries
|
|
955
|
+
};
|