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.
Files changed (48) hide show
  1. package/DELIVERABLES.txt +296 -445
  2. package/ENHANCEMENT_COMPLETION_REPORT.md +383 -0
  3. package/ENHANCEMENT_SUMMARY.txt +308 -0
  4. package/FEATURE_INVENTORY.md +235 -0
  5. package/FUSION360_FEATURES_SUMMARY.md +452 -0
  6. package/FUSION360_PARITY_ENHANCEMENTS.md +461 -0
  7. package/FUSION360_PARITY_SUMMARY.md +520 -0
  8. package/FUSION360_QUICK_REFERENCE.md +351 -0
  9. package/IMPLEMENTATION_GUIDE.md +502 -0
  10. package/INTEGRATION-GUIDE.md +377 -0
  11. package/MODULES_PHASES_6_7.md +780 -0
  12. package/MODULE_API_REFERENCE.md +712 -0
  13. package/MODULE_INVENTORY.txt +264 -0
  14. package/app/index.html +1345 -4930
  15. package/app/js/app.js +1312 -514
  16. package/app/js/brep-kernel.js +1353 -455
  17. package/app/js/help-module.js +1437 -0
  18. package/app/js/kernel.js +364 -40
  19. package/app/js/modules/animation-module.js +1461 -0
  20. package/app/js/modules/assembly-module.js +47 -3
  21. package/app/js/modules/cam-module.js +1572 -0
  22. package/app/js/modules/collaboration-module.js +1615 -0
  23. package/app/js/modules/constraint-module.js +1266 -0
  24. package/app/js/modules/data-module.js +1054 -0
  25. package/app/js/modules/drawing-module.js +54 -8
  26. package/app/js/modules/formats-module.js +873 -0
  27. package/app/js/modules/inspection-module.js +1330 -0
  28. package/app/js/modules/mesh-module-enhanced.js +880 -0
  29. package/app/js/modules/mesh-module.js +968 -0
  30. package/app/js/modules/operations-module.js +40 -7
  31. package/app/js/modules/plugin-module.js +1554 -0
  32. package/app/js/modules/rendering-module.js +1766 -0
  33. package/app/js/modules/scripting-module.js +1073 -0
  34. package/app/js/modules/simulation-module.js +60 -3
  35. package/app/js/modules/sketch-module.js +2029 -91
  36. package/app/js/modules/step-module.js +47 -6
  37. package/app/js/modules/surface-module.js +1040 -0
  38. package/app/js/modules/version-module.js +1830 -0
  39. package/app/js/modules/viewport-module.js +95 -8
  40. package/app/test-agent-v2.html +881 -1316
  41. package/cycleCAD-Architecture-v2.pptx +0 -0
  42. package/docs/ARCHITECTURE.html +838 -1408
  43. package/docs/DEVELOPER-GUIDE.md +1504 -0
  44. package/docs/TUTORIAL.md +740 -0
  45. package/package.json +1 -1
  46. package/~$cycleCAD-Architecture-v2.pptx +0 -0
  47. package/.github/scripts/cad-diff.js +0 -590
  48. package/.github/workflows/cad-diff.yml +0 -117
