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.
- package/DELIVERABLES.txt +296 -445
- package/ENHANCEMENT_COMPLETION_REPORT.md +383 -0
- package/ENHANCEMENT_SUMMARY.txt +308 -0
- package/FEATURE_INVENTORY.md +235 -0
- package/FUSION360_FEATURES_SUMMARY.md +452 -0
- package/FUSION360_PARITY_ENHANCEMENTS.md +461 -0
- package/FUSION360_PARITY_SUMMARY.md +520 -0
- package/FUSION360_QUICK_REFERENCE.md +351 -0
- package/IMPLEMENTATION_GUIDE.md +502 -0
- package/INTEGRATION-GUIDE.md +377 -0
- package/MODULES_PHASES_6_7.md +780 -0
- package/MODULE_API_REFERENCE.md +712 -0
- package/MODULE_INVENTORY.txt +264 -0
- package/app/index.html +1345 -4930
- package/app/js/app.js +1312 -514
- 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 +1461 -0
- package/app/js/modules/assembly-module.js +47 -3
- package/app/js/modules/cam-module.js +1572 -0
- package/app/js/modules/collaboration-module.js +1615 -0
- package/app/js/modules/constraint-module.js +1266 -0
- package/app/js/modules/data-module.js +1054 -0
- package/app/js/modules/drawing-module.js +54 -8
- package/app/js/modules/formats-module.js +873 -0
- package/app/js/modules/inspection-module.js +1330 -0
- package/app/js/modules/mesh-module-enhanced.js +880 -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 +1554 -0
- package/app/js/modules/rendering-module.js +1766 -0
- package/app/js/modules/scripting-module.js +1073 -0
- package/app/js/modules/simulation-module.js +60 -3
- package/app/js/modules/sketch-module.js +2029 -91
- package/app/js/modules/step-module.js +47 -6
- package/app/js/modules/surface-module.js +1040 -0
- package/app/js/modules/version-module.js +1830 -0
- package/app/js/modules/viewport-module.js +95 -8
- package/app/test-agent-v2.html +881 -1316
- package/cycleCAD-Architecture-v2.pptx +0 -0
- 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/~$cycleCAD-Architecture-v2.pptx +0 -0
- package/.github/scripts/cad-diff.js +0 -590
- 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
|
+
};
|