cyclecad 3.2.1 → 3.4.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/DOCKER-SETUP-VERIFICATION.md +399 -0
- package/DOCKER-TESTING.md +463 -0
- package/FUSION360_MODULES.md +478 -0
- package/FUSION_MODULES_README.md +352 -0
- package/INTEGRATION_SNIPPETS.md +608 -0
- package/KILLER-FEATURES-DELIVERY.md +469 -0
- package/MODULES_SUMMARY.txt +337 -0
- package/QUICK_REFERENCE.txt +298 -0
- package/README-DOCKER-TESTING.txt +438 -0
- package/app/index.html +23 -10
- package/app/js/fusion-help.json +1808 -0
- package/app/js/help-module-v3.js +1096 -0
- package/app/js/killer-features-help.json +395 -0
- package/app/js/killer-features.js +1508 -0
- package/app/js/modules/fusion-assembly.js +842 -0
- package/app/js/modules/fusion-cam.js +785 -0
- package/app/js/modules/fusion-data.js +814 -0
- package/app/js/modules/fusion-drawing.js +844 -0
- package/app/js/modules/fusion-inspection.js +756 -0
- package/app/js/modules/fusion-render.js +774 -0
- package/app/js/modules/fusion-simulation.js +986 -0
- package/app/js/modules/fusion-sketch.js +1044 -0
- package/app/js/modules/fusion-solid.js +1095 -0
- package/app/js/modules/fusion-surface.js +949 -0
- package/app/tests/FUSION_TEST_SUITE.md +266 -0
- package/app/tests/README.md +77 -0
- package/app/tests/TESTING-CHECKLIST.md +177 -0
- package/app/tests/TEST_SUITE_SUMMARY.txt +236 -0
- package/app/tests/brep-live-test.html +848 -0
- package/app/tests/docker-integration-test.html +811 -0
- package/app/tests/fusion-all-tests.html +670 -0
- package/app/tests/fusion-assembly-tests.html +461 -0
- package/app/tests/fusion-cam-tests.html +421 -0
- package/app/tests/fusion-simulation-tests.html +421 -0
- package/app/tests/fusion-sketch-tests.html +613 -0
- package/app/tests/fusion-solid-tests.html +529 -0
- package/app/tests/index.html +453 -0
- package/app/tests/killer-features-test.html +509 -0
- package/app/tests/run-tests.html +874 -0
- package/app/tests/step-import-live-test.html +1115 -0
- package/app/tests/test-agent-v3.html +93 -696
- package/architecture-dashboard.html +1970 -0
- package/docs/API-REFERENCE.md +1423 -0
- package/docs/BREP-LIVE-TEST-GUIDE.md +453 -0
- package/docs/DEVELOPER-GUIDE-v3.md +795 -0
- package/docs/DOCKER-QUICK-TEST.md +376 -0
- package/docs/FUSION-FEATURES-GUIDE.md +2513 -0
- package/docs/FUSION-TUTORIAL.md +1203 -0
- package/docs/INFRASTRUCTURE-GUIDE-INDEX.md +327 -0
- package/docs/KEYBOARD-SHORTCUTS.md +402 -0
- package/docs/KILLER-FEATURES-INTEGRATION.md +412 -0
- package/docs/KILLER-FEATURES-SUMMARY.md +424 -0
- package/docs/KILLER-FEATURES-TUTORIAL.md +784 -0
- package/docs/KILLER-FEATURES.md +562 -0
- package/docs/QUICK-REFERENCE.md +282 -0
- package/docs/README-v3-DOCS.md +274 -0
- package/docs/TUTORIAL-v3.md +1190 -0
- package/docs/architecture-dashboard.html +1970 -0
- package/docs/architecture-v3.html +1038 -0
- package/linkedin-post-v3.md +58 -0
- package/package.json +1 -1
- package/scripts/dev-setup.sh +338 -0
- package/scripts/docker-health-check.sh +159 -0
- package/scripts/integration-test.sh +311 -0
- package/scripts/test-docker.sh +515 -0
|
@@ -0,0 +1,795 @@
|
|
|
1
|
+
# cycleCAD v3.2 Developer Guide
|
|
2
|
+
|
|
3
|
+
Complete reference for developers extending cycleCAD through modules, plugins, and API integration.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
1. [Kernel Architecture](#kernel-architecture)
|
|
8
|
+
2. [Module Development](#module-development)
|
|
9
|
+
3. [Event System](#event-system)
|
|
10
|
+
4. [Command API](#command-api)
|
|
11
|
+
5. [Plugin Development](#plugin-development)
|
|
12
|
+
6. [Testing](#testing)
|
|
13
|
+
7. [Contributing](#contributing)
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Kernel Architecture
|
|
18
|
+
|
|
19
|
+
### LEGO Microkernel Concept
|
|
20
|
+
|
|
21
|
+
cycleCAD uses a **LEGO microkernel** pattern: minimal core with maximum modularity.
|
|
22
|
+
|
|
23
|
+
```javascript
|
|
24
|
+
// Kernel (100 lines)
|
|
25
|
+
const kernel = {
|
|
26
|
+
modules: Map,
|
|
27
|
+
state: Object,
|
|
28
|
+
eventBus: EventEmitter,
|
|
29
|
+
|
|
30
|
+
register(moduleDef) { ... },
|
|
31
|
+
activate(name) { ... },
|
|
32
|
+
exec(command, params) { ... },
|
|
33
|
+
on(event, handler) { ... }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Each module is a LEGO brick
|
|
37
|
+
// Kernel provides the base plate
|
|
38
|
+
// Developers snap in bricks (modules)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Why Microkernel?
|
|
42
|
+
|
|
43
|
+
- **Loose coupling**: Modules don't directly depend on each other
|
|
44
|
+
- **Easy testing**: Modules can be tested in isolation
|
|
45
|
+
- **Extensibility**: Add new modules without modifying core
|
|
46
|
+
- **Hot-reload**: Swap modules at runtime (advanced)
|
|
47
|
+
- **Scalability**: 21 modules can grow to 50+ without bloat
|
|
48
|
+
|
|
49
|
+
### Kernel API
|
|
50
|
+
|
|
51
|
+
```javascript
|
|
52
|
+
// Register a module
|
|
53
|
+
kernel.register({
|
|
54
|
+
name: 'myModule',
|
|
55
|
+
init() { /* setup */ },
|
|
56
|
+
deactivate() { /* cleanup */ },
|
|
57
|
+
commands: { myCommand() { ... } }
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
// Activate workspace (activates relevant modules)
|
|
61
|
+
kernel.activate('Design') // activates sketch, modeling, features
|
|
62
|
+
|
|
63
|
+
// Execute command
|
|
64
|
+
await kernel.exec('shape.cylinder', { d: 50, h: 80 })
|
|
65
|
+
|
|
66
|
+
// Listen to events
|
|
67
|
+
kernel.on('model:changed', (delta) => { ... })
|
|
68
|
+
|
|
69
|
+
// Get/set state
|
|
70
|
+
kernel.state.get('activeModel')
|
|
71
|
+
kernel.state.set('activeModel', model)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Module Development
|
|
77
|
+
|
|
78
|
+
### Module Lifecycle
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
1. register() → Module registered with kernel
|
|
82
|
+
2. init() → Module initializes (load data, setup UI)
|
|
83
|
+
3. activate() → Module becomes active in workspace
|
|
84
|
+
4. commands... → User invokes commands
|
|
85
|
+
5. deactivate() → Module stops (user switched workspace)
|
|
86
|
+
6. unload() → Module removed from memory (optional)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Module Template
|
|
90
|
+
|
|
91
|
+
```javascript
|
|
92
|
+
// app/js/modules/myfeature.js
|
|
93
|
+
export const MyFeatureModule = {
|
|
94
|
+
// Metadata
|
|
95
|
+
name: 'myfeature',
|
|
96
|
+
version: '1.0.0',
|
|
97
|
+
dependencies: ['viewport', 'tree'], // Modules this depends on
|
|
98
|
+
|
|
99
|
+
// Lifecycle
|
|
100
|
+
init() {
|
|
101
|
+
// Called when module loads
|
|
102
|
+
// Set up UI, event listeners, state
|
|
103
|
+
console.log('[MyFeature] Initializing...')
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
deactivate() {
|
|
107
|
+
// Called when workspace changes
|
|
108
|
+
// Clean up listeners, hide UI
|
|
109
|
+
console.log('[MyFeature] Deactivating...')
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
// Commands (callable via API)
|
|
113
|
+
commands: {
|
|
114
|
+
myCommand(params) {
|
|
115
|
+
// { name, value, ... }
|
|
116
|
+
// Return result or throw error
|
|
117
|
+
return { success: true, result: {...} }
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
anotherCommand(params) {
|
|
121
|
+
// Can be async
|
|
122
|
+
return fetch('/api/data').then(r => r.json())
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
// Events this module publishes
|
|
127
|
+
events: [
|
|
128
|
+
'myfeature:action-completed',
|
|
129
|
+
'myfeature:error'
|
|
130
|
+
],
|
|
131
|
+
|
|
132
|
+
// UI Panel (optional)
|
|
133
|
+
getUI() {
|
|
134
|
+
return `
|
|
135
|
+
<div id="myfeature-panel">
|
|
136
|
+
<button id="my-btn">Click me</button>
|
|
137
|
+
</div>
|
|
138
|
+
`
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Complete Example: Simple Calculator Module
|
|
144
|
+
|
|
145
|
+
```javascript
|
|
146
|
+
export const CalculatorModule = {
|
|
147
|
+
name: 'calculator',
|
|
148
|
+
version: '1.0.0',
|
|
149
|
+
dependencies: [],
|
|
150
|
+
|
|
151
|
+
init() {
|
|
152
|
+
this.result = 0
|
|
153
|
+
this.kernel = window.kernel
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
commands: {
|
|
157
|
+
add(params) {
|
|
158
|
+
const { a, b } = params
|
|
159
|
+
this.result = a + b
|
|
160
|
+
this.kernel.emit('calculator:result', { result: this.result })
|
|
161
|
+
return { result: this.result }
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
multiply(params) {
|
|
165
|
+
const { a, b } = params
|
|
166
|
+
this.result = a * b
|
|
167
|
+
this.kernel.emit('calculator:result', { result: this.result })
|
|
168
|
+
return { result: this.result }
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
getResult(params) {
|
|
172
|
+
return { result: this.result }
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
events: ['calculator:result'],
|
|
177
|
+
|
|
178
|
+
getUI() {
|
|
179
|
+
return `
|
|
180
|
+
<div id="calculator-panel">
|
|
181
|
+
<input id="calc-input" type="text" value="0" readonly>
|
|
182
|
+
<button onclick="kernel.exec('calculator.add', {a: 5, b: 3})">5+3</button>
|
|
183
|
+
<button onclick="kernel.exec('calculator.multiply', {a: 5, b: 3})">5×3</button>
|
|
184
|
+
</div>
|
|
185
|
+
`
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Module Best Practices
|
|
191
|
+
|
|
192
|
+
1. **Single Responsibility**: One module = one feature (sketch, modeling, etc.)
|
|
193
|
+
2. **Declare Dependencies**: List modules you depend on
|
|
194
|
+
3. **Emit Events**: Let other modules react to your actions
|
|
195
|
+
4. **No Direct Imports**: Don't import other modules directly; use kernel.exec()
|
|
196
|
+
5. **Clean UI**: getUI() should return clean HTML, no inline styles
|
|
197
|
+
6. **Error Handling**: Throw descriptive errors
|
|
198
|
+
7. **Type Checking**: Validate params in commands
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## Event System
|
|
203
|
+
|
|
204
|
+
### Event Categories
|
|
205
|
+
|
|
206
|
+
cycleCAD uses 30+ core events. Modules can publish custom events.
|
|
207
|
+
|
|
208
|
+
#### Model Events
|
|
209
|
+
```javascript
|
|
210
|
+
kernel.on('model:created', (payload) => { ... })
|
|
211
|
+
kernel.on('model:modified', (payload) => { ... })
|
|
212
|
+
kernel.on('model:deleted', (payload) => { ... })
|
|
213
|
+
|
|
214
|
+
// Payload example
|
|
215
|
+
{
|
|
216
|
+
modelId: 'abc123',
|
|
217
|
+
timestamp: 1234567890,
|
|
218
|
+
delta: { property: 'oldValue' → 'newValue' }
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
#### Geometry Events
|
|
223
|
+
```javascript
|
|
224
|
+
kernel.on('geometry:changed', (payload) => { ... })
|
|
225
|
+
kernel.on('mesh:updated', (payload) => { ... })
|
|
226
|
+
|
|
227
|
+
// Payload
|
|
228
|
+
{
|
|
229
|
+
meshId: 'mesh-0',
|
|
230
|
+
vertices: [...],
|
|
231
|
+
faces: [...],
|
|
232
|
+
bbox: { min, max }
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
#### UI Events
|
|
237
|
+
```javascript
|
|
238
|
+
kernel.on('ui:panel-opened', (payload) => { ... })
|
|
239
|
+
kernel.on('ui:selection-changed', (payload) => { ... })
|
|
240
|
+
|
|
241
|
+
// Payload
|
|
242
|
+
{
|
|
243
|
+
panelName: 'properties',
|
|
244
|
+
selectedIds: ['face-0', 'face-1']
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
#### Workspace Events
|
|
249
|
+
```javascript
|
|
250
|
+
kernel.on('workspace:switched', (payload) => { ... })
|
|
251
|
+
|
|
252
|
+
// Payload
|
|
253
|
+
{
|
|
254
|
+
from: 'Design',
|
|
255
|
+
to: 'CAM',
|
|
256
|
+
activeModules: ['cam-pipeline', 'viewport', 'toolbar']
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### Publishing Custom Events
|
|
261
|
+
|
|
262
|
+
```javascript
|
|
263
|
+
// From within a module
|
|
264
|
+
this.kernel.emit('mymodule:action-done', {
|
|
265
|
+
actionId: 'cut-001',
|
|
266
|
+
duration: 1200, // milliseconds
|
|
267
|
+
result: {...}
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
// Listen from another module
|
|
271
|
+
kernel.on('mymodule:action-done', (payload) => {
|
|
272
|
+
console.log('Action completed:', payload.actionId)
|
|
273
|
+
})
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### Event Best Practices
|
|
277
|
+
|
|
278
|
+
1. **Namespace events**: `modulename:action` (e.g., `cam:toolpath-generated`)
|
|
279
|
+
2. **Include metadata**: Always send timestamp, IDs, and relevant data
|
|
280
|
+
3. **Fire after state change**: Event = notification of completed action
|
|
281
|
+
4. **Don't fire on every frame**: Only on meaningful changes
|
|
282
|
+
5. **Handle async carefully**: Emit after async completes (promises)
|
|
283
|
+
|
|
284
|
+
---
|
|
285
|
+
|
|
286
|
+
## Command API
|
|
287
|
+
|
|
288
|
+
### 55 Core Commands
|
|
289
|
+
|
|
290
|
+
Commands are functions exposed through the kernel. Any module can call any command.
|
|
291
|
+
|
|
292
|
+
#### Shape Commands (10)
|
|
293
|
+
```javascript
|
|
294
|
+
kernel.exec('shape.cylinder', { d: 50, h: 80 })
|
|
295
|
+
kernel.exec('shape.box', { w: 100, d: 60, h: 40 })
|
|
296
|
+
kernel.exec('shape.sphere', { r: 50 })
|
|
297
|
+
kernel.exec('shape.cone', { d: 50, h: 100 })
|
|
298
|
+
kernel.exec('shape.torus', { majorR: 50, minorR: 10 })
|
|
299
|
+
kernel.exec('shape.wedge', { w: 100, h: 50, d: 30 })
|
|
300
|
+
kernel.exec('shape.extrude', { sketchId: 'sk-0', depth: 50 })
|
|
301
|
+
kernel.exec('shape.revolve', { sketchId: 'sk-0', axis: [0,0,1], angle: 360 })
|
|
302
|
+
kernel.exec('shape.sweep', { profileId: 'prof', pathId: 'path' })
|
|
303
|
+
kernel.exec('shape.loft', { profileIds: ['p0', 'p1', 'p2'] })
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
#### Feature Commands (10)
|
|
307
|
+
```javascript
|
|
308
|
+
kernel.exec('feature.fillet', { edges: [0, 1, 2], radius: 5 })
|
|
309
|
+
kernel.exec('feature.chamfer', { edges: [0, 1], distance: 2 })
|
|
310
|
+
kernel.exec('feature.pattern', { type: 'rectangular', x: 3, y: 2, spacing: 50 })
|
|
311
|
+
kernel.exec('feature.mirror', { body: 'body-0', plane: 'XZ' })
|
|
312
|
+
kernel.exec('feature.shell', { thickness: 2 })
|
|
313
|
+
kernel.exec('feature.draft', { angle: 5 })
|
|
314
|
+
kernel.exec('feature.thread', { pitch: 1.75, type: 'ISO' })
|
|
315
|
+
kernel.exec('feature.split', { tool: 'plane-0' })
|
|
316
|
+
kernel.exec('feature.wrap', { surface: 'surf-0' })
|
|
317
|
+
kernel.exec('feature.scale', { factor: 1.5 })
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
#### Assembly Commands (8)
|
|
321
|
+
```javascript
|
|
322
|
+
kernel.exec('assembly.addComponent', { file: 'part.step', position: [0,0,0] })
|
|
323
|
+
kernel.exec('assembly.createJoint', { type: 'mate', body1: 'b1', body2: 'b2', face1: 'f1', face2: 'f2' })
|
|
324
|
+
kernel.exec('assembly.explode', { mode: 'radial' })
|
|
325
|
+
kernel.exec('assembly.generateBOM', { })
|
|
326
|
+
kernel.exec('assembly.checkInterference', { })
|
|
327
|
+
kernel.exec('assembly.drive', { jointId: 'j0', value: 0.5 })
|
|
328
|
+
kernel.exec('assembly.pattern', { type: 'rectangular', count: 4 })
|
|
329
|
+
kernel.exec('assembly.hideComponent', { componentId: 'c-0' })
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
#### Export Commands (8)
|
|
333
|
+
```javascript
|
|
334
|
+
kernel.exec('export.stl', { format: 'binary' })
|
|
335
|
+
kernel.exec('export.step', { })
|
|
336
|
+
kernel.exec('export.gltf', { includeAnimation: true })
|
|
337
|
+
kernel.exec('export.obj', { })
|
|
338
|
+
kernel.exec('export.pdf', { paperSize: 'A4', scale: 1 })
|
|
339
|
+
kernel.exec('export.dxf', { })
|
|
340
|
+
kernel.exec('export.json', { })
|
|
341
|
+
kernel.exec('export.png', { width: 1920, height: 1080, dpi: 300 })
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
#### Import Commands (6)
|
|
345
|
+
```javascript
|
|
346
|
+
kernel.exec('import.step', { file: blob })
|
|
347
|
+
kernel.exec('import.iges', { file: blob })
|
|
348
|
+
kernel.exec('import.obj', { file: blob })
|
|
349
|
+
kernel.exec('import.stl', { file: blob })
|
|
350
|
+
kernel.exec('import.stp', { file: blob })
|
|
351
|
+
kernel.exec('import.ipt', { file: blob })
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
#### Validation Commands (6)
|
|
355
|
+
```javascript
|
|
356
|
+
kernel.exec('validate.designReview', { })
|
|
357
|
+
kernel.exec('validate.checkInterference', { })
|
|
358
|
+
kernel.exec('validate.estimateCost', { material: 'aluminum' })
|
|
359
|
+
kernel.exec('validate.estimateWeight', { })
|
|
360
|
+
kernel.exec('validate.manufacturability', { process: 'cnc' })
|
|
361
|
+
kernel.exec('validate.fea', { type: 'static' })
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
#### AI Commands (5)
|
|
365
|
+
```javascript
|
|
366
|
+
kernel.exec('ai.textToCAD', { prompt: 'socket head bolt M10' })
|
|
367
|
+
kernel.exec('ai.identifyPart', { image: blob })
|
|
368
|
+
kernel.exec('ai.suggestFeature', { context: 'current model' })
|
|
369
|
+
kernel.exec('ai.generateDesign', { description: '...' })
|
|
370
|
+
kernel.exec('ai.designReview', { })
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
#### Render Commands (4)
|
|
374
|
+
```javascript
|
|
375
|
+
kernel.exec('render.snapshot', { width: 1920, height: 1080 })
|
|
376
|
+
kernel.exec('render.multiview', { views: ['front', 'top', 'iso'] })
|
|
377
|
+
kernel.exec('render.fitToObject', { })
|
|
378
|
+
kernel.exec('render.setMaterial', { material: 'steel' })
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
#### Workspace Commands (3)
|
|
382
|
+
```javascript
|
|
383
|
+
kernel.exec('workspace.switch', { name: 'CAM' })
|
|
384
|
+
kernel.exec('workspace.getActive', { })
|
|
385
|
+
kernel.exec('workspace.listWorkspaces', { })
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
#### File Commands (3)
|
|
389
|
+
```javascript
|
|
390
|
+
kernel.exec('file.save', { path: '/models/bracket.ccad' })
|
|
391
|
+
kernel.exec('file.load', { path: '/models/bracket.ccad' })
|
|
392
|
+
kernel.exec('file.new', { })
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
### Command Error Handling
|
|
396
|
+
|
|
397
|
+
```javascript
|
|
398
|
+
// Commands throw on error
|
|
399
|
+
try {
|
|
400
|
+
const result = await kernel.exec('shape.cylinder', { d: -50 }) // Invalid!
|
|
401
|
+
} catch (error) {
|
|
402
|
+
console.error('Error:', error.message)
|
|
403
|
+
// "Invalid diameter: must be positive"
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Check result
|
|
407
|
+
const result = await kernel.exec('shape.cylinder', { d: 50 })
|
|
408
|
+
if (result.success) {
|
|
409
|
+
console.log('Geometry ID:', result.geometryId)
|
|
410
|
+
} else {
|
|
411
|
+
console.error('Failed:', result.error)
|
|
412
|
+
}
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
---
|
|
416
|
+
|
|
417
|
+
## Plugin Development
|
|
418
|
+
|
|
419
|
+
### What is a Plugin?
|
|
420
|
+
|
|
421
|
+
Plugin = JavaScript module that extends cycleCAD with custom features.
|
|
422
|
+
|
|
423
|
+
**Examples:**
|
|
424
|
+
- Custom 3D printer integration (send to Prusa, Ultimaker)
|
|
425
|
+
- CAD library (fasteners, bearings, motors)
|
|
426
|
+
- Manufacturing specific tools (waterjet cutting, injection molding)
|
|
427
|
+
- Domain-specific features (architectural design, furniture)
|
|
428
|
+
|
|
429
|
+
### Plugin Structure
|
|
430
|
+
|
|
431
|
+
```
|
|
432
|
+
my-plugin/
|
|
433
|
+
├── manifest.json # Plugin metadata
|
|
434
|
+
├── index.js # Entry point
|
|
435
|
+
├── modules/
|
|
436
|
+
│ ├── feature1.js # Custom module
|
|
437
|
+
│ └── feature2.js
|
|
438
|
+
├── assets/
|
|
439
|
+
│ ├── icons/
|
|
440
|
+
│ └── models/
|
|
441
|
+
└── README.md
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
### Manifest Format
|
|
445
|
+
|
|
446
|
+
```json
|
|
447
|
+
{
|
|
448
|
+
"id": "com.example.myplugin",
|
|
449
|
+
"name": "My Plugin",
|
|
450
|
+
"version": "1.0.0",
|
|
451
|
+
"description": "Custom features for cycleCAD",
|
|
452
|
+
"author": "Your Name",
|
|
453
|
+
"license": "MIT",
|
|
454
|
+
"cyclecadVersion": ">=3.2.0",
|
|
455
|
+
"modules": [
|
|
456
|
+
"modules/feature1.js",
|
|
457
|
+
"modules/feature2.js"
|
|
458
|
+
],
|
|
459
|
+
"permissions": [
|
|
460
|
+
"geometry.read",
|
|
461
|
+
"geometry.write",
|
|
462
|
+
"ui.panel",
|
|
463
|
+
"storage.local",
|
|
464
|
+
"network.request"
|
|
465
|
+
]
|
|
466
|
+
}
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
### Plugin Entry Point
|
|
470
|
+
|
|
471
|
+
```javascript
|
|
472
|
+
// index.js
|
|
473
|
+
export default {
|
|
474
|
+
async init(kernel) {
|
|
475
|
+
// kernel = the cycleCAD kernel
|
|
476
|
+
|
|
477
|
+
// Register modules
|
|
478
|
+
const Feature1 = (await import('./modules/feature1.js')).default
|
|
479
|
+
kernel.register(Feature1)
|
|
480
|
+
|
|
481
|
+
// Listen to events
|
|
482
|
+
kernel.on('model:created', (payload) => {
|
|
483
|
+
console.log('Model created:', payload.modelId)
|
|
484
|
+
})
|
|
485
|
+
|
|
486
|
+
// Add toolbar button
|
|
487
|
+
const toolbar = document.querySelector('#toolbar')
|
|
488
|
+
const btn = document.createElement('button')
|
|
489
|
+
btn.textContent = 'My Plugin'
|
|
490
|
+
btn.onclick = () => kernel.exec('myplugin.action', {})
|
|
491
|
+
toolbar.appendChild(btn)
|
|
492
|
+
|
|
493
|
+
return {
|
|
494
|
+
name: 'My Plugin',
|
|
495
|
+
version: '1.0.0',
|
|
496
|
+
uninstall() {
|
|
497
|
+
// Cleanup
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
### Installing Plugins
|
|
505
|
+
|
|
506
|
+
```javascript
|
|
507
|
+
// From user action or programmatic
|
|
508
|
+
const pluginUrl = '/plugins/my-plugin/index.js'
|
|
509
|
+
const module = await import(pluginUrl)
|
|
510
|
+
const plugin = await module.default.init(kernel)
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
### Plugin Best Practices
|
|
514
|
+
|
|
515
|
+
1. **Small scope**: One feature per plugin
|
|
516
|
+
2. **Use kernel events**: Don't hook directly into DOM
|
|
517
|
+
3. **Error handling**: All commands should handle errors gracefully
|
|
518
|
+
4. **Documentation**: Include README with examples
|
|
519
|
+
5. **Performance**: Async operations where needed
|
|
520
|
+
6. **Permissions**: Request only what you need
|
|
521
|
+
|
|
522
|
+
---
|
|
523
|
+
|
|
524
|
+
## Testing
|
|
525
|
+
|
|
526
|
+
### Unit Testing Modules
|
|
527
|
+
|
|
528
|
+
```javascript
|
|
529
|
+
// test/myfeature.test.js
|
|
530
|
+
import { MyFeatureModule } from '../app/js/modules/myfeature.js'
|
|
531
|
+
|
|
532
|
+
describe('MyFeature Module', () => {
|
|
533
|
+
let kernel, module
|
|
534
|
+
|
|
535
|
+
beforeEach(() => {
|
|
536
|
+
kernel = createMockKernel()
|
|
537
|
+
module = MyFeatureModule
|
|
538
|
+
module.kernel = kernel
|
|
539
|
+
module.init()
|
|
540
|
+
})
|
|
541
|
+
|
|
542
|
+
test('myCommand returns correct result', () => {
|
|
543
|
+
const result = module.commands.myCommand({ value: 42 })
|
|
544
|
+
expect(result).toEqual({ success: true, value: 42 })
|
|
545
|
+
})
|
|
546
|
+
|
|
547
|
+
test('emits event on action', () => {
|
|
548
|
+
const handler = jest.fn()
|
|
549
|
+
kernel.on('myfeature:action-done', handler)
|
|
550
|
+
|
|
551
|
+
module.commands.doAction({ })
|
|
552
|
+
|
|
553
|
+
expect(handler).toHaveBeenCalled()
|
|
554
|
+
})
|
|
555
|
+
})
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
### Integration Testing
|
|
559
|
+
|
|
560
|
+
```javascript
|
|
561
|
+
// test/integration.test.js
|
|
562
|
+
describe('Sketch → Extrude → Fillet Workflow', () => {
|
|
563
|
+
let kernel
|
|
564
|
+
|
|
565
|
+
beforeEach(async () => {
|
|
566
|
+
kernel = await initializeCycleCAD()
|
|
567
|
+
kernel.activate('Design')
|
|
568
|
+
})
|
|
569
|
+
|
|
570
|
+
test('Can create a filleted box', async () => {
|
|
571
|
+
// Create sketch
|
|
572
|
+
await kernel.exec('sketch.create', { plane: 'XY' })
|
|
573
|
+
await kernel.exec('sketch.rectangle', { w: 100, h: 50 })
|
|
574
|
+
|
|
575
|
+
// Extrude
|
|
576
|
+
const extrudeResult = await kernel.exec('shape.extrude', {
|
|
577
|
+
sketchId: 'sketch-0',
|
|
578
|
+
depth: 50
|
|
579
|
+
})
|
|
580
|
+
|
|
581
|
+
// Fillet
|
|
582
|
+
const filletResult = await kernel.exec('feature.fillet', {
|
|
583
|
+
edges: [0, 1, 2, 3],
|
|
584
|
+
radius: 5
|
|
585
|
+
})
|
|
586
|
+
|
|
587
|
+
// Verify
|
|
588
|
+
expect(filletResult.success).toBe(true)
|
|
589
|
+
expect(filletResult.geometryId).toBeDefined()
|
|
590
|
+
})
|
|
591
|
+
})
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
### E2E Testing with Playwright
|
|
595
|
+
|
|
596
|
+
```javascript
|
|
597
|
+
// test/e2e.test.js
|
|
598
|
+
import { test, expect } from '@playwright/test'
|
|
599
|
+
|
|
600
|
+
test('Create and export STL', async ({ page }) => {
|
|
601
|
+
await page.goto('http://localhost:8080')
|
|
602
|
+
|
|
603
|
+
// Click Design workspace
|
|
604
|
+
await page.click('[data-workspace="Design"]')
|
|
605
|
+
|
|
606
|
+
// Create sketch
|
|
607
|
+
await page.click('[id="sketch-btn"]')
|
|
608
|
+
await page.click('[data-plane="XY"]')
|
|
609
|
+
|
|
610
|
+
// Draw rectangle
|
|
611
|
+
await page.click('[id="rect-tool"]')
|
|
612
|
+
await page.mouse.click(100, 100)
|
|
613
|
+
await page.mouse.click(200, 150)
|
|
614
|
+
|
|
615
|
+
// Extrude
|
|
616
|
+
await page.click('[id="extrude-btn"]')
|
|
617
|
+
await page.fill('[id="extrude-depth"]', '50')
|
|
618
|
+
await page.click('[id="ok-btn"]')
|
|
619
|
+
|
|
620
|
+
// Export
|
|
621
|
+
await page.click('[id="export-btn"]')
|
|
622
|
+
await page.selectOption('[id="export-format"]', 'stl')
|
|
623
|
+
|
|
624
|
+
// Verify download
|
|
625
|
+
const downloadPromise = page.waitForEvent('download')
|
|
626
|
+
await page.click('[id="export-confirm"]')
|
|
627
|
+
const download = await downloadPromise
|
|
628
|
+
|
|
629
|
+
expect(download.suggestedFilename()).toBe('model.stl')
|
|
630
|
+
})
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
---
|
|
634
|
+
|
|
635
|
+
## Contributing
|
|
636
|
+
|
|
637
|
+
### Code Style
|
|
638
|
+
|
|
639
|
+
- **Language**: ES6+ JavaScript (no TypeScript)
|
|
640
|
+
- **Formatting**: 2 spaces, no semicolons (optional)
|
|
641
|
+
- **Comments**: JSDoc for public APIs
|
|
642
|
+
- **Naming**: camelCase for functions/vars, PascalCase for classes
|
|
643
|
+
- **Linting**: ESLint (see `.eslintrc.json`)
|
|
644
|
+
|
|
645
|
+
### Git Workflow
|
|
646
|
+
|
|
647
|
+
1. **Fork** the repository
|
|
648
|
+
2. **Create branch**: `feature/my-feature` or `fix/my-bug`
|
|
649
|
+
3. **Write tests**: Every feature needs tests
|
|
650
|
+
4. **Commit**: Descriptive messages ("Add fillet radius editor")
|
|
651
|
+
5. **Push**: To your fork
|
|
652
|
+
6. **PR**: Describe what you're adding/fixing
|
|
653
|
+
|
|
654
|
+
### PR Checklist
|
|
655
|
+
|
|
656
|
+
- [ ] Tests added/updated
|
|
657
|
+
- [ ] Documentation updated
|
|
658
|
+
- [ ] No linting errors (`npm run lint`)
|
|
659
|
+
- [ ] All tests pass (`npm test`)
|
|
660
|
+
- [ ] Follows code style guide
|
|
661
|
+
- [ ] Feature complete (not WIP)
|
|
662
|
+
- [ ] No breaking changes documented
|
|
663
|
+
|
|
664
|
+
### Commit Message Format
|
|
665
|
+
|
|
666
|
+
```
|
|
667
|
+
type(scope): description
|
|
668
|
+
|
|
669
|
+
body (optional)
|
|
670
|
+
footer (optional)
|
|
671
|
+
|
|
672
|
+
// Examples:
|
|
673
|
+
feat(sketch): add tangent constraint (O key)
|
|
674
|
+
fix(viewport): correct mouse offset by 32px
|
|
675
|
+
refactor(kernel): simplify event bus initialization
|
|
676
|
+
docs(tutorial): add sweep and loft examples
|
|
677
|
+
test(assembly): add interference detection tests
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
Types: `feat`, `fix`, `refactor`, `docs`, `test`, `perf`, `build`
|
|
681
|
+
|
|
682
|
+
### Issue Reporting
|
|
683
|
+
|
|
684
|
+
Include:
|
|
685
|
+
- Minimal reproduction steps
|
|
686
|
+
- Expected vs actual behavior
|
|
687
|
+
- Browser/OS version
|
|
688
|
+
- Browser console errors
|
|
689
|
+
- Screenshots (for UI issues)
|
|
690
|
+
|
|
691
|
+
### Feature Requests
|
|
692
|
+
|
|
693
|
+
Include:
|
|
694
|
+
- Use case description
|
|
695
|
+
- Mockup or example (if applicable)
|
|
696
|
+
- Priority (nice-to-have vs critical)
|
|
697
|
+
- Suggested implementation (if applicable)
|
|
698
|
+
|
|
699
|
+
---
|
|
700
|
+
|
|
701
|
+
## Advanced Topics
|
|
702
|
+
|
|
703
|
+
### Hot Module Reloading
|
|
704
|
+
|
|
705
|
+
```javascript
|
|
706
|
+
// In development, reload module without restarting
|
|
707
|
+
if (module.hot) {
|
|
708
|
+
module.hot.accept('./mymodule.js', () => {
|
|
709
|
+
kernel.unload('myfeature')
|
|
710
|
+
const updated = require('./mymodule.js')
|
|
711
|
+
kernel.register(updated)
|
|
712
|
+
kernel.activate('Design')
|
|
713
|
+
})
|
|
714
|
+
}
|
|
715
|
+
```
|
|
716
|
+
|
|
717
|
+
### State Management (Advanced)
|
|
718
|
+
|
|
719
|
+
```javascript
|
|
720
|
+
// Get full state
|
|
721
|
+
const state = kernel.state.getAll()
|
|
722
|
+
|
|
723
|
+
// Observe state changes
|
|
724
|
+
kernel.state.watch('activeModel', (oldVal, newVal) => {
|
|
725
|
+
console.log('Model changed:', oldVal, '→', newVal)
|
|
726
|
+
})
|
|
727
|
+
|
|
728
|
+
// Batch updates
|
|
729
|
+
kernel.state.beginBatch()
|
|
730
|
+
kernel.state.set('a', 1)
|
|
731
|
+
kernel.state.set('b', 2)
|
|
732
|
+
kernel.state.endBatch() // Single re-render
|
|
733
|
+
```
|
|
734
|
+
|
|
735
|
+
### Performance Optimization
|
|
736
|
+
|
|
737
|
+
```javascript
|
|
738
|
+
// Defer heavy computation
|
|
739
|
+
const result = await kernel.exec('feature.fillet', {
|
|
740
|
+
edges: largeArray,
|
|
741
|
+
async: true, // Non-blocking
|
|
742
|
+
onProgress: (progress) => {
|
|
743
|
+
console.log('Fillet progress:', progress)
|
|
744
|
+
}
|
|
745
|
+
})
|
|
746
|
+
|
|
747
|
+
// Use Web Workers for CAM/FEA
|
|
748
|
+
const worker = new Worker('/workers/cam-worker.js')
|
|
749
|
+
worker.postMessage({ toolpath: paths })
|
|
750
|
+
worker.onmessage = (e) => {
|
|
751
|
+
const gcode = e.data
|
|
752
|
+
}
|
|
753
|
+
```
|
|
754
|
+
|
|
755
|
+
---
|
|
756
|
+
|
|
757
|
+
## Troubleshooting
|
|
758
|
+
|
|
759
|
+
### Module not found
|
|
760
|
+
- Check manifest.json includes the module
|
|
761
|
+
- Verify file path is correct
|
|
762
|
+
- Check browser console for import errors
|
|
763
|
+
|
|
764
|
+
### Command fails with "not found"
|
|
765
|
+
- Module may not be activated for current workspace
|
|
766
|
+
- Check kernel.state.get('activeModules')
|
|
767
|
+
- Try switching workspaces
|
|
768
|
+
|
|
769
|
+
### Events not firing
|
|
770
|
+
- Make sure event name matches (case-sensitive)
|
|
771
|
+
- Check module is initialized (kernel.init())
|
|
772
|
+
- Verify listener added before event fires
|
|
773
|
+
|
|
774
|
+
### Memory leaks
|
|
775
|
+
- Always unsubscribe from events in deactivate()
|
|
776
|
+
- Clean up DOM elements in deactivate()
|
|
777
|
+
- Close connections (WebSocket, fetch) properly
|
|
778
|
+
|
|
779
|
+
---
|
|
780
|
+
|
|
781
|
+
## Useful Resources
|
|
782
|
+
|
|
783
|
+
- **Full API Docs**: `/docs/API-REFERENCE.md`
|
|
784
|
+
- **Architecture**: `/docs/architecture-v3.html`
|
|
785
|
+
- **Examples**: `/examples/` directory
|
|
786
|
+
- **Discord**: Community help
|
|
787
|
+
- **GitHub Issues**: Bug reports, feature requests
|
|
788
|
+
|
|
789
|
+
---
|
|
790
|
+
|
|
791
|
+
## License
|
|
792
|
+
|
|
793
|
+
cycleCAD is MIT licensed. Plugins can use any OSS license.
|
|
794
|
+
|
|
795
|
+
**Happy coding!**
|