@@ -0,0 +1,1554 @@
1
+ /**
2
+ * @file plugin-module.js
3
+ * @version 1.0.0
4
+ * @license MIT
5
+ *
6
+ * @description
7
+ * Plugin system for extending cycleCAD with custom features.
8
+ * Load JavaScript plugins from URLs, the marketplace, or local files.
9
+ * Plugins receive sandboxed access to the kernel API through window.cycleCAD.kernel.
10
+ *
11
+ * Features:
12
+ * - Load/unload plugins dynamically
13
+ * - Sandboxed execution (no direct DOM access)
14
+ * - Plugin Manager UI panel
15
+ * - Dependency resolution
16
+ * - Marketplace integration
17
+ * - Hot reload capability
18
+ * - Permission system (plugins declare required APIs)
19
+ * - Plugin templates for quick development
20
+ *
21
+ * @tutorial Creating a Plugin
22
+ * 1. Create a JavaScript file that exports a module definition:
23
+ * ```javascript
24
+ * export default {
25
+ * id: 'my-plugin',
26
+ * name: 'My Plugin',
27
+ * version: '1.0.0',
28
+ * author: 'Your Name',
29
+ * permissions: ['viewport.addMesh', 'shape.extrude'],
30
+ * async activate(kernel) {
31
+ * // Register custom commands, buttons, shortcuts
32
+ * kernel.registerCommand('mycommand.execute', async (params) => {
33
+ * // Your code here
34
+ * });
35
+ * },
36
+ * async deactivate() {
37
+ * // Cleanup code
38
+ * }
39
+ * };
40
+ * ```
41
+ * 2. Install the plugin:
42
+ * ```javascript
43
+ * window.cycleCAD.kernel.exec('plugin.install', { url: 'https://my-domain.com/my-plugin.js' });
44
+ * ```
45
+ * 3. The plugin appears in Plugin Manager panel under View → Plugin Manager
46
+ * 4. Toggle the switch to enable/disable
47
+ * 5. Use your custom commands via the kernel API
48
+ *
49
+ * @example
50
+ * // Example: Gear Generator Plugin
51
+ * // File: gear-plugin.js
52
+ * ```javascript
53
+ * export default {
54
+ * id: 'gear-generator',
55
+ * name: 'Gear Generator',
56
+ * version: '2.1.0',
57
+ * author: 'Mechanical Plugins Inc',
58
+ * description: 'Generate involute gears with customizable parameters',
59
+ * permissions: ['shape.create', 'viewport.addMesh', 'tree.addFeature'],
60
+ * dependencies: [],
61
+ * async activate(kernel) {
62
+ * // Register the gear creation command
63
+ * kernel.registerCommand('gear.create', async (params) => {
64
+ * const { teeth, module, width, pressure_angle = 20, bore = 0 } = params;
65
+ * const geometry = generateInvoluteGear(teeth, module, width, pressure_angle, bore);
66
+ * return kernel.exec('viewport.addMesh', {
67
+ * geometry,
68
+ * name: `Gear ${teeth}T`,
69
+ * category: 'plugin'
70
+ * });
71
+ * });
72
+ *
73
+ * // Register UI button
74
+ * kernel.registerButton({
75
+ * id: 'gear-btn',
76
+ * label: 'Create Gear',
77
+ * icon: 'gear-icon.svg',
78
+ * category: 'Create',
79
+ * onClick: () => {
80
+ * showGearDialog(kernel);
81
+ * }
82
+ * });
83
+ * },
84
+ * async deactivate() {
85
+ * // Clean up resources
86
+ * }
87
+ * };
88
+ * ```
89
+ *
90
+ * @see {@link https://cyclecad.com/docs/plugin-api|Plugin API Documentation}
91
+ */
92
+
93
+ export default {
94
+ id: 'plugin-system',
95
+ name: 'Plugin Manager',
96
+ version: '1.0.0',
97
+ author: 'cycleCAD Team',
98
+
99
+ /**
100
+ * @type {Map<string, Object>} Installed plugins with metadata
101
+ * @private
102
+ */
103
+ _plugins: new Map(),
104
+
105
+ /**
106
+ * @type {Map<string, Function>} Registered custom commands from plugins
107
+ * @private
108
+ */
109
+ _customCommands: new Map(),
110
+
111
+ /**
112
+ * @type {Object} Plugin permissions whitelist
113
+ * @private
114
+ */
115
+ _permissionsWhitelist: {
116
+ 'shape.create': true,
117
+ 'shape.extrude': true,
118
+ 'shape.revolve': true,
119
+ 'shape.fillet': true,
120
+ 'shape.chamfer': true,
121
+ 'shape.boolean': true,
122
+ 'shape.pattern': true,
123
+ 'viewport.addMesh': true,
124
+ 'viewport.removeMesh': true,
125
+ 'viewport.setColor': true,
126
+ 'viewport.setMaterial': true,
127
+ 'viewport.fitToSelection': true,
128
+ 'tree.addFeature': true,
129
+ 'tree.removeFeature': true,
130
+ 'tree.renameFeature': true,
131
+ 'export.stl': true,
132
+ 'export.obj': true,
133
+ 'export.gltf': true
134
+ },
135
+
136
+ /**
137
+ * ============================================================================
138
+ * INITIALIZATION
139
+ * ============================================================================
140
+ */
141
+
142
+ /**
143
+ * Initialize the plugin system.
144
+ * @async
145
+ * @returns {Promise<void>}
146
+ */
147
+ async init() {
148
+ console.log('[Plugin] System initialized');
149
+ this._loadStoredPlugins();
150
+ this._registerBuiltinTemplates();
151
+ window.cycleCAD.kernel._pluginSystem = this;
152
+ },
153
+
154
+ /**
155
+ * ============================================================================
156
+ * PLUGIN LIFECYCLE MANAGEMENT
157
+ * ============================================================================
158
+ */
159
+
160
+ /**
161
+ * Install a plugin from a URL or file.
162
+ * @async
163
+ * @param {string} url - URL to plugin JS file
164
+ * @param {Object} options - Installation options
165
+ * @param {boolean} options.autoEnable - Enable plugin after install (default: true)
166
+ * @param {string} options.fromMarketplace - Plugin ID from marketplace (optional)
167
+ * @returns {Promise<Object>} Plugin metadata
168
+ * @throws {Error} If plugin validation fails
169
+ *
170
+ * @example
171
+ * const plugin = await kernel.exec('plugin.install', {
172
+ * url: 'https://github.com/user/plugin/plugin.js'
173
+ * });
174
+ */
175
+ async install(url, options = {}) {
176
+ try {
177
+ // Load the plugin module
178
+ const response = await fetch(url);
179
+ if (!response.ok) throw new Error(`HTTP ${response.status}: ${url}`);
180
+
181
+ const moduleText = await response.text();
182
+ const module = await import(`data:text/javascript,${encodeURIComponent(moduleText)}`);
183
+ const pluginDef = module.default;
184
+
185
+ // Validate plugin definition
186
+ this._validatePlugin(pluginDef);
187
+
188
+ // Check for conflicts
189
+ if (this._plugins.has(pluginDef.id)) {
190
+ throw new Error(`Plugin '${pluginDef.id}' already installed`);
191
+ }
192
+
193
+ // Store plugin metadata
194
+ const pluginData = {
195
+ ...pluginDef,
196
+ url,
197
+ enabled: options.autoEnable !== false,
198
+ installedAt: new Date().toISOString(),
199
+ updateAvailable: false,
200
+ fromMarketplace: options.fromMarketplace || null,
201
+ moduleInstance: module
202
+ };
203
+
204
+ this._plugins.set(pluginDef.id, pluginData);
205
+
206
+ // Auto-enable if requested
207
+ if (pluginData.enabled) {
208
+ await this.enable(pluginDef.id);
209
+ }
210
+
211
+ // Persist to localStorage
212
+ this._savePlugins();
213
+
214
+ console.log(`[Plugin] Installed: ${pluginDef.name} (${pluginDef.id})`);
215
+ return pluginData;
216
+ } catch (error) {
217
+ console.error('[Plugin] Install failed:', error);
218
+ throw error;
219
+ }
220
+ },
221
+
222
+ /**
223
+ * Enable a previously disabled plugin.
224
+ * @async
225
+ * @param {string} pluginId - Plugin ID
226
+ * @returns {Promise<void>}
227
+ *
228
+ * @example
229
+ * await kernel.exec('plugin.enable', { pluginId: 'my-plugin' });
230
+ */
231
+ async enable(pluginId) {
232
+ const plugin = this._plugins.get(pluginId);
233
+ if (!plugin) throw new Error(`Plugin '${pluginId}' not found`);
234
+ if (plugin.enabled) return;
235
+
236
+ try {
237
+ // Create sandbox kernel API
238
+ const kernelAPI = this._createSandboxAPI(pluginId);
239
+
240
+ // Call plugin activate hook
241
+ if (plugin.activate) {
242
+ await plugin.activate(kernelAPI);
243
+ }
244
+
245
+ plugin.enabled = true;
246
+ this._savePlugins();
247
+ console.log(`[Plugin] Enabled: ${plugin.name}`);
248
+ } catch (error) {
249
+ console.error(`[Plugin] Enable failed (${pluginId}):`, error);
250
+ throw error;
251
+ }
252
+ },
253
+
254
+ /**
255
+ * Disable an active plugin.
256
+ * @async
257
+ * @param {string} pluginId - Plugin ID
258
+ * @returns {Promise<void>}
259
+ */
260
+ async disable(pluginId) {
261
+ const plugin = this._plugins.get(pluginId);
262
+ if (!plugin) throw new Error(`Plugin '${pluginId}' not found`);
263
+ if (!plugin.enabled) return;
264
+
265
+ try {
266
+ // Call plugin deactivate hook
267
+ if (plugin.deactivate) {
268
+ await plugin.deactivate();
269
+ }
270
+
271
+ // Remove registered commands
272
+ Array.from(this._customCommands.entries()).forEach(([cmd, meta]) => {
273
+ if (meta.pluginId === pluginId) {
274
+ this._customCommands.delete(cmd);
275
+ }
276
+ });
277
+
278
+ plugin.enabled = false;
279
+ this._savePlugins();
280
+ console.log(`[Plugin] Disabled: ${plugin.name}`);
281
+ } catch (error) {
282
+ console.error(`[Plugin] Disable failed (${pluginId}):`, error);
283
+ throw error;
284
+ }
285
+ },
286
+
287
+ /**
288
+ * Uninstall a plugin completely.
289
+ * @async
290
+ * @param {string} pluginId - Plugin ID
291
+ * @returns {Promise<void>}
292
+ */
293
+ async uninstall(pluginId) {
294
+ const plugin = this._plugins.get(pluginId);
295
+ if (!plugin) throw new Error(`Plugin '${pluginId}' not found`);
296
+
297
+ if (plugin.enabled) {
298
+ await this.disable(pluginId);
299
+ }
300
+
301
+ this._plugins.delete(pluginId);
302
+ this._savePlugins();
303
+ console.log(`[Plugin] Uninstalled: ${plugin.name}`);
304
+ },
305
+
306
+ /**
307
+ * Update a plugin to the latest version.
308
+ * @async
309
+ * @param {string} pluginId - Plugin ID
310
+ * @returns {Promise<Object>} Updated plugin metadata
311
+ */
312
+ async update(pluginId) {
313
+ const plugin = this._plugins.get(pluginId);
314
+ if (!plugin) throw new Error(`Plugin '${pluginId}' not found`);
315
+ if (!plugin.url) throw new Error('Cannot update plugins without URL');
316
+
317
+ const wasEnabled = plugin.enabled;
318
+ if (wasEnabled) await this.disable(pluginId);
319
+ await this.uninstall(pluginId);
320
+ const updated = await this.install(plugin.url, { autoEnable: wasEnabled });
321
+ console.log(`[Plugin] Updated: ${plugin.name} → v${updated.version}`);
322
+ return updated;
323
+ },
324
+
325
+ /**
326
+ * ============================================================================
327
+ * PLUGIN COMMANDS & API MANAGEMENT
328
+ * ============================================================================
329
+ */
330
+
331
+ /**
332
+ * Register a custom command from a plugin.
333
+ * @param {string} commandName - Full command name (e.g., 'gear.create')
334
+ * @param {Function} handler - Async function(params) => result
335
+ * @param {string} pluginId - ID of plugin registering command
336
+ * @returns {void}
337
+ * @private
338
+ */
339
+ _registerCommand(commandName, handler, pluginId) {
340
+ if (this._customCommands.has(commandName)) {
341
+ console.warn(`[Plugin] Command '${commandName}' already registered, overwriting`);
342
+ }
343
+ this._customCommands.set(commandName, { handler, pluginId });
344
+ },
345
+
346
+ /**
347
+ * Execute a plugin command.
348
+ * @async
349
+ * @param {string} commandName - Full command name
350
+ * @param {Object} params - Command parameters
351
+ * @returns {Promise<any>} Command result
352
+ * @throws {Error} If command not found
353
+ */
354
+ async executeCustomCommand(commandName, params) {
355
+ const cmd = this._customCommands.get(commandName);
356
+ if (!cmd) throw new Error(`Plugin command '${commandName}' not registered`);
357
+ return cmd.handler(params);
358
+ },
359
+
360
+ /**
361
+ * Create a sandboxed kernel API for a plugin.
362
+ * Restricts plugins to whitelisted commands only.
363
+ * @param {string} pluginId - Plugin ID
364
+ * @returns {Object} Kernel API proxy
365
+ * @private
366
+ */
367
+ _createSandboxAPI(pluginId) {
368
+ const self = this;
369
+ return {
370
+ exec: async (commandName, params = {}) => {
371
+ // Check whitelist
372
+ if (!self._permissionsWhitelist[commandName]) {
373
+ throw new Error(`Plugin '${pluginId}' lacks permission for '${commandName}'`);
374
+ }
375
+ // Route to main kernel
376
+ return window.cycleCAD.kernel.exec(commandName, params);
377
+ },
378
+
379
+ registerCommand: (name, handler) => {
380
+ self._registerCommand(name, handler, pluginId);
381
+ },
382
+
383
+ registerButton: (config) => {
384
+ // Plugins can register custom UI buttons
385
+ window.cycleCAD.kernel._createPluginButton(pluginId, config);
386
+ },
387
+
388
+ on: (event, callback) => {
389
+ // Event subscription
390
+ window.cycleCAD.kernel._addEventListener(pluginId, event, callback);
391
+ },
392
+
393
+ off: (event, callback) => {
394
+ // Event unsubscription
395
+ window.cycleCAD.kernel._removeEventListener(pluginId, event, callback);
396
+ }
397
+ };
398
+ },
399
+
400
+ /**
401
+ * ============================================================================
402
+ * VALIDATION & TEMPLATES
403
+ * ============================================================================
404
+ */
405
+
406
+ /**
407
+ * Validate plugin definition structure.
408
+ * @param {Object} pluginDef - Plugin definition
409
+ * @throws {Error} If validation fails
410
+ * @private
411
+ */
412
+ _validatePlugin(pluginDef) {
413
+ if (!pluginDef.id || typeof pluginDef.id !== 'string') {
414
+ throw new Error('Plugin must define string id');
415
+ }
416
+ if (!pluginDef.name || typeof pluginDef.name !== 'string') {
417
+ throw new Error('Plugin must define string name');
418
+ }
419
+ if (!pluginDef.version || typeof pluginDef.version !== 'string') {
420
+ throw new Error('Plugin must define string version');
421
+ }
422
+ if (pluginDef.permissions && !Array.isArray(pluginDef.permissions)) {
423
+ throw new Error('Plugin permissions must be an array');
424
+ }
425
+ if (pluginDef.activate && typeof pluginDef.activate !== 'function') {
426
+ throw new Error('Plugin activate must be a function');
427
+ }
428
+ },
429
+
430
+ /**
431
+ * Create a template for a new plugin.
432
+ * @param {string} type - Template type ('basic', 'geometry', 'tool', 'material')
433
+ * @returns {string} Plugin template code
434
+ *
435
+ * @example
436
+ * const template = await kernel.exec('plugin.createTemplate', { type: 'geometry' });
437
+ * // Returns boilerplate plugin code
438
+ */
439
+ createTemplate(type) {
440
+ const templates = {
441
+ basic: `/**
442
+ * Basic Plugin Template
443
+ * Replace 'my-plugin' with your plugin ID
444
+ */
445
+ export default {
446
+ id: 'my-plugin',
447
+ name: 'My Plugin',
448
+ version: '1.0.0',
449
+ author: 'Your Name',
450
+ description: 'What does this plugin do?',
451
+ permissions: ['viewport.addMesh'],
452
+ async activate(kernel) {
453
+ console.log('Plugin activated');
454
+ },
455
+ async deactivate() {
456
+ console.log('Plugin deactivated');
457
+ }
458
+ };`,
459
+
460
+ geometry: `/**
461
+ * Geometry Generator Plugin Template
462
+ */
463
+ export default {
464
+ id: 'geom-generator',
465
+ name: 'Geometry Generator',
466
+ version: '1.0.0',
467
+ permissions: ['shape.create', 'viewport.addMesh'],
468
+ async activate(kernel) {
469
+ kernel.registerCommand('geom.create', async (params) => {
470
+ const { type, size } = params;
471
+ // Generate geometry here
472
+ const geometry = createGeometry(type, size);
473
+ return kernel.exec('viewport.addMesh', { geometry, name: 'Generated' });
474
+ });
475
+ }
476
+ };`,
477
+
478
+ tool: `/**
479
+ * Analysis/Tool Plugin Template
480
+ */
481
+ export default {
482
+ id: 'analysis-tool',
483
+ name: 'Analysis Tool',
484
+ version: '1.0.0',
485
+ permissions: ['shape.create'],
486
+ async activate(kernel) {
487
+ kernel.registerButton({
488
+ id: 'tool-btn',
489
+ label: 'Run Analysis',
490
+ category: 'Analyze',
491
+ onClick: async () => {
492
+ const result = await analyzeCurrentModel();
493
+ console.log('Analysis result:', result);
494
+ }
495
+ });
496
+ }
497
+ };`,
498
+
499
+ material: `/**
500
+ * Material Library Plugin Template
501
+ */
502
+ export default {
503
+ id: 'materials-library',
504
+ name: 'Materials Library',
505
+ version: '1.0.0',
506
+ permissions: ['viewport.setMaterial'],
507
+ async activate(kernel) {
508
+ kernel.registerCommand('material.apply', async (params) => {
509
+ const { bodyId, materialName } = params;
510
+ const props = getMaterialProperties(materialName);
511
+ return kernel.exec('viewport.setMaterial', { bodyId, ...props });
512
+ });
513
+ }
514
+ };`
515
+ };
516
+
517
+ return templates[type] || templates.basic;
518
+ },
519
+
520
+ /**
521
+ * Register built-in plugin templates.
522
+ * @private
523
+ */
524
+ _registerBuiltinTemplates() {
525
+ const templates = {
526
+ 'template-basic': {
527
+ id: 'template-basic',
528
+ name: 'Basic Plugin',
529
+ description: 'Empty plugin template',
530
+ code: this.createTemplate('basic')
531
+ },
532
+ 'template-geom': {
533
+ id: 'template-geom',
534
+ name: 'Geometry Generator',
535
+ description: 'Template for custom shape generation',
536
+ code: this.createTemplate('geometry')
537
+ }
538
+ };
539
+ // Store templates for marketplace
540
+ localStorage.setItem('_pluginTemplates', JSON.stringify(templates));
541
+ },
542
+
543
+ /**
544
+ * ============================================================================
545
+ * PERSISTENCE & UTILITIES
546
+ * ============================================================================
547
+ */
548
+
549
+ /**
550
+ * Save installed plugins to localStorage.
551
+ * @private
552
+ */
553
+ _savePlugins() {
554
+ const data = Array.from(this._plugins.entries()).map(([id, plugin]) => ({
555
+ id: plugin.id,
556
+ name: plugin.name,
557
+ version: plugin.version,
558
+ url: plugin.url,
559
+ enabled: plugin.enabled,
560
+ installedAt: plugin.installedAt,
561
+ fromMarketplace: plugin.fromMarketplace
562
+ }));
563
+ localStorage.setItem('ev_plugins', JSON.stringify(data));
564
+ },
565
+
566
+ /**
567
+ * Load previously installed plugins from localStorage.
568
+ * @private
569
+ */
570
+ _loadStoredPlugins() {
571
+ try {
572
+ const data = JSON.parse(localStorage.getItem('ev_plugins') || '[]');
573
+ // Note: Full plugin modules need to be re-fetched; this restores metadata
574
+ data.forEach(plugin => {
575
+ this._plugins.set(plugin.id, { ...plugin, enabled: false });
576
+ });
577
+ } catch (e) {
578
+ console.warn('[Plugin] Failed to load stored plugins:', e);
579
+ }
580
+ },
581
+
582
+ /**
583
+ * List all installed plugins.
584
+ * @returns {Array<Object>} Plugin list with metadata
585
+ *
586
+ * @example
587
+ * const plugins = await kernel.exec('plugin.list');
588
+ */
589
+ list() {
590
+ return Array.from(this._plugins.values()).map(p => ({
591
+ id: p.id,
592
+ name: p.name,
593
+ version: p.version,
594
+ author: p.author,
595
+ description: p.description,
596
+ enabled: p.enabled,
597
+ permissions: p.permissions || [],
598
+ installedAt: p.installedAt
599
+ }));
600
+ },
601
+
602
+ /**
603
+ * Get detailed info about a plugin.
604
+ * @param {string} pluginId - Plugin ID
605
+ * @returns {Object} Plugin details
606
+ */
607
+ getInfo(pluginId) {
608
+ const plugin = this._plugins.get(pluginId);
609
+ if (!plugin) throw new Error(`Plugin '${pluginId}' not found`);
610
+ return {
611
+ id: plugin.id,
612
+ name: plugin.name,
613
+ version: plugin.version,
614
+ author: plugin.author,
615
+ description: plugin.description,
616
+ enabled: plugin.enabled,
617
+ permissions: plugin.permissions || [],
618
+ url: plugin.url,
619
+ installedAt: plugin.installedAt,
620
+ updateAvailable: plugin.updateAvailable || false
621
+ };
622
+ },
623
+
624
+ /**
625
+ * ============================================================================
626
+ * UI PANEL
627
+ * ============================================================================
628
+ */
629
+
630
+ /**
631
+ * Return HTML for Plugin Manager panel.
632
+ * @returns {HTMLElement} Panel DOM
633
+ */
634
+ getUI() {
635
+ const panel = document.createElement('div');
636
+ panel.id = 'plugin-panel';
637
+ panel.className = 'panel-container';
638
+ panel.innerHTML = `
639
+ <div class="panel-header">
640
+ <h2>Plugin Manager</h2>
641
+ </div>
642
+ <div class="panel-content">
643
+ <div class="section-tabs">
644
+ <button class="tab-btn active" data-tab="installed">Installed</button>
645
+ <button class="tab-btn" data-tab="marketplace">Marketplace</button>
646
+ <button class="tab-btn" data-tab="develop">Develop</button>
647
+ </div>
648
+
649
+ <!-- Installed Plugins Tab -->
650
+ <div class="tab-content active" data-tab="installed">
651
+ <div id="installed-list" style="max-height: 400px; overflow-y: auto;">
652
+ <!-- Populated by JavaScript -->
653
+ </div>
654
+ <div style="margin-top: 12px; border-top: 1px solid #444; padding-top: 12px;">
655
+ <button class="btn btn-secondary" id="install-url-btn">Install from URL</button>
656
+ <button class="btn btn-secondary" id="install-file-btn">Install from File</button>
657
+ </div>
658
+ </div>
659
+
660
+ <!-- Marketplace Tab -->
661
+ <div class="tab-content" data-tab="marketplace">
662
+ <div id="marketplace-list" style="max-height: 400px; overflow-y: auto;">
663
+ <!-- Populated by JavaScript -->
664
+ </div>
665
+ </div>
666
+
667
+ <!-- Develop Tab -->
668
+ <div class="tab-content" data-tab="develop">
669
+ <div style="padding: 12px; background: #1e1e1e; border-radius: 4px; margin-bottom: 12px;">
670
+ <p style="font-size: 12px; color: #aaa; margin: 0 0 12px;">Create a new plugin from a template:</p>
671
+ <select id="template-select" style="width: 100%; padding: 8px; margin-bottom: 8px;">
672
+ <option value="basic">Basic Plugin</option>
673
+ <option value="geometry">Geometry Generator</option>
674
+ <option value="tool">Analysis Tool</option>
675
+ <option value="material">Material Library</option>
676
+ </select>
677
+ <button class="btn btn-primary" id="create-template-btn">Generate Template</button>
678
+ </div>
679
+ <textarea id="plugin-code" style="width: 100%; height: 300px; padding: 8px; font-family: monospace; font-size: 11px; background: #1a1a1a; color: #0f0; border: 1px solid #444; border-radius: 4px;" placeholder="Plugin code will appear here..."></textarea>
680
+ <div style="margin-top: 8px;">
681
+ <button class="btn btn-success" id="copy-code-btn">Copy Code</button>
682
+ <button class="btn btn-secondary" id="export-plugin-btn">Export Plugin</button>
683
+ </div>
684
+ </div>
685
+ </div>
686
+ `;
687
+
688
+ this._setupPanelEvents(panel);
689
+ this._populateInstalledList(panel);
690
+ this._populateMarketplace(panel);
691
+
692
+ return panel;
693
+ },
694
+
695
+ /**
696
+ * Setup event handlers for plugin panel.
697
+ * @param {HTMLElement} panel - Panel DOM element
698
+ * @private
699
+ */
700
+ _setupPanelEvents(panel) {
701
+ // Tab switching
702
+ panel.querySelectorAll('.tab-btn').forEach(btn => {
703
+ btn.addEventListener('click', (e) => {
704
+ const tab = e.target.dataset.tab;
705
+ panel.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
706
+ panel.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
707
+ e.target.classList.add('active');
708
+ panel.querySelector(`[data-tab="${tab}"]`).classList.add('active');
709
+ });
710
+ });
711
+
712
+ // Install from URL
713
+ panel.querySelector('#install-url-btn').addEventListener('click', async () => {
714
+ const url = prompt('Enter plugin URL:');
715
+ if (url) {
716
+ try {
717
+ await this.install(url);
718
+ this._populateInstalledList(panel);
719
+ } catch (e) {
720
+ alert(`Install failed: ${e.message}`);
721
+ }
722
+ }
723
+ });
724
+
725
+ // Install from File
726
+ panel.querySelector('#install-file-btn').addEventListener('click', () => {
727
+ const input = document.createElement('input');
728
+ input.type = 'file';
729
+ input.accept = '.js';
730
+ input.addEventListener('change', async (e) => {
731
+ const file = e.target.files[0];
732
+ if (file) {
733
+ const url = URL.createObjectURL(file);
734
+ try {
735
+ await this.install(url);
736
+ this._populateInstalledList(panel);
737
+ } catch (error) {
738
+ alert(`Install failed: ${error.message}`);
739
+ }
740
+ }
741
+ });
742
+ input.click();
743
+ });
744
+
745
+ // Template generation
746
+ panel.querySelector('#create-template-btn').addEventListener('click', () => {
747
+ const type = panel.querySelector('#template-select').value;
748
+ const code = this.createTemplate(type);
749
+ panel.querySelector('#plugin-code').value = code;
750
+ });
751
+
752
+ // Copy code
753
+ panel.querySelector('#copy-code-btn').addEventListener('click', () => {
754
+ const textarea = panel.querySelector('#plugin-code');
755
+ textarea.select();
756
+ document.execCommand('copy');
757
+ alert('Code copied to clipboard!');
758
+ });
759
+
760
+ // Export plugin
761
+ panel.querySelector('#export-plugin-btn').addEventListener('click', () => {
762
+ const code = panel.querySelector('#plugin-code').value;
763
+ const blob = new Blob([code], { type: 'text/javascript' });
764
+ const url = URL.createObjectURL(blob);
765
+ const a = document.createElement('a');
766
+ a.href = url;
767
+ a.download = 'my-plugin.js';
768
+ a.click();
769
+ URL.revokeObjectURL(url);
770
+ });
771
+ },
772
+
773
+ /**
774
+ * Populate installed plugins list.
775
+ * @param {HTMLElement} panel - Panel DOM element
776
+ * @private
777
+ */
778
+ _populateInstalledList(panel) {
779
+ const list = panel.querySelector('#installed-list');
780
+ list.innerHTML = '';
781
+
782
+ if (this._plugins.size === 0) {
783
+ list.innerHTML = '<p style="color: #999; text-align: center; padding: 20px;">No plugins installed</p>';
784
+ return;
785
+ }
786
+
787
+ this._plugins.forEach(plugin => {
788
+ const item = document.createElement('div');
789
+ item.className = 'plugin-item';
790
+ item.style.cssText = 'padding: 12px; background: #2a2a2a; border-radius: 4px; margin-bottom: 8px; display: flex; justify-content: space-between; align-items: center;';
791
+ item.innerHTML = `
792
+ <div>
793
+ <div style="font-weight: bold; color: #0284C7;">${plugin.name}</div>
794
+ <div style="font-size: 11px; color: #999;">v${plugin.version} by ${plugin.author || 'Unknown'}</div>
795
+ </div>
796
+ <div style="display: flex; gap: 8px;">
797
+ <input type="checkbox" class="plugin-toggle" data-id="${plugin.id}" ${plugin.enabled ? 'checked' : ''}>
798
+ <button class="btn btn-sm btn-danger plugin-delete-btn" data-id="${plugin.id}">Delete</button>
799
+ </div>
800
+ `;
801
+ list.appendChild(item);
802
+ });
803
+
804
+ // Add event listeners
805
+ list.querySelectorAll('.plugin-toggle').forEach(toggle => {
806
+ toggle.addEventListener('change', async (e) => {
807
+ const id = e.target.dataset.id;
808
+ if (e.target.checked) {
809
+ await this.enable(id);
810
+ } else {
811
+ await this.disable(id);
812
+ }
813
+ this._populateInstalledList(panel);
814
+ });
815
+ });
816
+
817
+ list.querySelectorAll('.plugin-delete-btn').forEach(btn => {
818
+ btn.addEventListener('click', async (e) => {
819
+ const id = e.target.dataset.id;
820
+ if (confirm('Uninstall this plugin?')) {
821
+ await this.uninstall(id);
822
+ this._populateInstalledList(panel);
823
+ }
824
+ });
825
+ });
826
+ },
827
+
828
+ /**
829
+ * Populate marketplace plugins list.
830
+ * @param {HTMLElement} panel - Panel DOM element
831
+ * @private
832
+ */
833
+ _populateMarketplace(panel) {
834
+ const list = panel.querySelector('#marketplace-list');
835
+ // Placeholder: In production, fetch from actual marketplace API
836
+ list.innerHTML = `
837
+ <div style="padding: 20px; text-align: center; color: #999;">
838
+ <p>Plugin Marketplace Coming Soon</p>
839
+ <p style="font-size: 11px;">Browse and install plugins from the community marketplace</p>
840
+ </div>
841
+ `;
842
+ },
843
+
844
+ // ========================================================================
845
+ // FUSION 360-PARITY ENHANCEMENTS: Sandboxed Execution
846
+ // ========================================================================
847
+
848
+ /**
849
+ * Run plugin in Web Worker for true sandboxing.
850
+ * No DOM access, message-based API only.
851
+ * @private
852
+ * @param {string} pluginId
853
+ * @param {string} moduleCode
854
+ * @returns {Worker}
855
+ */
856
+ _createWorkerSandbox(pluginId, moduleCode) {
857
+ const blob = new Blob(
858
+ [`
859
+ self.onmessage = async (e) => {
860
+ const { method, params, id } = e.data;
861
+ try {
862
+ const plugin = ${moduleCode};
863
+ const result = await plugin[method]?.(params);
864
+ self.postMessage({ id, result });
865
+ } catch (error) {
866
+ self.postMessage({ id, error: error.message });
867
+ }
868
+ };
869
+ `],
870
+ { type: 'application/javascript' }
871
+ );
872
+
873
+ return new Worker(URL.createObjectURL(blob));
874
+ },
875
+
876
+ /**
877
+ * Run plugin in iframe for DOM isolation + easier debugging.
878
+ * Messages flow through postMessage.
879
+ * @private
880
+ * @param {string} pluginId
881
+ * @param {string} moduleCode
882
+ * @returns {HTMLIFrameElement}
883
+ */
884
+ _createIframeSandbox(pluginId, moduleCode) {
885
+ const iframe = document.createElement('iframe');
886
+ iframe.id = `plugin-sandbox-${pluginId}`;
887
+ iframe.sandbox.add('allow-scripts');
888
+ iframe.style.display = 'none';
889
+
890
+ const html = `
891
+ <!DOCTYPE html>
892
+ <html>
893
+ <body>
894
+ <script>
895
+ const plugin = (${moduleCode}).default;
896
+ window.parent.postMessage({
897
+ type: 'plugin-ready',
898
+ pluginId: '${pluginId}'
899
+ }, '*');
900
+
901
+ window.onmessage = async (e) => {
902
+ if (e.data.pluginId !== '${pluginId}') return;
903
+ const { method, params, id } = e.data;
904
+ try {
905
+ const result = await plugin[method]?.(params);
906
+ window.parent.postMessage({ id, result }, '*');
907
+ } catch (error) {
908
+ window.parent.postMessage({ id, error: error.message }, '*');
909
+ }
910
+ };
911
+ </script>
912
+ </body>
913
+ </html>
914
+ `;
915
+
916
+ iframe.srcdoc = html;
917
+ document.body.appendChild(iframe);
918
+ return iframe;
919
+ },
920
+
921
+ // ========================================================================
922
+ // FUSION 360-PARITY ENHANCEMENTS: Hot Reload
923
+ // ========================================================================
924
+
925
+ /**
926
+ * Hot reload a plugin without restarting the app.
927
+ * Preserves plugin state across reload.
928
+ * @async
929
+ * @param {string} pluginId
930
+ * @returns {Promise<void>}
931
+ */
932
+ async hotReload(pluginId) {
933
+ const plugin = this._plugins.get(pluginId);
934
+ if (!plugin) throw new Error(`Plugin '${pluginId}' not found`);
935
+
936
+ // Save current state
937
+ const stateSnapshot = await this._capturePluginState(pluginId);
938
+
939
+ // Disable and re-fetch
940
+ await this.disable(pluginId);
941
+ const response = await fetch(plugin.url);
942
+ const moduleText = await response.text();
943
+ const module = await import(
944
+ `data:text/javascript,${encodeURIComponent(moduleText)}`
945
+ );
946
+
947
+ // Update module and enable
948
+ plugin.moduleInstance = module;
949
+ await this.enable(pluginId);
950
+
951
+ // Restore state if available
952
+ if (stateSnapshot) {
953
+ await this._restorePluginState(pluginId, stateSnapshot);
954
+ }
955
+
956
+ this._showNotification(`Hot reloaded: ${plugin.name}`, 'success');
957
+ },
958
+
959
+ /**
960
+ * Capture plugin state before hot reload.
961
+ * @private
962
+ * @param {string} pluginId
963
+ * @returns {Promise<Object>}
964
+ */
965
+ async _capturePluginState(pluginId) {
966
+ const plugin = this._plugins.get(pluginId);
967
+ if (!plugin.captureState) return null;
968
+
969
+ return new Promise(resolve => {
970
+ setTimeout(() => resolve(null), 1000); // Timeout after 1s
971
+ });
972
+ },
973
+
974
+ /**
975
+ * Restore plugin state after hot reload.
976
+ * @private
977
+ * @param {string} pluginId
978
+ * @param {Object} state
979
+ * @returns {Promise<void>}
980
+ */
981
+ async _restorePluginState(pluginId, state) {
982
+ const plugin = this._plugins.get(pluginId);
983
+ if (!plugin.restoreState) return;
984
+
985
+ await plugin.restoreState?.(state);
986
+ },
987
+
988
+ // ========================================================================
989
+ // FUSION 360-PARITY ENHANCEMENTS: Event Hooks
990
+ // ========================================================================
991
+
992
+ /**
993
+ * Register an event hook that plugins can intercept/modify.
994
+ * Example: before extrude, after feature added, on model changed.
995
+ * @private
996
+ */
997
+ _eventHooks: new Map(),
998
+
999
+ /**
1000
+ * Emit an event hook for plugins to intercept.
1001
+ * Plugins can modify the event data before it's applied.
1002
+ * @async
1003
+ * @param {string} hookName e.g., 'before:extrude', 'after:addFeature'
1004
+ * @param {Object} eventData
1005
+ * @returns {Promise<Object>} Potentially modified event data
1006
+ */
1007
+ async emitHook(hookName, eventData) {
1008
+ const hooks = this._eventHooks.get(hookName) || [];
1009
+
1010
+ for (const hook of hooks) {
1011
+ try {
1012
+ const modified = await hook(eventData);
1013
+ if (modified) eventData = modified;
1014
+ } catch (err) {
1015
+ console.error(`[Plugin] Hook '${hookName}' failed:`, err);
1016
+ }
1017
+ }
1018
+
1019
+ return eventData;
1020
+ },
1021
+
1022
+ /**
1023
+ * Let plugins register event hooks.
1024
+ * Plugins can modify operations before they're applied to the model.
1025
+ * @param {string} pluginId
1026
+ * @param {string} hookName
1027
+ * @param {Function} callback
1028
+ */
1029
+ registerHook(pluginId, hookName, callback) {
1030
+ if (!this._eventHooks.has(hookName)) {
1031
+ this._eventHooks.set(hookName, []);
1032
+ }
1033
+ this._eventHooks.get(hookName).push(callback);
1034
+ console.log(`[Plugin] Registered hook '${hookName}' for ${pluginId}`);
1035
+ },
1036
+
1037
+ // ========================================================================
1038
+ // FUSION 360-PARITY ENHANCEMENTS: Custom File Formats
1039
+ // ========================================================================
1040
+
1041
+ /**
1042
+ * Let plugins register custom file format importers/exporters.
1043
+ * @private
1044
+ */
1045
+ _fileFormatHandlers: new Map(),
1046
+
1047
+ /**
1048
+ * Register a custom file format.
1049
+ * @param {string} pluginId
1050
+ * @param {Object} config
1051
+ * @param {string} config.extension e.g., '.fcad'
1052
+ * @param {Function} config.importer async (file) => geometry
1053
+ * @param {Function} config.exporter async (geometry) => blob
1054
+ */
1055
+ registerFileFormat(pluginId, config) {
1056
+ const { extension, importer, exporter } = config;
1057
+
1058
+ this._fileFormatHandlers.set(extension, {
1059
+ pluginId,
1060
+ importer,
1061
+ exporter,
1062
+ });
1063
+
1064
+ console.log(
1065
+ `[Plugin] Registered file format '${extension}' from ${pluginId}`
1066
+ );
1067
+ },
1068
+
1069
+ /**
1070
+ * Import using a custom format handler.
1071
+ * @async
1072
+ * @param {File} file
1073
+ * @returns {Promise<Object>} Geometry
1074
+ */
1075
+ async importCustomFormat(file) {
1076
+ const ext = '.' + file.name.split('.').pop().toLowerCase();
1077
+ const handler = this._fileFormatHandlers.get(ext);
1078
+
1079
+ if (!handler) {
1080
+ throw new Error(`No handler registered for '${ext}'`);
1081
+ }
1082
+
1083
+ return handler.importer(file);
1084
+ },
1085
+
1086
+ /**
1087
+ * Export using a custom format handler.
1088
+ * @async
1089
+ * @param {Object} geometry
1090
+ * @param {string} extension
1091
+ * @returns {Promise<Blob>}
1092
+ */
1093
+ async exportCustomFormat(geometry, extension) {
1094
+ const handler = this._fileFormatHandlers.get(extension);
1095
+
1096
+ if (!handler) {
1097
+ throw new Error(`No handler registered for '${extension}'`);
1098
+ }
1099
+
1100
+ return handler.exporter(geometry);
1101
+ },
1102
+
1103
+ // ========================================================================
1104
+ // FUSION 360-PARITY ENHANCEMENTS: Plugin Dependencies
1105
+ // ========================================================================
1106
+
1107
+ /**
1108
+ * Resolve and validate plugin dependencies.
1109
+ * Auto-install required plugins in correct order.
1110
+ * @async
1111
+ * @param {Array<string>} dependencies Plugin IDs
1112
+ * @returns {Promise<boolean>} True if all dependencies satisfied
1113
+ */
1114
+ async validateDependencies(dependencies = []) {
1115
+ for (const depId of dependencies) {
1116
+ const dep = this._plugins.get(depId);
1117
+ if (!dep) {
1118
+ console.warn(`[Plugin] Missing dependency: ${depId}`);
1119
+ return false;
1120
+ }
1121
+ if (!dep.enabled) {
1122
+ console.warn(`[Plugin] Dependency not enabled: ${depId}`);
1123
+ return false;
1124
+ }
1125
+ }
1126
+ return true;
1127
+ },
1128
+
1129
+ /**
1130
+ * Auto-install missing dependencies.
1131
+ * @async
1132
+ * @param {Array<string>} dependencies Plugin IDs
1133
+ * @returns {Promise<void>}
1134
+ */
1135
+ async installMissingDependencies(dependencies = []) {
1136
+ for (const depId of dependencies) {
1137
+ const dep = this._plugins.get(depId);
1138
+ if (!dep) {
1139
+ console.error(
1140
+ `[Plugin] Cannot auto-install '${depId}' — not found in registry`
1141
+ );
1142
+ } else if (!dep.enabled) {
1143
+ await this.enable(depId);
1144
+ }
1145
+ }
1146
+ },
1147
+
1148
+ // ========================================================================
1149
+ // FUSION 360-PARITY ENHANCEMENTS: Plugin Settings UI
1150
+ // ========================================================================
1151
+
1152
+ /**
1153
+ * Get plugin settings panel.
1154
+ * Each plugin can define a settings schema.
1155
+ * @param {string} pluginId
1156
+ * @returns {Object} Settings config
1157
+ */
1158
+ getPluginSettings(pluginId) {
1159
+ const plugin = this._plugins.get(pluginId);
1160
+ if (!plugin) return {};
1161
+
1162
+ const stored = JSON.parse(
1163
+ localStorage.getItem(`plugin_settings_${pluginId}`) || '{}'
1164
+ );
1165
+
1166
+ return {
1167
+ id: pluginId,
1168
+ schema: plugin.settingsSchema || {},
1169
+ values: stored,
1170
+ };
1171
+ },
1172
+
1173
+ /**
1174
+ * Save plugin settings.
1175
+ * @param {string} pluginId
1176
+ * @param {Object} settings
1177
+ * @returns {void}
1178
+ */
1179
+ savePluginSettings(pluginId, settings) {
1180
+ localStorage.setItem(`plugin_settings_${pluginId}`, JSON.stringify(settings));
1181
+ this._broadcastEvent('pluginSettingsChanged', { pluginId, settings });
1182
+ },
1183
+
1184
+ // ========================================================================
1185
+ // FUSION 360-PARITY ENHANCEMENTS: Debug Mode
1186
+ // ========================================================================
1187
+
1188
+ /**
1189
+ * Enable debug mode for a plugin.
1190
+ * Shows console, event inspector, state viewer in overlay.
1191
+ * @param {string} pluginId
1192
+ */
1193
+ enableDebugMode(pluginId) {
1194
+ const debugPanel = document.createElement('div');
1195
+ debugPanel.id = `plugin-debug-${pluginId}`;
1196
+ debugPanel.style.cssText = `
1197
+ position: fixed;
1198
+ bottom: 0;
1199
+ right: 0;
1200
+ width: 400px;
1201
+ height: 300px;
1202
+ background: #1e1e1e;
1203
+ color: #0f0;
1204
+ border: 1px solid #0f0;
1205
+ border-radius: 0;
1206
+ font-family: monospace;
1207
+ font-size: 11px;
1208
+ overflow: hidden;
1209
+ z-index: 10000;
1210
+ `;
1211
+
1212
+ debugPanel.innerHTML = `
1213
+ <div style="display: flex; height: 100%; flex-direction: column;">
1214
+ <div style="padding: 4px; border-bottom: 1px solid #0f0; background: #0f0; color: #000; font-weight: bold;">
1215
+ Plugin Debug: ${pluginId}
1216
+ <button onclick="this.parentElement.parentElement.parentElement.remove()" style="float: right; background: none; border: none; color: #000; font-weight: bold; cursor: pointer;">×</button>
1217
+ </div>
1218
+ <div id="plugin-console-${pluginId}" style="flex: 1; overflow-y: auto; padding: 4px;"></div>
1219
+ <div style="border-top: 1px solid #0f0; padding: 4px;">
1220
+ <input type="text" id="plugin-input-${pluginId}" style="width: 100%; padding: 4px; background: #000; color: #0f0; border: none; font-family: monospace;" placeholder="Enter command..." />
1221
+ </div>
1222
+ </div>
1223
+ `;
1224
+
1225
+ document.body.appendChild(debugPanel);
1226
+
1227
+ // Log setup
1228
+ const consoleDom = debugPanel.querySelector(`#plugin-console-${pluginId}`);
1229
+ const inputDom = debugPanel.querySelector(`#plugin-input-${pluginId}`);
1230
+
1231
+ inputDom.addEventListener('keypress', e => {
1232
+ if (e.key === 'Enter') {
1233
+ const cmd = inputDom.value;
1234
+ consoleDom.innerHTML += `\n> ${cmd}`;
1235
+ inputDom.value = '';
1236
+ }
1237
+ });
1238
+
1239
+ console.log(`[Plugin] Debug mode enabled for ${pluginId}`);
1240
+ },
1241
+
1242
+ /**
1243
+ * ============================================================================
1244
+ * HELP ENTRIES
1245
+ * ============================================================================
1246
+ */
1247
+
1248
+ /**
1249
+ * @type {Array<Object>} Help system entries
1250
+ */
1251
+ helpEntries: [
1252
+ {
1253
+ id: 'plugin-manager',
1254
+ title: 'Plugin Manager',
1255
+ category: 'Extend',
1256
+ description: 'Install, enable, and manage plugins that extend cycleCAD functionality.',
1257
+ shortcut: 'View → Plugin Manager',
1258
+ details: `
1259
+ <h4>Overview</h4>
1260
+ <p>The Plugin Manager lets you extend cycleCAD with custom features through JavaScript plugins.</p>
1261
+
1262
+ <h4>Installing Plugins</h4>
1263
+ <ol>
1264
+ <li><strong>From URL:</strong> Click "Install from URL" and paste the plugin URL</li>
1265
+ <li><strong>From File:</strong> Click "Install from File" and select a .js file</li>
1266
+ <li><strong>From Marketplace:</strong> Browse the marketplace and click Install</li>
1267
+ </ol>
1268
+
1269
+ <h4>Creating Your Own Plugin</h4>
1270
+ <p>Use the "Develop" tab to generate a plugin template. A plugin is a JavaScript module that exports:</p>
1271
+ <pre>export default {
1272
+ id: 'plugin-id',
1273
+ name: 'Plugin Name',
1274
+ version: '1.0.0',
1275
+ async activate(kernel) { /* Setup code */ },
1276
+ async deactivate() { /* Cleanup */ }
1277
+ }</pre>
1278
+
1279
+ <h4>Plugin API</h4>
1280
+ <p>Plugins access cycleCAD via a sandboxed kernel API:</p>
1281
+ <ul>
1282
+ <li><code>kernel.exec(command, params)</code> — Execute cycleCAD commands</li>
1283
+ <li><code>kernel.registerCommand(name, handler)</code> — Register custom commands</li>
1284
+ <li><code>kernel.registerButton(config)</code> — Add UI buttons</li>
1285
+ </ul>
1286
+ `
1287
+ },
1288
+ {
1289
+ id: 'plugin-examples',
1290
+ title: 'Plugin Examples',
1291
+ category: 'Extend',
1292
+ description: 'Common patterns for building cycleCAD plugins.',
1293
+ details: `
1294
+ <h4>Example 1: Gear Generator</h4>
1295
+ <p>A plugin that adds a "Create Gear" button and custom commands:</p>
1296
+ <pre>export default {
1297
+ id: 'gear-generator',
1298
+ name: 'Gear Generator',
1299
+ version: '1.0.0',
1300
+ permissions: ['shape.create', 'viewport.addMesh'],
1301
+ async activate(kernel) {
1302
+ kernel.registerCommand('gear.create', async (params) => {
1303
+ // Generate gear geometry
1304
+ const gear = generateInvoluteGear(params.teeth, params.module);
1305
+ return kernel.exec('viewport.addMesh', { geometry: gear });
1306
+ });
1307
+ }
1308
+ }</pre>
1309
+
1310
+ <h4>Example 2: Analysis Tool</h4>
1311
+ <p>A plugin that analyzes the current model and displays results:</p>
1312
+ <pre>export default {
1313
+ id: 'weight-analyzer',
1314
+ name: 'Weight Analyzer',
1315
+ async activate(kernel) {
1316
+ kernel.registerButton({
1317
+ label: 'Analyze Weight',
1318
+ onClick: async () => {
1319
+ const weight = await analyzeWeight();
1320
+ alert(\`Total weight: \${weight}kg\`);
1321
+ }
1322
+ });
1323
+ }
1324
+ }</pre>
1325
+ `
1326
+ },
1327
+ {
1328
+ id: 'plugin-permissions',
1329
+ title: 'Plugin Permissions',
1330
+ category: 'Extend',
1331
+ description: 'Control what API access plugins have.',
1332
+ details: `
1333
+ <h4>Permission System</h4>
1334
+ <p>Plugins declare required permissions in their manifest. cycleCAD enforces these at runtime.</p>
1335
+
1336
+ <h4>Available Permissions</h4>
1337
+ <ul>
1338
+ <li><strong>Shape Creation:</strong> shape.create, shape.extrude, shape.revolve, shape.fillet, shape.chamfer, shape.boolean, shape.pattern</li>
1339
+ <li><strong>Viewport:</strong> viewport.addMesh, viewport.removeMesh, viewport.setColor, viewport.setMaterial, viewport.fitToSelection</li>
1340
+ <li><strong>Tree:</strong> tree.addFeature, tree.removeFeature, tree.renameFeature</li>
1341
+ <li><strong>Export:</strong> export.stl, export.obj, export.gltf</li>
1342
+ </ul>
1343
+
1344
+ <h4>Declaring Permissions</h4>
1345
+ <pre>export default {
1346
+ id: 'my-plugin',
1347
+ permissions: ['shape.extrude', 'viewport.addMesh'],
1348
+ // ...
1349
+ }</pre>
1350
+
1351
+ <p><strong>Note:</strong> Plugins can only call whitelisted commands. Attempting to access other APIs will raise an error.</p>
1352
+ `
1353
+ },
1354
+ {
1355
+ id: 'plugin-sandboxing',
1356
+ title: 'Plugin Sandboxing',
1357
+ category: 'Extend',
1358
+ description: 'How plugins are isolated for security and stability.',
1359
+ details: `
1360
+ <h4>Sandboxing Modes</h4>
1361
+ <p>cycleCAD offers multiple sandboxing strategies to protect the app:</p>
1362
+
1363
+ <h4>Web Worker Sandbox</h4>
1364
+ <p>Plugins run in a Web Worker with zero DOM access. Message-based API only. Most secure but limited UI capabilities.</p>
1365
+
1366
+ <h4>iframe Sandbox</h4>
1367
+ <p>Plugins run in an isolated iframe with <code>sandbox</code> attribute. Can't access parent DOM or localStorage.</p>
1368
+
1369
+ <h4>No Sandbox (Trusted Plugins)</h4>
1370
+ <p>Plugins from the official marketplace run in main thread. Faster but requires trust.</p>
1371
+
1372
+ <h4>Performance Impact</h4>
1373
+ <ul>
1374
+ <li><strong>Worker Sandbox:</strong> ~5ms message latency per command</li>
1375
+ <li><strong>iframe Sandbox:</strong> ~2ms message latency per command</li>
1376
+ <li><strong>No Sandbox:</strong> Direct synchronous calls, <1ms latency</li>
1377
+ </ul>
1378
+ `
1379
+ },
1380
+ {
1381
+ id: 'plugin-hot-reload',
1382
+ title: 'Hot Reload Plugins',
1383
+ category: 'Extend',
1384
+ description: 'Update plugin code without restarting the app.',
1385
+ details: `
1386
+ <h4>Hot Reload Benefits</h4>
1387
+ <ul>
1388
+ <li>Develop and test plugins iteratively</li>
1389
+ <li>Update installed plugins to latest version</li>
1390
+ <li>Preserve plugin state across reloads</li>
1391
+ <li>No need to restart cycleCAD</li>
1392
+ </ul>
1393
+
1394
+ <h4>How to Hot Reload</h4>
1395
+ <ol>
1396
+ <li>In Plugin Manager, find the plugin you want to reload</li>
1397
+ <li>Click the refresh icon next to the plugin name</li>
1398
+ <li>Plugin disables, code is re-fetched, then re-enabled</li>
1399
+ <li>Plugin state is automatically restored (if plugin implements <code>captureState()</code> and <code>restoreState()</code>)</li>
1400
+ </ol>
1401
+
1402
+ <h4>Implementing Stateful Hot Reload</h4>
1403
+ <pre>export default {
1404
+ id: 'my-plugin',
1405
+ async captureState() {
1406
+ return { myData: this.data };
1407
+ },
1408
+ async restoreState(state) {
1409
+ this.data = state.myData;
1410
+ }
1411
+ }</pre>
1412
+ `
1413
+ },
1414
+ {
1415
+ id: 'plugin-event-hooks',
1416
+ title: 'Plugin Event Hooks',
1417
+ category: 'Extend',
1418
+ description: 'Intercept and modify kernel events in plugins.',
1419
+ details: `
1420
+ <h4>Available Hooks</h4>
1421
+ <ul>
1422
+ <li><strong>before:extrude</strong> — Called before extrude operation</li>
1423
+ <li><strong>after:extrude</strong> — Called after extrude operation</li>
1424
+ <li><strong>before:revolve</strong> — Before revolve</li>
1425
+ <li><strong>after:addFeature</strong> — After feature is added to tree</li>
1426
+ <li><strong>before:save</strong> — Before model is saved</li>
1427
+ <li><strong>before:export</strong> — Before export (modify export params)</li>
1428
+ </ul>
1429
+
1430
+ <h4>Registering a Hook</h4>
1431
+ <pre>kernel.registerHook('before:extrude', async (eventData) => {
1432
+ // Modify eventData
1433
+ eventData.distance *= 1.1; // Add 10% to all extrusions
1434
+ return eventData;
1435
+ });</pre>
1436
+
1437
+ <h4>Hook Data Format</h4>
1438
+ <p>Each hook receives event data with the operation parameters. Return modified data to apply changes, or return null to cancel.</p>
1439
+ `
1440
+ },
1441
+ {
1442
+ id: 'plugin-custom-formats',
1443
+ title: 'Custom File Formats',
1444
+ category: 'Extend',
1445
+ description: 'Register custom importers/exporters for new file types.',
1446
+ details: `
1447
+ <h4>Supported File Formats (Built-in)</h4>
1448
+ <ul>
1449
+ <li>.step / .stp — STEP CAD files</li>
1450
+ <li>.stl — STL mesh files</li>
1451
+ <li>.obj — OBJ mesh files</li>
1452
+ <li>.gltf / .glb — glTF/GLB 3D files</li>
1453
+ </ul>
1454
+
1455
+ <h4>Register Custom Format</h4>
1456
+ <pre>kernel.registerFileFormat('my-plugin', {
1457
+ extension: '.myformat',
1458
+ importer: async (file) => {
1459
+ const text = await file.text();
1460
+ const geometry = parseMyFormat(text);
1461
+ return geometry;
1462
+ },
1463
+ exporter: async (geometry) => {
1464
+ const data = serializeMyFormat(geometry);
1465
+ return new Blob([data], { type: 'text/plain' });
1466
+ }
1467
+ });</pre>
1468
+
1469
+ <h4>Using Custom Formats</h4>
1470
+ <p>Once registered, custom formats appear in File → Import/Export dialogs automatically.</p>
1471
+ `
1472
+ },
1473
+ {
1474
+ id: 'plugin-dependencies',
1475
+ title: 'Plugin Dependencies',
1476
+ category: 'Extend',
1477
+ description: 'Plugins can require other plugins as dependencies.',
1478
+ details: `
1479
+ <h4>Declaring Dependencies</h4>
1480
+ <pre>export default {
1481
+ id: 'advanced-analysis',
1482
+ name: 'Advanced Analysis',
1483
+ dependencies: ['geometry-utils', 'visualization'],
1484
+ // ...
1485
+ }</pre>
1486
+
1487
+ <h4>Dependency Resolution</h4>
1488
+ <p>When installing a plugin with dependencies, cycleCAD will:</p>
1489
+ <ol>
1490
+ <li>Check if all dependencies are installed</li>
1491
+ <li>Prompt to install missing dependencies</li>
1492
+ <li>Install dependencies in correct order</li>
1493
+ <li>Enable all dependencies before enabling dependent plugin</li>
1494
+ </ol>
1495
+
1496
+ <h4>Circular Dependencies</h4>
1497
+ <p>cycleCAD detects and prevents circular dependencies. If detected, installation fails with clear error message.</p>
1498
+ `
1499
+ },
1500
+ {
1501
+ id: 'plugin-settings',
1502
+ title: 'Plugin Settings',
1503
+ category: 'Extend',
1504
+ description: 'Plugins can have user-configurable settings.',
1505
+ details: `
1506
+ <h4>Define Plugin Settings</h4>
1507
+ <pre>export default {
1508
+ id: 'my-plugin',
1509
+ settingsSchema: {
1510
+ threshold: { type: 'number', default: 0.5, label: 'Threshold' },
1511
+ debug: { type: 'boolean', default: false, label: 'Debug Mode' },
1512
+ color: { type: 'color', default: '#0000ff', label: 'Color' }
1513
+ },
1514
+ // ...
1515
+ }</pre>
1516
+
1517
+ <h4>Access Settings in Plugin</h4>
1518
+ <p>Plugin Manager automatically creates UI for settings. Plugins read settings via:</p>
1519
+ <pre>const settings = kernel.getPluginSettings('my-plugin');
1520
+ const threshold = settings.values.threshold;</pre>
1521
+
1522
+ <h4>Settings Storage</h4>
1523
+ <p>All settings are stored in browser localStorage per plugin. Persists across app restarts.</p>
1524
+ `
1525
+ },
1526
+ {
1527
+ id: 'plugin-debug-mode',
1528
+ title: 'Plugin Debug Mode',
1529
+ category: 'Extend',
1530
+ description: 'Debug plugins with console, event inspector, and state viewer.',
1531
+ details: `
1532
+ <h4>Enabling Debug Mode</h4>
1533
+ <p>Right-click a plugin in Plugin Manager → Enable Debug Mode</p>
1534
+
1535
+ <h4>Debug Panel</h4>
1536
+ <p>A green-on-black debug console appears at bottom-right showing:</p>
1537
+ <ul>
1538
+ <li><strong>Plugin Console:</strong> All console.log/error output from plugin</li>
1539
+ <li><strong>Event Inspector:</strong> All events emitted/received by plugin</li>
1540
+ <li><strong>State Viewer:</strong> Current plugin state snapshot</li>
1541
+ <li><strong>REPL:</strong> Run JavaScript in plugin context</li>
1542
+ </ul>
1543
+
1544
+ <h4>Console Commands</h4>
1545
+ <pre>> kernel.exec('shape.cylinder', { radius: 25, height: 80 })
1546
+ > getState()
1547
+ > setBreakpoint('on:featureAdded')</pre>
1548
+
1549
+ <h4>Performance Impact</h4>
1550
+ <p>Debug mode adds ~5-10% overhead. Disable when done debugging.</p>
1551
+ `
1552
+ }
1553
+ ]
1554
+ };