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.
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,957 @@
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
+ * ============================================================================
846
+ * HELP ENTRIES
847
+ * ============================================================================
848
+ */
849
+
850
+ /**
851
+ * @type {Array<Object>} Help system entries
852
+ */
853
+ helpEntries: [
854
+ {
855
+ id: 'plugin-manager',
856
+ title: 'Plugin Manager',
857
+ category: 'Extend',
858
+ description: 'Install, enable, and manage plugins that extend cycleCAD functionality.',
859
+ shortcut: 'View → Plugin Manager',
860
+ details: `
861
+ <h4>Overview</h4>
862
+ <p>The Plugin Manager lets you extend cycleCAD with custom features through JavaScript plugins.</p>
863
+
864
+ <h4>Installing Plugins</h4>
865
+ <ol>
866
+ <li><strong>From URL:</strong> Click "Install from URL" and paste the plugin URL</li>
867
+ <li><strong>From File:</strong> Click "Install from File" and select a .js file</li>
868
+ <li><strong>From Marketplace:</strong> Browse the marketplace and click Install</li>
869
+ </ol>
870
+
871
+ <h4>Creating Your Own Plugin</h4>
872
+ <p>Use the "Develop" tab to generate a plugin template. A plugin is a JavaScript module that exports:</p>
873
+ <pre>export default {
874
+ id: 'plugin-id',
875
+ name: 'Plugin Name',
876
+ version: '1.0.0',
877
+ async activate(kernel) { /* Setup code */ },
878
+ async deactivate() { /* Cleanup */ }
879
+ }</pre>
880
+
881
+ <h4>Plugin API</h4>
882
+ <p>Plugins access cycleCAD via a sandboxed kernel API:</p>
883
+ <ul>
884
+ <li><code>kernel.exec(command, params)</code> — Execute cycleCAD commands</li>
885
+ <li><code>kernel.registerCommand(name, handler)</code> — Register custom commands</li>
886
+ <li><code>kernel.registerButton(config)</code> — Add UI buttons</li>
887
+ </ul>
888
+ `
889
+ },
890
+ {
891
+ id: 'plugin-examples',
892
+ title: 'Plugin Examples',
893
+ category: 'Extend',
894
+ description: 'Common patterns for building cycleCAD plugins.',
895
+ details: `
896
+ <h4>Example 1: Gear Generator</h4>
897
+ <p>A plugin that adds a "Create Gear" button and custom commands:</p>
898
+ <pre>export default {
899
+ id: 'gear-generator',
900
+ name: 'Gear Generator',
901
+ version: '1.0.0',
902
+ permissions: ['shape.create', 'viewport.addMesh'],
903
+ async activate(kernel) {
904
+ kernel.registerCommand('gear.create', async (params) => {
905
+ // Generate gear geometry
906
+ const gear = generateInvoluteGear(params.teeth, params.module);
907
+ return kernel.exec('viewport.addMesh', { geometry: gear });
908
+ });
909
+ }
910
+ }</pre>
911
+
912
+ <h4>Example 2: Analysis Tool</h4>
913
+ <p>A plugin that analyzes the current model and displays results:</p>
914
+ <pre>export default {
915
+ id: 'weight-analyzer',
916
+ name: 'Weight Analyzer',
917
+ async activate(kernel) {
918
+ kernel.registerButton({
919
+ label: 'Analyze Weight',
920
+ onClick: async () => {
921
+ const weight = await analyzeWeight();
922
+ alert(\`Total weight: \${weight}kg\`);
923
+ }
924
+ });
925
+ }
926
+ }</pre>
927
+ `
928
+ },
929
+ {
930
+ id: 'plugin-permissions',
931
+ title: 'Plugin Permissions',
932
+ category: 'Extend',
933
+ description: 'Control what API access plugins have.',
934
+ details: `
935
+ <h4>Permission System</h4>
936
+ <p>Plugins declare required permissions in their manifest. cycleCAD enforces these at runtime.</p>
937
+
938
+ <h4>Available Permissions</h4>
939
+ <ul>
940
+ <li><strong>Shape Creation:</strong> shape.create, shape.extrude, shape.revolve, shape.fillet, shape.chamfer, shape.boolean, shape.pattern</li>
941
+ <li><strong>Viewport:</strong> viewport.addMesh, viewport.removeMesh, viewport.setColor, viewport.setMaterial, viewport.fitToSelection</li>
942
+ <li><strong>Tree:</strong> tree.addFeature, tree.removeFeature, tree.renameFeature</li>
943
+ <li><strong>Export:</strong> export.stl, export.obj, export.gltf</li>
944
+ </ul>
945
+
946
+ <h4>Declaring Permissions</h4>
947
+ <pre>export default {
948
+ id: 'my-plugin',
949
+ permissions: ['shape.extrude', 'viewport.addMesh'],
950
+ // ...
951
+ }</pre>
952
+
953
+ <p><strong>Note:</strong> Plugins can only call whitelisted commands. Attempting to access other APIs will raise an error.</p>
954
+ `
955
+ }
956
+ ]
957
+ };