cyclecad 2.0.0 → 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.
Files changed (33) hide show
  1. package/IMPLEMENTATION_GUIDE.md +502 -0
  2. package/INTEGRATION-GUIDE.md +377 -0
  3. package/MODULES_PHASES_6_7.md +780 -0
  4. package/app/index.html +106 -2
  5. package/app/js/brep-kernel.js +1353 -455
  6. package/app/js/help-module.js +1437 -0
  7. package/app/js/kernel.js +364 -40
  8. package/app/js/modules/animation-module.js +967 -0
  9. package/app/js/modules/assembly-module.js +47 -3
  10. package/app/js/modules/cam-module.js +1067 -0
  11. package/app/js/modules/collaboration-module.js +1102 -0
  12. package/app/js/modules/data-module.js +1656 -0
  13. package/app/js/modules/drawing-module.js +54 -8
  14. package/app/js/modules/formats-module.js +1173 -0
  15. package/app/js/modules/inspection-module.js +937 -0
  16. package/app/js/modules/mesh-module.js +968 -0
  17. package/app/js/modules/operations-module.js +40 -7
  18. package/app/js/modules/plugin-module.js +957 -0
  19. package/app/js/modules/rendering-module.js +1306 -0
  20. package/app/js/modules/scripting-module.js +955 -0
  21. package/app/js/modules/simulation-module.js +60 -3
  22. package/app/js/modules/sketch-module.js +1032 -90
  23. package/app/js/modules/step-module.js +47 -6
  24. package/app/js/modules/surface-module.js +728 -0
  25. package/app/js/modules/version-module.js +1410 -0
  26. package/app/js/modules/viewport-module.js +95 -8
  27. package/app/test-agent-v2.html +881 -1316
  28. package/docs/ARCHITECTURE.html +838 -1408
  29. package/docs/DEVELOPER-GUIDE.md +1504 -0
  30. package/docs/TUTORIAL.md +740 -0
  31. package/package.json +1 -1
  32. package/.github/scripts/cad-diff.js +0 -590
  33. 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
+ };