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.
- package/IMPLEMENTATION_GUIDE.md +502 -0
- package/INTEGRATION-GUIDE.md +377 -0
- package/MODULES_PHASES_6_7.md +780 -0
- package/app/index.html +106 -2
- package/app/js/brep-kernel.js +1353 -455
- package/app/js/help-module.js +1437 -0
- package/app/js/kernel.js +364 -40
- package/app/js/modules/animation-module.js +967 -0
- package/app/js/modules/assembly-module.js +47 -3
- package/app/js/modules/cam-module.js +1067 -0
- package/app/js/modules/collaboration-module.js +1102 -0
- package/app/js/modules/data-module.js +1656 -0
- package/app/js/modules/drawing-module.js +54 -8
- package/app/js/modules/formats-module.js +1173 -0
- package/app/js/modules/inspection-module.js +937 -0
- package/app/js/modules/mesh-module.js +968 -0
- package/app/js/modules/operations-module.js +40 -7
- package/app/js/modules/plugin-module.js +957 -0
- package/app/js/modules/rendering-module.js +1306 -0
- package/app/js/modules/scripting-module.js +955 -0
- package/app/js/modules/simulation-module.js +60 -3
- package/app/js/modules/sketch-module.js +1032 -90
- package/app/js/modules/step-module.js +47 -6
- package/app/js/modules/surface-module.js +728 -0
- package/app/js/modules/version-module.js +1410 -0
- package/app/js/modules/viewport-module.js +95 -8
- package/app/test-agent-v2.html +881 -1316
- package/docs/ARCHITECTURE.html +838 -1408
- package/docs/DEVELOPER-GUIDE.md +1504 -0
- package/docs/TUTORIAL.md +740 -0
- package/package.json +1 -1
- package/.github/scripts/cad-diff.js +0 -590
- package/.github/workflows/cad-diff.yml +0 -117
|
@@ -0,0 +1,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
|
+
};
|