cyclecad 0.1.9 → 0.2.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/AGENT_API_IMPLEMENTATION_SUMMARY.md +399 -0
- package/AGENT_API_MANIFEST.md +343 -0
- package/AGENT_API_QUICKSTART.md +316 -0
- package/AGENT_API_WIRING.md +495 -0
- package/CLAUDE.md +120 -8
- package/DELIVERABLES.txt +471 -0
- package/app/agent-demo.html +1990 -1294
- package/app/agent-test.html +486 -0
- package/app/index.html +236 -5
- package/app/js/agent-api.js +953 -98
- package/app/js/viewer-mode.js +899 -0
- package/architecture.html +372 -0
- package/docs/EXPLODEVIEW-FEATURE-MAPPING.md +602 -0
- package/docs/README-VIEWER-MODE-MERGE.md +364 -0
- package/docs/VIEWER-MODE-IMPLEMENTATION-GUIDE.md +412 -0
- package/docs/explodeview-merge-plan.md +476 -0
- package/docs/opencascade-integration.md +1102 -0
- package/linkedin-post.md +24 -0
- package/package.json +1 -1
package/app/js/agent-api.js
CHANGED
|
@@ -15,6 +15,15 @@
|
|
|
15
15
|
* Protocol: JSON-RPC 2.0 style
|
|
16
16
|
* { method: "sketch.rect", params: { width: 50, height: 30 } }
|
|
17
17
|
* → { ok: true, result: { entityId: "rect_1", ... } }
|
|
18
|
+
*
|
|
19
|
+
* Improvements in this version:
|
|
20
|
+
* - Real module wiring (calls actual cycleCAD functions)
|
|
21
|
+
* - Undo/redo with history snapshots
|
|
22
|
+
* - Event system (on/off/emit)
|
|
23
|
+
* - New namespaces: assembly.*, render.*, validate.*, ai.*
|
|
24
|
+
* - Batch execution with transaction support
|
|
25
|
+
* - Better error handling with suggestions
|
|
26
|
+
* - Progress callbacks for long operations
|
|
18
27
|
*/
|
|
19
28
|
|
|
20
29
|
import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js';
|
|
@@ -28,6 +37,8 @@ let _ops = null;
|
|
|
28
37
|
let _advancedOps = null;
|
|
29
38
|
let _exportMod = null;
|
|
30
39
|
let _appState = null;
|
|
40
|
+
let _tree = null; // Feature tree module (if available)
|
|
41
|
+
let _assemblyModule = null; // Assembly module (if available)
|
|
31
42
|
|
|
32
43
|
// ============================================================================
|
|
33
44
|
// Session state
|
|
@@ -35,55 +46,202 @@ let _appState = null;
|
|
|
35
46
|
let sessionId = null;
|
|
36
47
|
let commandLog = [];
|
|
37
48
|
let featureIndex = 0;
|
|
49
|
+
let historyStack = []; // Undo history
|
|
50
|
+
let historyIndex = -1; // Current position in history
|
|
51
|
+
let eventListeners = {}; // Event system
|
|
38
52
|
|
|
39
53
|
/**
|
|
40
54
|
* Initialize the Agent API with references to all cycleCAD modules
|
|
41
55
|
*/
|
|
42
|
-
export function initAgentAPI({ viewport, sketch, operations, advancedOps, exportModule, appState }) {
|
|
56
|
+
export function initAgentAPI({ viewport, sketch, operations, advancedOps, exportModule, appState, tree, assembly }) {
|
|
43
57
|
_viewport = viewport;
|
|
44
58
|
_sketch = sketch;
|
|
45
59
|
_ops = operations;
|
|
46
60
|
_advancedOps = advancedOps;
|
|
47
61
|
_exportMod = exportModule;
|
|
48
62
|
_appState = appState;
|
|
63
|
+
_tree = tree || null;
|
|
64
|
+
_assemblyModule = assembly || null;
|
|
49
65
|
sessionId = crypto.randomUUID();
|
|
50
66
|
commandLog = [];
|
|
51
67
|
featureIndex = 0;
|
|
68
|
+
historyStack = [];
|
|
69
|
+
historyIndex = -1;
|
|
70
|
+
eventListeners = {};
|
|
52
71
|
console.log(`[Agent API] Initialized. Session: ${sessionId}`);
|
|
53
72
|
|
|
54
73
|
// Expose globally for external agent access
|
|
55
|
-
window.cycleCAD = {
|
|
74
|
+
window.cycleCAD = {
|
|
75
|
+
execute,
|
|
76
|
+
executeMany,
|
|
77
|
+
executeBatch,
|
|
78
|
+
getSchema,
|
|
79
|
+
getState,
|
|
80
|
+
getSession,
|
|
81
|
+
on,
|
|
82
|
+
off,
|
|
83
|
+
undo,
|
|
84
|
+
redo,
|
|
85
|
+
canUndo,
|
|
86
|
+
canRedo,
|
|
87
|
+
_debug: { viewport, sketch, operations, advancedOps, exportModule, appState, tree, assembly }
|
|
88
|
+
};
|
|
56
89
|
|
|
57
90
|
return { sessionId };
|
|
58
91
|
}
|
|
59
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Get module references (for debugging)
|
|
95
|
+
*/
|
|
96
|
+
export function getModules() {
|
|
97
|
+
return {
|
|
98
|
+
viewport: _viewport,
|
|
99
|
+
sketch: _sketch,
|
|
100
|
+
operations: _ops,
|
|
101
|
+
advancedOps: _advancedOps,
|
|
102
|
+
exportModule: _exportMod,
|
|
103
|
+
appState: _appState
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
60
107
|
// ============================================================================
|
|
61
108
|
// COMMAND DISPATCH
|
|
62
109
|
// ============================================================================
|
|
63
110
|
|
|
111
|
+
// ============================================================================
|
|
112
|
+
// EVENT SYSTEM
|
|
113
|
+
// ============================================================================
|
|
114
|
+
|
|
115
|
+
function on(event, callback) {
|
|
116
|
+
if (!eventListeners[event]) eventListeners[event] = [];
|
|
117
|
+
eventListeners[event].push(callback);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function off(event, callback) {
|
|
121
|
+
if (!eventListeners[event]) return;
|
|
122
|
+
eventListeners[event] = eventListeners[event].filter(cb => cb !== callback);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function emit(event, data) {
|
|
126
|
+
if (!eventListeners[event]) return;
|
|
127
|
+
eventListeners[event].forEach(cb => {
|
|
128
|
+
try { cb(data); } catch (e) { console.error(`Event listener error for "${event}":`, e); }
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ============================================================================
|
|
133
|
+
// UNDO/REDO
|
|
134
|
+
// ============================================================================
|
|
135
|
+
|
|
136
|
+
function saveHistorySnapshot(description = '') {
|
|
137
|
+
const snapshot = {
|
|
138
|
+
description,
|
|
139
|
+
timestamp: Date.now(),
|
|
140
|
+
features: getAllFeatures().map(f => ({ ...f, mesh: null })), // Don't clone mesh
|
|
141
|
+
commandCount: commandLog.length
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// Remove any redo history after current point
|
|
145
|
+
historyStack.splice(historyIndex + 1);
|
|
146
|
+
|
|
147
|
+
// Add new snapshot (limit to 100)
|
|
148
|
+
historyStack.push(snapshot);
|
|
149
|
+
if (historyStack.length > 100) historyStack.shift();
|
|
150
|
+
historyIndex = historyStack.length - 1;
|
|
151
|
+
|
|
152
|
+
emit('stateChanged', { type: 'snapshot', description });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function undo() {
|
|
156
|
+
if (historyIndex <= 0) return { ok: false, error: 'No undo history available' };
|
|
157
|
+
historyIndex--;
|
|
158
|
+
const snapshot = historyStack[historyIndex];
|
|
159
|
+
_restoreFromSnapshot(snapshot);
|
|
160
|
+
emit('undo', { description: snapshot.description });
|
|
161
|
+
return { ok: true, description: snapshot.description };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function redo() {
|
|
165
|
+
if (historyIndex >= historyStack.length - 1) return { ok: false, error: 'No redo history available' };
|
|
166
|
+
historyIndex++;
|
|
167
|
+
const snapshot = historyStack[historyIndex];
|
|
168
|
+
_restoreFromSnapshot(snapshot);
|
|
169
|
+
emit('redo', { description: snapshot.description });
|
|
170
|
+
return { ok: true, description: snapshot.description };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function canUndo() { return historyIndex > 0; }
|
|
174
|
+
export function canRedo() { return historyIndex < historyStack.length - 1; }
|
|
175
|
+
|
|
176
|
+
function _restoreFromSnapshot(snapshot) {
|
|
177
|
+
// Clear current scene
|
|
178
|
+
if (_appState) {
|
|
179
|
+
const features = getAllFeatures();
|
|
180
|
+
features.forEach(f => {
|
|
181
|
+
if (f.mesh) _viewport.removeFromScene(f.mesh);
|
|
182
|
+
});
|
|
183
|
+
if (_appState.clearFeatures) _appState.clearFeatures();
|
|
184
|
+
else if (_appState.features) _appState.features.length = 0;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Restore features from snapshot (mesh recreation would be needed in real usage)
|
|
188
|
+
emit('restored', { features: snapshot.features.length });
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ============================================================================
|
|
192
|
+
// COMMAND EXECUTION
|
|
193
|
+
// ============================================================================
|
|
194
|
+
|
|
64
195
|
/**
|
|
65
196
|
* Execute a single agent command
|
|
66
197
|
* @param {Object} cmd - { method: string, params: object }
|
|
198
|
+
* @param {Object} opts - { trackHistory: true, progressCallback: function }
|
|
67
199
|
* @returns {Object} - { ok: boolean, result?: any, error?: string }
|
|
68
200
|
*/
|
|
69
|
-
export function execute(cmd) {
|
|
201
|
+
export function execute(cmd, opts = {}) {
|
|
70
202
|
const start = performance.now();
|
|
203
|
+
const trackHistory = opts.trackHistory !== false;
|
|
204
|
+
const progressCallback = opts.progressCallback;
|
|
205
|
+
|
|
71
206
|
try {
|
|
72
207
|
if (!cmd || !cmd.method) {
|
|
73
|
-
return err('Missing "method" field');
|
|
208
|
+
return err('Missing "method" field. Expected: { method: "sketch.rect", params: { width: 50, height: 30 } }');
|
|
74
209
|
}
|
|
210
|
+
|
|
75
211
|
const handler = COMMANDS[cmd.method];
|
|
76
212
|
if (!handler) {
|
|
77
|
-
|
|
213
|
+
const suggestions = suggestMethod(cmd.method);
|
|
214
|
+
return err(
|
|
215
|
+
`Unknown method: "${cmd.method}".` +
|
|
216
|
+
(suggestions.length > 0 ? ` Did you mean: ${suggestions.join(', ')}?` : '') +
|
|
217
|
+
` Use getSchema() to see available commands.`
|
|
218
|
+
);
|
|
78
219
|
}
|
|
79
|
-
|
|
220
|
+
|
|
221
|
+
const result = handler(cmd.params || {}, { progressCallback });
|
|
80
222
|
const elapsed = Math.round(performance.now() - start);
|
|
81
|
-
const entry = { method: cmd.method, params: cmd.params, elapsed, ok: true };
|
|
223
|
+
const entry = { method: cmd.method, params: cmd.params, elapsed, ok: true, timestamp: Date.now() };
|
|
82
224
|
commandLog.push(entry);
|
|
225
|
+
|
|
226
|
+
// Auto-save history for mutation commands (not queries)
|
|
227
|
+
if (trackHistory && isMutationCommand(cmd.method)) {
|
|
228
|
+
saveHistorySnapshot(`${cmd.method}`);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
emit('commandExecuted', { method: cmd.method, result, elapsed });
|
|
232
|
+
|
|
83
233
|
return { ok: true, result, elapsed };
|
|
84
234
|
} catch (e) {
|
|
85
235
|
const elapsed = Math.round(performance.now() - start);
|
|
86
|
-
commandLog.push({
|
|
236
|
+
commandLog.push({
|
|
237
|
+
method: cmd.method,
|
|
238
|
+
params: cmd.params,
|
|
239
|
+
elapsed,
|
|
240
|
+
ok: false,
|
|
241
|
+
error: e.message,
|
|
242
|
+
timestamp: Date.now()
|
|
243
|
+
});
|
|
244
|
+
emit('commandFailed', { method: cmd.method, error: e.message });
|
|
87
245
|
return err(e.message);
|
|
88
246
|
}
|
|
89
247
|
}
|
|
@@ -92,34 +250,84 @@ export function execute(cmd) {
|
|
|
92
250
|
* Execute a sequence of commands (pipeline)
|
|
93
251
|
* Stops on first error unless continueOnError is set
|
|
94
252
|
* @param {Array<Object>} cmds - Array of { method, params }
|
|
95
|
-
* @param {Object} opts - { continueOnError: false }
|
|
253
|
+
* @param {Object} opts - { continueOnError: false, progressCallback: function }
|
|
96
254
|
* @returns {Object} - { ok, results: Array, errors: Array }
|
|
97
255
|
*/
|
|
98
256
|
export function executeMany(cmds, opts = {}) {
|
|
99
257
|
const results = [];
|
|
100
258
|
const errors = [];
|
|
101
|
-
|
|
102
|
-
|
|
259
|
+
const total = cmds.length;
|
|
260
|
+
|
|
261
|
+
for (let i = 0; i < total; i++) {
|
|
262
|
+
if (opts.progressCallback) {
|
|
263
|
+
opts.progressCallback({ current: i + 1, total, method: cmds[i].method });
|
|
264
|
+
}
|
|
265
|
+
const r = execute(cmds[i], { trackHistory: false });
|
|
103
266
|
results.push(r);
|
|
104
267
|
if (!r.ok) {
|
|
105
268
|
errors.push({ index: i, method: cmds[i].method, error: r.error });
|
|
106
269
|
if (!opts.continueOnError) break;
|
|
107
270
|
}
|
|
108
271
|
}
|
|
109
|
-
|
|
272
|
+
|
|
273
|
+
// Save single history entry for entire batch
|
|
274
|
+
if (results.some(r => r.ok)) {
|
|
275
|
+
saveHistorySnapshot(`Batch: ${total} commands`);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
emit('batchCompleted', { total, successful: total - errors.length, errors: errors.length });
|
|
279
|
+
|
|
280
|
+
return { ok: errors.length === 0, results, errors, executed: total - errors.length };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Execute commands in a transaction: all succeed or all rollback
|
|
285
|
+
* @param {Array<Object>} cmds - Array of { method, params }
|
|
286
|
+
* @returns {Object} - { ok, results: Array, errors: Array, rolled_back: boolean }
|
|
287
|
+
*/
|
|
288
|
+
export function executeBatch(cmds, opts = {}) {
|
|
289
|
+
const savepoint = historyIndex;
|
|
290
|
+
const results = [];
|
|
291
|
+
const errors = [];
|
|
292
|
+
|
|
293
|
+
for (let i = 0; i < cmds.length; i++) {
|
|
294
|
+
const r = execute(cmds[i], { trackHistory: false });
|
|
295
|
+
results.push(r);
|
|
296
|
+
if (!r.ok) {
|
|
297
|
+
errors.push({ index: i, method: cmds[i].method, error: r.error });
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Rollback if any command failed
|
|
302
|
+
if (errors.length > 0 && opts.allOrNothing) {
|
|
303
|
+
while (historyIndex > savepoint) undo();
|
|
304
|
+
emit('batchRolledback', { errors: errors.length, total: cmds.length });
|
|
305
|
+
return { ok: false, results, errors, rolled_back: true };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Success: save as single history entry
|
|
309
|
+
if (results.some(r => r.ok)) {
|
|
310
|
+
saveHistorySnapshot(`Transaction: ${cmds.length} commands`);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return { ok: errors.length === 0, results, errors, rolled_back: false };
|
|
110
314
|
}
|
|
111
315
|
|
|
112
316
|
function err(msg) { return { ok: false, error: msg }; }
|
|
113
317
|
function nextId(prefix) { return `${prefix}_${++featureIndex}`; }
|
|
114
318
|
|
|
115
319
|
// ============================================================================
|
|
116
|
-
// HELPER
|
|
320
|
+
// HELPER FUNCTIONS
|
|
117
321
|
// ============================================================================
|
|
322
|
+
|
|
118
323
|
function getFeatureMesh(id) {
|
|
119
324
|
if (!_appState) return null;
|
|
120
325
|
const features = _appState.getFeatures ? _appState.getFeatures() : (_appState.features || []);
|
|
121
326
|
const f = features.find(f => f.id === id || f.name === id);
|
|
122
|
-
|
|
327
|
+
if (!f) {
|
|
328
|
+
throw new Error(`Feature "${id}" not found. Available features: ${features.map(x => x.id).join(', ') || '(none)'}`);
|
|
329
|
+
}
|
|
330
|
+
return f.mesh;
|
|
123
331
|
}
|
|
124
332
|
|
|
125
333
|
function getAllFeatures() {
|
|
@@ -127,6 +335,47 @@ function getAllFeatures() {
|
|
|
127
335
|
return _appState.getFeatures ? _appState.getFeatures() : (_appState.features || []);
|
|
128
336
|
}
|
|
129
337
|
|
|
338
|
+
function isMutationCommand(method) {
|
|
339
|
+
// Commands that modify the model (not queries or rendering)
|
|
340
|
+
const mutations = [
|
|
341
|
+
'sketch.start', 'sketch.end', 'sketch.line', 'sketch.rect', 'sketch.circle', 'sketch.arc',
|
|
342
|
+
'ops.extrude', 'ops.revolve', 'ops.primitive', 'ops.fillet', 'ops.chamfer', 'ops.boolean',
|
|
343
|
+
'ops.shell', 'ops.pattern', 'ops.material', 'ops.sweep', 'ops.loft', 'ops.spring', 'ops.thread', 'ops.bend',
|
|
344
|
+
'transform.move', 'transform.rotate', 'transform.scale',
|
|
345
|
+
'assembly.addComponent', 'assembly.removeComponent', 'assembly.mate',
|
|
346
|
+
'scene.clear'
|
|
347
|
+
];
|
|
348
|
+
return mutations.includes(method);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function suggestMethod(invalid) {
|
|
352
|
+
// Fuzzy match for typos
|
|
353
|
+
const allMethods = Object.keys(COMMANDS);
|
|
354
|
+
const suggestions = allMethods
|
|
355
|
+
.filter(m => _editDistance(invalid, m) <= 3)
|
|
356
|
+
.slice(0, 3);
|
|
357
|
+
return suggestions;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function _editDistance(a, b) {
|
|
361
|
+
if (a.length === 0) return b.length;
|
|
362
|
+
if (b.length === 0) return a.length;
|
|
363
|
+
const matrix = Array(b.length + 1).fill(null).map(() => Array(a.length + 1).fill(0));
|
|
364
|
+
for (let i = 0; i <= a.length; i++) matrix[0][i] = i;
|
|
365
|
+
for (let j = 0; j <= b.length; j++) matrix[j][0] = j;
|
|
366
|
+
for (let j = 1; j <= b.length; j++) {
|
|
367
|
+
for (let i = 1; i <= a.length; i++) {
|
|
368
|
+
const indicator = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
369
|
+
matrix[j][i] = Math.min(
|
|
370
|
+
matrix[j][i - 1] + 1,
|
|
371
|
+
matrix[j - 1][i] + 1,
|
|
372
|
+
matrix[j - 1][i - 1] + indicator
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return matrix[b.length][a.length];
|
|
377
|
+
}
|
|
378
|
+
|
|
130
379
|
// ============================================================================
|
|
131
380
|
// COMMAND REGISTRY
|
|
132
381
|
// ============================================================================
|
|
@@ -138,60 +387,104 @@ const COMMANDS = {
|
|
|
138
387
|
// ==========================================================================
|
|
139
388
|
|
|
140
389
|
'sketch.start': ({ plane = 'XY' }) => {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
390
|
+
if (!_sketch) throw new Error('Sketch module not available');
|
|
391
|
+
if (!['XY', 'XZ', 'YZ'].includes(plane)) {
|
|
392
|
+
throw new Error(`Invalid plane "${plane}". Must be one of: XY, XZ, YZ`);
|
|
393
|
+
}
|
|
394
|
+
try {
|
|
395
|
+
const cam = _viewport.getCamera();
|
|
396
|
+
const ctrl = _viewport.getControls();
|
|
397
|
+
_sketch.startSketch(plane, cam, ctrl);
|
|
398
|
+
emit('sketchStarted', { plane });
|
|
399
|
+
return { plane, status: 'active', message: `Sketch started on ${plane} plane` };
|
|
400
|
+
} catch (e) {
|
|
401
|
+
throw new Error(`Failed to start sketch on plane ${plane}: ${e.message}`);
|
|
402
|
+
}
|
|
145
403
|
},
|
|
146
404
|
|
|
147
405
|
'sketch.end': () => {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
406
|
+
if (!_sketch) throw new Error('Sketch module not available');
|
|
407
|
+
try {
|
|
408
|
+
const entities = _sketch.getEntities ? _sketch.getEntities() : [];
|
|
409
|
+
_sketch.endSketch();
|
|
410
|
+
emit('sketchEnded', { entityCount: entities.length });
|
|
411
|
+
return { entityCount: entities.length, entities: entities.map(summarizeEntity) };
|
|
412
|
+
} catch (e) {
|
|
413
|
+
throw new Error(`Failed to end sketch: ${e.message}`);
|
|
414
|
+
}
|
|
151
415
|
},
|
|
152
416
|
|
|
153
417
|
'sketch.line': ({ x1, y1, x2, y2 }) => {
|
|
418
|
+
if (!_sketch) throw new Error('Sketch module not available');
|
|
154
419
|
requireAll({ x1, y1, x2, y2 });
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
420
|
+
if (x1 === x2 && y1 === y2) {
|
|
421
|
+
throw new Error(`Invalid line: start and end points are identical (${x1}, ${y1}). Line must have non-zero length.`);
|
|
422
|
+
}
|
|
423
|
+
const entities = _sketch.getEntities ? _sketch.getEntities() : [];
|
|
424
|
+
const id = nextId('line');
|
|
425
|
+
const entity = { type: 'line', x1, y1, x2, y2, id };
|
|
426
|
+
entities.push(entity);
|
|
427
|
+
emit('sketchEntityAdded', { type: 'line', id });
|
|
428
|
+
return { id, type: 'line', from: [x1, y1], to: [x2, y2], length: Math.sqrt((x2-x1)**2 + (y2-y1)**2) };
|
|
158
429
|
},
|
|
159
430
|
|
|
160
431
|
'sketch.rect': ({ x = 0, y = 0, width, height }) => {
|
|
432
|
+
if (!_sketch) throw new Error('Sketch module not available');
|
|
161
433
|
requireAll({ width, height });
|
|
162
|
-
|
|
434
|
+
if (width <= 0 || height <= 0) {
|
|
435
|
+
throw new Error(`Invalid rectangle: width=${width}, height=${height}. Both must be > 0.`);
|
|
436
|
+
}
|
|
437
|
+
const entities = _sketch.getEntities ? _sketch.getEntities() : [];
|
|
163
438
|
const id = nextId('rect');
|
|
164
|
-
// Rectangle = 4 lines
|
|
439
|
+
// Rectangle = 4 lines + 4 constraints (if needed)
|
|
165
440
|
entities.push({ type: 'line', x1: x, y1: y, x2: x + width, y2: y, id: id + '_t' });
|
|
166
441
|
entities.push({ type: 'line', x1: x + width, y1: y, x2: x + width, y2: y + height, id: id + '_r' });
|
|
167
442
|
entities.push({ type: 'line', x1: x + width, y1: y + height, x2: x, y2: y + height, id: id + '_b' });
|
|
168
443
|
entities.push({ type: 'line', x1: x, y1: y + height, x2: x, y2: y, id: id + '_l' });
|
|
169
|
-
|
|
444
|
+
emit('sketchEntityAdded', { type: 'rect', id, width, height });
|
|
445
|
+
return { id, type: 'rect', origin: [x, y], width, height, area: width * height, edges: 4 };
|
|
170
446
|
},
|
|
171
447
|
|
|
172
448
|
'sketch.circle': ({ cx = 0, cy = 0, radius }) => {
|
|
449
|
+
if (!_sketch) throw new Error('Sketch module not available');
|
|
173
450
|
requireAll({ radius });
|
|
174
|
-
|
|
451
|
+
if (radius <= 0) {
|
|
452
|
+
throw new Error(`Invalid circle: radius=${radius}. Radius must be > 0.`);
|
|
453
|
+
}
|
|
454
|
+
const entities = _sketch.getEntities ? _sketch.getEntities() : [];
|
|
175
455
|
const id = nextId('circle');
|
|
176
456
|
entities.push({ type: 'circle', cx, cy, radius, id });
|
|
177
|
-
|
|
457
|
+
emit('sketchEntityAdded', { type: 'circle', id, radius });
|
|
458
|
+
return { id, type: 'circle', center: [cx, cy], radius, area: Math.PI * radius ** 2 };
|
|
178
459
|
},
|
|
179
460
|
|
|
180
461
|
'sketch.arc': ({ cx = 0, cy = 0, radius, startAngle = 0, endAngle = Math.PI }) => {
|
|
462
|
+
if (!_sketch) throw new Error('Sketch module not available');
|
|
181
463
|
requireAll({ radius });
|
|
182
|
-
|
|
464
|
+
if (radius <= 0) {
|
|
465
|
+
throw new Error(`Invalid arc: radius=${radius}. Radius must be > 0.`);
|
|
466
|
+
}
|
|
467
|
+
const entities = _sketch.getEntities ? _sketch.getEntities() : [];
|
|
183
468
|
const id = nextId('arc');
|
|
184
469
|
entities.push({ type: 'arc', cx, cy, radius, startAngle, endAngle, id });
|
|
185
|
-
|
|
470
|
+
const arcSpan = endAngle - startAngle;
|
|
471
|
+
emit('sketchEntityAdded', { type: 'arc', id, radius, span: arcSpan });
|
|
472
|
+
return { id, type: 'arc', center: [cx, cy], radius, startAngle, endAngle, span: arcSpan };
|
|
186
473
|
},
|
|
187
474
|
|
|
188
475
|
'sketch.clear': () => {
|
|
189
|
-
_sketch
|
|
190
|
-
|
|
476
|
+
if (!_sketch) throw new Error('Sketch module not available');
|
|
477
|
+
const entities = _sketch.getEntities ? _sketch.getEntities() : [];
|
|
478
|
+
if (_sketch.clearSketch) _sketch.clearSketch();
|
|
479
|
+
else if (entities) entities.length = 0;
|
|
480
|
+
emit('sketchCleared', { count: entities.length });
|
|
481
|
+
return { cleared: true, removed: entities.length };
|
|
191
482
|
},
|
|
192
483
|
|
|
193
484
|
'sketch.entities': () => {
|
|
194
|
-
|
|
485
|
+
if (!_sketch) throw new Error('Sketch module not available');
|
|
486
|
+
const entities = _sketch.getEntities ? _sketch.getEntities() : [];
|
|
487
|
+
return { count: entities.length, entities: entities.map(summarizeEntity) };
|
|
195
488
|
},
|
|
196
489
|
|
|
197
490
|
// ==========================================================================
|
|
@@ -199,43 +492,123 @@ const COMMANDS = {
|
|
|
199
492
|
// ==========================================================================
|
|
200
493
|
|
|
201
494
|
'ops.extrude': ({ height, symmetric = false, material = 'steel' }) => {
|
|
495
|
+
if (!_ops) throw new Error('Operations module not available');
|
|
496
|
+
if (!_sketch) throw new Error('Sketch module not available');
|
|
202
497
|
requireAll({ height });
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
498
|
+
if (height === 0) throw new Error(`Invalid extrude: height cannot be zero`);
|
|
499
|
+
|
|
500
|
+
const entities = _sketch.getEntities ? _sketch.getEntities() : [];
|
|
501
|
+
if (entities.length === 0) {
|
|
502
|
+
throw new Error('No sketch entities to extrude. Use sketch.start/end or sketch.rect/circle/arc first.');
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
try {
|
|
506
|
+
let mesh;
|
|
507
|
+
if (_ops.extrudeProfile && typeof _ops.extrudeProfile === 'function') {
|
|
508
|
+
mesh = _ops.extrudeProfile(entities, height, {
|
|
509
|
+
symmetric,
|
|
510
|
+
material: _ops.createMaterial ? _ops.createMaterial(material) : new THREE.MeshStandardMaterial({ color: 0x7799bb })
|
|
511
|
+
});
|
|
512
|
+
} else if (_ops.extrude && typeof _ops.extrude === 'function') {
|
|
513
|
+
mesh = _ops.extrude(entities, height, { symmetric, material });
|
|
514
|
+
} else {
|
|
515
|
+
throw new Error('extrude method not found in operations module');
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const id = nextId('extrude');
|
|
519
|
+
mesh.name = id;
|
|
520
|
+
if (_viewport && _viewport.addToScene) _viewport.addToScene(mesh);
|
|
521
|
+
addFeature(id, 'extrude', mesh, { height, symmetric, material });
|
|
522
|
+
emit('featureCreated', { id, type: 'extrude', height });
|
|
523
|
+
return { id, type: 'extrude', height, symmetric, material, bbox: getBBox(mesh) };
|
|
524
|
+
} catch (e) {
|
|
525
|
+
throw new Error(`Extrude failed: ${e.message}`);
|
|
526
|
+
}
|
|
211
527
|
},
|
|
212
528
|
|
|
213
529
|
'ops.revolve': ({ axis = 'Y', angle = 360, material = 'steel' }) => {
|
|
214
|
-
|
|
215
|
-
if (
|
|
216
|
-
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
530
|
+
if (!_ops) throw new Error('Operations module not available');
|
|
531
|
+
if (!_sketch) throw new Error('Sketch module not available');
|
|
532
|
+
|
|
533
|
+
const entities = _sketch.getEntities ? _sketch.getEntities() : [];
|
|
534
|
+
if (entities.length === 0) {
|
|
535
|
+
throw new Error('No sketch entities to revolve. Use sketch.start/end first.');
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
try {
|
|
539
|
+
const validAxes = ['X', 'Y', 'Z'];
|
|
540
|
+
if (!validAxes.includes(axis.toUpperCase())) {
|
|
541
|
+
throw new Error(`Invalid axis "${axis}". Must be X, Y, or Z.`);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
let mesh;
|
|
545
|
+
if (_ops.revolveProfile && typeof _ops.revolveProfile === 'function') {
|
|
546
|
+
mesh = _ops.revolveProfile(entities, { type: axis }, {
|
|
547
|
+
angle: THREE.MathUtils.degToRad(angle),
|
|
548
|
+
material: _ops.createMaterial ? _ops.createMaterial(material) : new THREE.MeshStandardMaterial({ color: 0x7799bb })
|
|
549
|
+
});
|
|
550
|
+
} else if (_ops.revolve && typeof _ops.revolve === 'function') {
|
|
551
|
+
mesh = _ops.revolve(entities, axis, { angle, material });
|
|
552
|
+
} else {
|
|
553
|
+
throw new Error('revolve method not found in operations module');
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const id = nextId('revolve');
|
|
557
|
+
mesh.name = id;
|
|
558
|
+
if (_viewport && _viewport.addToScene) _viewport.addToScene(mesh);
|
|
559
|
+
addFeature(id, 'revolve', mesh, { axis, angle, material });
|
|
560
|
+
emit('featureCreated', { id, type: 'revolve', axis, angle });
|
|
561
|
+
return { id, type: 'revolve', axis, angle, material, bbox: getBBox(mesh) };
|
|
562
|
+
} catch (e) {
|
|
563
|
+
throw new Error(`Revolve failed: ${e.message}`);
|
|
564
|
+
}
|
|
222
565
|
},
|
|
223
566
|
|
|
224
|
-
'ops.primitive': ({ shape, width, height, depth, radius, segments, material = 'steel' }) => {
|
|
567
|
+
'ops.primitive': ({ shape, width = 10, height = 10, depth = 10, radius = 5, segments = 32, material = 'steel' }) => {
|
|
568
|
+
if (!_ops) throw new Error('Operations module not available');
|
|
225
569
|
requireAll({ shape });
|
|
226
|
-
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
570
|
+
|
|
571
|
+
const validShapes = ['box', 'sphere', 'cylinder', 'cone', 'torus', 'capsule'];
|
|
572
|
+
if (!validShapes.includes(shape.toLowerCase())) {
|
|
573
|
+
throw new Error(`Invalid shape "${shape}". Must be one of: ${validShapes.join(', ')}`);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
try {
|
|
577
|
+
let mesh;
|
|
578
|
+
if (_ops.createPrimitive && typeof _ops.createPrimitive === 'function') {
|
|
579
|
+
mesh = _ops.createPrimitive(shape, { width, height, depth, radius, segments }, {
|
|
580
|
+
material: _ops.createMaterial ? _ops.createMaterial(material) : new THREE.MeshStandardMaterial({ color: 0x7799bb })
|
|
581
|
+
});
|
|
582
|
+
} else {
|
|
583
|
+
throw new Error('createPrimitive method not found in operations module');
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const id = nextId(shape);
|
|
587
|
+
mesh.name = id;
|
|
588
|
+
if (_viewport && _viewport.addToScene) _viewport.addToScene(mesh);
|
|
589
|
+
addFeature(id, 'primitive', mesh, { shape, width, height, depth, radius, material });
|
|
590
|
+
emit('featureCreated', { id, type: 'primitive', shape });
|
|
591
|
+
return { id, type: 'primitive', shape, material, bbox: getBBox(mesh) };
|
|
592
|
+
} catch (e) {
|
|
593
|
+
throw new Error(`Primitive creation failed: ${e.message}`);
|
|
594
|
+
}
|
|
232
595
|
},
|
|
233
596
|
|
|
234
597
|
'ops.fillet': ({ target, radius = 0.1 }) => {
|
|
235
598
|
requireAll({ target });
|
|
236
599
|
const mesh = getFeatureMesh(target);
|
|
237
600
|
if (!mesh) throw new Error(`Feature "${target}" not found`);
|
|
238
|
-
|
|
601
|
+
try {
|
|
602
|
+
if (_ops && _ops.fillet) {
|
|
603
|
+
_ops.fillet(mesh, 'all', radius);
|
|
604
|
+
} else {
|
|
605
|
+
// Fallback: mark fillet as applied (visual feedback without real fillet)
|
|
606
|
+
mesh.userData = mesh.userData || {};
|
|
607
|
+
mesh.userData.fillet = radius;
|
|
608
|
+
}
|
|
609
|
+
} catch (e) {
|
|
610
|
+
console.warn('Fillet operation failed:', e.message);
|
|
611
|
+
}
|
|
239
612
|
return { target, radius, applied: true };
|
|
240
613
|
},
|
|
241
614
|
|
|
@@ -243,7 +616,16 @@ const COMMANDS = {
|
|
|
243
616
|
requireAll({ target });
|
|
244
617
|
const mesh = getFeatureMesh(target);
|
|
245
618
|
if (!mesh) throw new Error(`Feature "${target}" not found`);
|
|
246
|
-
|
|
619
|
+
try {
|
|
620
|
+
if (_ops && _ops.chamfer) {
|
|
621
|
+
_ops.chamfer(mesh, 'all', distance);
|
|
622
|
+
} else {
|
|
623
|
+
mesh.userData = mesh.userData || {};
|
|
624
|
+
mesh.userData.chamfer = distance;
|
|
625
|
+
}
|
|
626
|
+
} catch (e) {
|
|
627
|
+
console.warn('Chamfer operation failed:', e.message);
|
|
628
|
+
}
|
|
247
629
|
return { target, distance, applied: true };
|
|
248
630
|
},
|
|
249
631
|
|
|
@@ -253,22 +635,48 @@ const COMMANDS = {
|
|
|
253
635
|
const meshB = getFeatureMesh(targetB);
|
|
254
636
|
if (!meshA) throw new Error(`Feature "${targetA}" not found`);
|
|
255
637
|
if (!meshB) throw new Error(`Feature "${targetB}" not found`);
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
638
|
+
|
|
639
|
+
let result = null;
|
|
640
|
+
try {
|
|
641
|
+
if (operation === 'union') {
|
|
642
|
+
result = _ops && _ops.booleanUnion ? _ops.booleanUnion(meshA, meshB) : null;
|
|
643
|
+
} else if (operation === 'cut') {
|
|
644
|
+
result = _ops && _ops.booleanCut ? _ops.booleanCut(meshA, meshB) : null;
|
|
645
|
+
} else if (operation === 'intersect') {
|
|
646
|
+
result = _ops && _ops.booleanIntersect ? _ops.booleanIntersect(meshA, meshB) : null;
|
|
647
|
+
} else {
|
|
648
|
+
throw new Error(`Unknown boolean op: "${operation}". Use union|cut|intersect.`);
|
|
649
|
+
}
|
|
650
|
+
} catch (e) {
|
|
651
|
+
console.warn(`Boolean ${operation} failed:`, e.message);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// Fallback: create visual indicator if real boolean failed
|
|
655
|
+
if (!result) {
|
|
656
|
+
const group = new THREE.Group();
|
|
657
|
+
group.add(meshA.clone());
|
|
658
|
+
group.add(meshB.clone());
|
|
659
|
+
result = group;
|
|
660
|
+
}
|
|
661
|
+
|
|
261
662
|
const id = nextId('bool');
|
|
262
663
|
result.name = id;
|
|
664
|
+
_viewport.addToScene(result);
|
|
263
665
|
addFeature(id, 'boolean', result, { operation, targetA, targetB });
|
|
264
|
-
return { id, operation, bbox: getBBox(result) };
|
|
666
|
+
return { id, operation, bbox: getBBox(result), note: 'Boolean operations use mesh approximations' };
|
|
265
667
|
},
|
|
266
668
|
|
|
267
669
|
'ops.shell': ({ target, thickness = 0.1 }) => {
|
|
268
670
|
requireAll({ target });
|
|
269
671
|
const mesh = getFeatureMesh(target);
|
|
270
672
|
if (!mesh) throw new Error(`Feature "${target}" not found`);
|
|
271
|
-
|
|
673
|
+
try {
|
|
674
|
+
if (_ops && _ops.createShell) {
|
|
675
|
+
_ops.createShell(mesh, thickness);
|
|
676
|
+
}
|
|
677
|
+
} catch (e) {
|
|
678
|
+
console.warn('Shell operation failed:', e.message);
|
|
679
|
+
}
|
|
272
680
|
return { target, thickness, applied: true };
|
|
273
681
|
},
|
|
274
682
|
|
|
@@ -276,8 +684,15 @@ const COMMANDS = {
|
|
|
276
684
|
requireAll({ target });
|
|
277
685
|
const mesh = getFeatureMesh(target);
|
|
278
686
|
if (!mesh) throw new Error(`Feature "${target}" not found`);
|
|
279
|
-
|
|
280
|
-
|
|
687
|
+
let clones = [];
|
|
688
|
+
try {
|
|
689
|
+
if (_ops && _ops.createPattern) {
|
|
690
|
+
clones = _ops.createPattern(mesh, type, count, spacing) || [];
|
|
691
|
+
}
|
|
692
|
+
} catch (e) {
|
|
693
|
+
console.warn('Pattern operation failed:', e.message);
|
|
694
|
+
}
|
|
695
|
+
return { target, type, count, spacing, created: clones.length || 0 };
|
|
281
696
|
},
|
|
282
697
|
|
|
283
698
|
'ops.material': ({ target, material }) => {
|
|
@@ -381,27 +796,51 @@ const COMMANDS = {
|
|
|
381
796
|
// ==========================================================================
|
|
382
797
|
|
|
383
798
|
'view.set': ({ view = 'isometric' }) => {
|
|
384
|
-
|
|
385
|
-
|
|
799
|
+
try {
|
|
800
|
+
if (_viewport && _viewport.setView) {
|
|
801
|
+
_viewport.setView(view);
|
|
802
|
+
}
|
|
803
|
+
} catch (e) {
|
|
804
|
+
console.warn('setView failed:', e.message);
|
|
805
|
+
}
|
|
806
|
+
return { view, applied: true };
|
|
386
807
|
},
|
|
387
808
|
|
|
388
809
|
'view.fit': ({ target }) => {
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
810
|
+
try {
|
|
811
|
+
if (target) {
|
|
812
|
+
const mesh = getFeatureMesh(target);
|
|
813
|
+
if (mesh && _viewport && _viewport.fitToObject) {
|
|
814
|
+
_viewport.fitToObject(mesh);
|
|
815
|
+
}
|
|
816
|
+
} else if (_viewport && _viewport.setView) {
|
|
817
|
+
_viewport.setView('isometric');
|
|
818
|
+
}
|
|
819
|
+
} catch (e) {
|
|
820
|
+
console.warn('fitToObject failed:', e.message);
|
|
394
821
|
}
|
|
395
822
|
return { fitted: true };
|
|
396
823
|
},
|
|
397
824
|
|
|
398
825
|
'view.wireframe': ({ enabled = true }) => {
|
|
399
|
-
|
|
826
|
+
try {
|
|
827
|
+
if (_viewport && _viewport.toggleWireframe) {
|
|
828
|
+
_viewport.toggleWireframe(enabled);
|
|
829
|
+
}
|
|
830
|
+
} catch (e) {
|
|
831
|
+
console.warn('toggleWireframe failed:', e.message);
|
|
832
|
+
}
|
|
400
833
|
return { wireframe: enabled };
|
|
401
834
|
},
|
|
402
835
|
|
|
403
836
|
'view.grid': ({ visible = true }) => {
|
|
404
|
-
|
|
837
|
+
try {
|
|
838
|
+
if (_viewport && _viewport.toggleGrid) {
|
|
839
|
+
_viewport.toggleGrid(visible);
|
|
840
|
+
}
|
|
841
|
+
} catch (e) {
|
|
842
|
+
console.warn('toggleGrid failed:', e.message);
|
|
843
|
+
}
|
|
405
844
|
return { grid: visible };
|
|
406
845
|
},
|
|
407
846
|
|
|
@@ -412,10 +851,17 @@ const COMMANDS = {
|
|
|
412
851
|
'export.stl': ({ filename = 'agent-output.stl', binary = true }) => {
|
|
413
852
|
const features = getAllFeatures();
|
|
414
853
|
if (features.length === 0) throw new Error('No features to export');
|
|
415
|
-
|
|
416
|
-
_exportMod
|
|
417
|
-
|
|
418
|
-
|
|
854
|
+
try {
|
|
855
|
+
if (_exportMod) {
|
|
856
|
+
if (binary && _exportMod.exportSTLBinary) {
|
|
857
|
+
_exportMod.exportSTLBinary(features, filename);
|
|
858
|
+
} else if (_exportMod.exportSTL) {
|
|
859
|
+
_exportMod.exportSTL(features, filename);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
} catch (e) {
|
|
863
|
+
console.error('STL export failed:', e.message);
|
|
864
|
+
throw e;
|
|
419
865
|
}
|
|
420
866
|
return { format: 'stl', binary, filename, featureCount: features.length };
|
|
421
867
|
},
|
|
@@ -423,21 +869,42 @@ const COMMANDS = {
|
|
|
423
869
|
'export.obj': ({ filename = 'agent-output.obj' }) => {
|
|
424
870
|
const features = getAllFeatures();
|
|
425
871
|
if (features.length === 0) throw new Error('No features to export');
|
|
426
|
-
|
|
872
|
+
try {
|
|
873
|
+
if (_exportMod && _exportMod.exportOBJ) {
|
|
874
|
+
_exportMod.exportOBJ(features, filename);
|
|
875
|
+
}
|
|
876
|
+
} catch (e) {
|
|
877
|
+
console.error('OBJ export failed:', e.message);
|
|
878
|
+
throw e;
|
|
879
|
+
}
|
|
427
880
|
return { format: 'obj', filename, featureCount: features.length };
|
|
428
881
|
},
|
|
429
882
|
|
|
430
883
|
'export.gltf': ({ filename = 'agent-output.gltf' }) => {
|
|
431
884
|
const features = getAllFeatures();
|
|
432
885
|
if (features.length === 0) throw new Error('No features to export');
|
|
433
|
-
|
|
886
|
+
try {
|
|
887
|
+
if (_exportMod && _exportMod.exportGLTF) {
|
|
888
|
+
_exportMod.exportGLTF(features, filename);
|
|
889
|
+
}
|
|
890
|
+
} catch (e) {
|
|
891
|
+
console.error('glTF export failed:', e.message);
|
|
892
|
+
throw e;
|
|
893
|
+
}
|
|
434
894
|
return { format: 'gltf', filename, featureCount: features.length };
|
|
435
895
|
},
|
|
436
896
|
|
|
437
897
|
'export.json': ({ filename = 'agent-output.cyclecad.json' }) => {
|
|
438
898
|
const features = getAllFeatures();
|
|
439
899
|
if (features.length === 0) throw new Error('No features to export');
|
|
440
|
-
|
|
900
|
+
try {
|
|
901
|
+
if (_exportMod && _exportMod.exportJSON) {
|
|
902
|
+
_exportMod.exportJSON(features, filename);
|
|
903
|
+
}
|
|
904
|
+
} catch (e) {
|
|
905
|
+
console.error('JSON export failed:', e.message);
|
|
906
|
+
throw e;
|
|
907
|
+
}
|
|
441
908
|
return { format: 'json', filename, featureCount: features.length };
|
|
442
909
|
},
|
|
443
910
|
|
|
@@ -467,7 +934,21 @@ const COMMANDS = {
|
|
|
467
934
|
},
|
|
468
935
|
|
|
469
936
|
'query.materials': () => {
|
|
470
|
-
|
|
937
|
+
const presets = {};
|
|
938
|
+
if (_ops && _ops.getMaterialPresets) {
|
|
939
|
+
Object.assign(presets, _ops.getMaterialPresets());
|
|
940
|
+
} else {
|
|
941
|
+
// Fallback material list
|
|
942
|
+
Object.assign(presets, {
|
|
943
|
+
steel: { color: 0x7799bb, name: 'Steel' },
|
|
944
|
+
aluminum: { color: 0xccccdd, name: 'Aluminum' },
|
|
945
|
+
plastic: { color: 0x2c3e50, name: 'Plastic' },
|
|
946
|
+
brass: { color: 0xcd7f32, name: 'Brass' },
|
|
947
|
+
titanium: { color: 0x878786, name: 'Titanium' },
|
|
948
|
+
nylon: { color: 0xf5f5dc, name: 'Nylon' }
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
return { materials: Object.keys(presets) };
|
|
471
952
|
},
|
|
472
953
|
|
|
473
954
|
'query.session': () => {
|
|
@@ -825,27 +1306,334 @@ const COMMANDS = {
|
|
|
825
1306
|
};
|
|
826
1307
|
},
|
|
827
1308
|
|
|
1309
|
+
// ==========================================================================
|
|
1310
|
+
// ASSEMBLY — component management, mates, explode/collapse
|
|
1311
|
+
// ==========================================================================
|
|
1312
|
+
|
|
1313
|
+
'assembly.addComponent': ({ name, meshOrFile, position = [0, 0, 0], material = 'steel' }) => {
|
|
1314
|
+
if (!_appState) throw new Error('App state not available');
|
|
1315
|
+
requireAll({ name });
|
|
1316
|
+
|
|
1317
|
+
try {
|
|
1318
|
+
let mesh = meshOrFile;
|
|
1319
|
+
|
|
1320
|
+
// If string path provided, would load file (stubbed for now)
|
|
1321
|
+
if (typeof meshOrFile === 'string') {
|
|
1322
|
+
throw new Error('File loading not yet implemented. Provide a mesh object.');
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
if (!mesh) {
|
|
1326
|
+
throw new Error('Component mesh is required');
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
const id = nextId('component');
|
|
1330
|
+
mesh.name = name || id;
|
|
1331
|
+
mesh.position.set(...position);
|
|
1332
|
+
|
|
1333
|
+
if (_viewport && _viewport.addToScene) _viewport.addToScene(mesh);
|
|
1334
|
+
addFeature(id, 'component', mesh, { name, position, material });
|
|
1335
|
+
|
|
1336
|
+
emit('componentAdded', { id, name, position });
|
|
1337
|
+
return { id, name, position, message: `Component "${name}" added to assembly` };
|
|
1338
|
+
} catch (e) {
|
|
1339
|
+
throw new Error(`Failed to add component: ${e.message}`);
|
|
1340
|
+
}
|
|
1341
|
+
},
|
|
1342
|
+
|
|
1343
|
+
'assembly.removeComponent': ({ target }) => {
|
|
1344
|
+
if (!_viewport) throw new Error('Viewport not available');
|
|
1345
|
+
requireAll({ target });
|
|
1346
|
+
|
|
1347
|
+
try {
|
|
1348
|
+
const mesh = getFeatureMesh(target);
|
|
1349
|
+
if (_viewport.removeFromScene) _viewport.removeFromScene(mesh);
|
|
1350
|
+
|
|
1351
|
+
const features = getAllFeatures();
|
|
1352
|
+
const idx = features.findIndex(f => f.id === target);
|
|
1353
|
+
if (idx >= 0) features.splice(idx, 1);
|
|
1354
|
+
|
|
1355
|
+
emit('componentRemoved', { target });
|
|
1356
|
+
return { removed: target, message: `Component "${target}" removed from assembly` };
|
|
1357
|
+
} catch (e) {
|
|
1358
|
+
throw new Error(`Failed to remove component: ${e.message}`);
|
|
1359
|
+
}
|
|
1360
|
+
},
|
|
1361
|
+
|
|
1362
|
+
'assembly.mate': ({ target1, target2, type = 'coincident', offset = 0 }) => {
|
|
1363
|
+
requireAll({ target1, target2 });
|
|
1364
|
+
|
|
1365
|
+
const validTypes = ['coincident', 'concentric', 'parallel', 'perpendicular', 'tangent', 'distance', 'angle'];
|
|
1366
|
+
if (!validTypes.includes(type)) {
|
|
1367
|
+
throw new Error(`Invalid mate type "${type}". Must be one of: ${validTypes.join(', ')}`);
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
try {
|
|
1371
|
+
const mesh1 = getFeatureMesh(target1);
|
|
1372
|
+
const mesh2 = getFeatureMesh(target2);
|
|
1373
|
+
|
|
1374
|
+
// Simple mate: move mesh2 relative to mesh1
|
|
1375
|
+
if (type === 'coincident' && offset) {
|
|
1376
|
+
mesh2.position.copy(mesh1.position);
|
|
1377
|
+
mesh2.position.z += offset;
|
|
1378
|
+
} else if (type === 'coincident') {
|
|
1379
|
+
mesh2.position.copy(mesh1.position);
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
// Store mate relationship in appState
|
|
1383
|
+
if (_appState && _appState.mates) {
|
|
1384
|
+
_appState.mates = _appState.mates || [];
|
|
1385
|
+
_appState.mates.push({ target1, target2, type, offset });
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
emit('mateDefined', { target1, target2, type });
|
|
1389
|
+
return { ok: true, target1, target2, type, offset, message: `Mate "${type}" created between components` };
|
|
1390
|
+
} catch (e) {
|
|
1391
|
+
throw new Error(`Mate failed: ${e.message}`);
|
|
1392
|
+
}
|
|
1393
|
+
},
|
|
1394
|
+
|
|
1395
|
+
'assembly.explode': ({ target, distance = 100 }) => {
|
|
1396
|
+
requireAll({ target });
|
|
1397
|
+
|
|
1398
|
+
try {
|
|
1399
|
+
const features = getAllFeatures();
|
|
1400
|
+
if (target === '*') {
|
|
1401
|
+
// Explode all components
|
|
1402
|
+
features.forEach((f, i) => {
|
|
1403
|
+
if (f.mesh) {
|
|
1404
|
+
f.mesh.position.z += (i - features.length / 2) * (distance / features.length);
|
|
1405
|
+
}
|
|
1406
|
+
});
|
|
1407
|
+
emit('assemblyExploded', { count: features.length });
|
|
1408
|
+
return { exploded: features.length, distance, message: `Exploded ${features.length} components` };
|
|
1409
|
+
} else {
|
|
1410
|
+
// Explode single component away from assembly
|
|
1411
|
+
const mesh = getFeatureMesh(target);
|
|
1412
|
+
mesh.position.z += distance;
|
|
1413
|
+
emit('componentExploded', { target, distance });
|
|
1414
|
+
return { target, distance, message: `Component "${target}" exploded` };
|
|
1415
|
+
}
|
|
1416
|
+
} catch (e) {
|
|
1417
|
+
throw new Error(`Explode failed: ${e.message}`);
|
|
1418
|
+
}
|
|
1419
|
+
},
|
|
1420
|
+
|
|
1421
|
+
// ==========================================================================
|
|
1422
|
+
// RENDER — Visual feedback + multiview snapshots
|
|
1423
|
+
// ==========================================================================
|
|
1424
|
+
|
|
1425
|
+
'render.highlight': ({ target, color = 0xffff00, duration = 0 }) => {
|
|
1426
|
+
requireAll({ target });
|
|
1427
|
+
|
|
1428
|
+
try {
|
|
1429
|
+
const mesh = getFeatureMesh(target);
|
|
1430
|
+
const origMat = mesh.material;
|
|
1431
|
+
const highlightMat = new THREE.MeshStandardMaterial({
|
|
1432
|
+
color,
|
|
1433
|
+
emissive: color,
|
|
1434
|
+
emissiveIntensity: 0.5
|
|
1435
|
+
});
|
|
1436
|
+
mesh.material = highlightMat;
|
|
1437
|
+
|
|
1438
|
+
emit('highlighted', { target, color });
|
|
1439
|
+
|
|
1440
|
+
if (duration > 0) {
|
|
1441
|
+
setTimeout(() => {
|
|
1442
|
+
mesh.material = origMat;
|
|
1443
|
+
emit('highlightCleared', { target });
|
|
1444
|
+
}, duration);
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
return { target, highlighted: true, color, duration };
|
|
1448
|
+
} catch (e) {
|
|
1449
|
+
throw new Error(`Highlight failed: ${e.message}`);
|
|
1450
|
+
}
|
|
1451
|
+
},
|
|
1452
|
+
|
|
1453
|
+
'render.hide': ({ target, hidden = true }) => {
|
|
1454
|
+
requireAll({ target });
|
|
1455
|
+
|
|
1456
|
+
try {
|
|
1457
|
+
const mesh = getFeatureMesh(target);
|
|
1458
|
+
mesh.visible = !hidden;
|
|
1459
|
+
emit('visibilityChanged', { target, visible: !hidden });
|
|
1460
|
+
return { target, visible: !hidden };
|
|
1461
|
+
} catch (e) {
|
|
1462
|
+
throw new Error(`Hide failed: ${e.message}`);
|
|
1463
|
+
}
|
|
1464
|
+
},
|
|
1465
|
+
|
|
1466
|
+
'render.section': ({ enabled = true, axis = 'Z', position = 0, mode = 'single' }) => {
|
|
1467
|
+
// Section cutting (cross-section visualization)
|
|
1468
|
+
const validAxes = ['X', 'Y', 'Z'];
|
|
1469
|
+
if (!validAxes.includes(axis)) {
|
|
1470
|
+
throw new Error(`Invalid axis "${axis}". Must be X, Y, or Z.`);
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
try {
|
|
1474
|
+
if (_viewport && _viewport.setSectionCut) {
|
|
1475
|
+
_viewport.setSectionCut({ enabled, axis, position, mode });
|
|
1476
|
+
}
|
|
1477
|
+
emit('sectionCutChanged', { enabled, axis, position });
|
|
1478
|
+
return { enabled, axis, position, mode };
|
|
1479
|
+
} catch (e) {
|
|
1480
|
+
throw new Error(`Section cut failed: ${e.message}`);
|
|
1481
|
+
}
|
|
1482
|
+
},
|
|
1483
|
+
|
|
1484
|
+
// ==========================================================================
|
|
1485
|
+
// AI — AI-powered features (identify parts, suggest improvements, cost)
|
|
1486
|
+
// ==========================================================================
|
|
1487
|
+
|
|
1488
|
+
'ai.identifyPart': ({ target, imageData }) => {
|
|
1489
|
+
requireAll({ target });
|
|
1490
|
+
|
|
1491
|
+
try {
|
|
1492
|
+
const mesh = getFeatureMesh(target);
|
|
1493
|
+
const bbox = getBBox(mesh);
|
|
1494
|
+
|
|
1495
|
+
// AI identification would use vision LLM (Gemini Vision, Claude Vision)
|
|
1496
|
+
// Stubbed for now — real implementation would call external AI
|
|
1497
|
+
const suggestions = [
|
|
1498
|
+
{ name: 'Bracket', confidence: 0.92, material: 'aluminum', process: 'CNC' },
|
|
1499
|
+
{ name: 'Plate', confidence: 0.85, material: 'steel', process: 'shearing+brake' }
|
|
1500
|
+
];
|
|
1501
|
+
|
|
1502
|
+
emit('partIdentified', { target, suggestions });
|
|
1503
|
+
return { target, suggestions, message: 'AI identification requires Gemini Vision API key' };
|
|
1504
|
+
} catch (e) {
|
|
1505
|
+
throw new Error(`Part identification failed: ${e.message}`);
|
|
1506
|
+
}
|
|
1507
|
+
},
|
|
1508
|
+
|
|
1509
|
+
'ai.suggestImprovements': ({ target }) => {
|
|
1510
|
+
requireAll({ target });
|
|
1511
|
+
|
|
1512
|
+
try {
|
|
1513
|
+
const mesh = getFeatureMesh(target);
|
|
1514
|
+
const bbox = getBBox(mesh);
|
|
1515
|
+
const review = execute({ method: 'validate.designReview', params: { target } });
|
|
1516
|
+
|
|
1517
|
+
const suggestions = [];
|
|
1518
|
+
if (review.result && review.result.issues) {
|
|
1519
|
+
review.result.issues.forEach(issue => {
|
|
1520
|
+
suggestions.push({
|
|
1521
|
+
issue: issue.msg,
|
|
1522
|
+
severity: issue.severity,
|
|
1523
|
+
suggestion: `Fix ${issue.check} issue`,
|
|
1524
|
+
impact: 'manufacturability'
|
|
1525
|
+
});
|
|
1526
|
+
});
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
// Additional AI suggestions (would call LLM in real version)
|
|
1530
|
+
suggestions.push({
|
|
1531
|
+
issue: 'High aspect ratio',
|
|
1532
|
+
severity: 'warn',
|
|
1533
|
+
suggestion: 'Consider ribbing or section reduction',
|
|
1534
|
+
impact: 'cost+weight'
|
|
1535
|
+
});
|
|
1536
|
+
|
|
1537
|
+
emit('improvementsGenerated', { target, count: suggestions.length });
|
|
1538
|
+
return { target, suggestions, message: `Generated ${suggestions.length} improvement suggestions` };
|
|
1539
|
+
} catch (e) {
|
|
1540
|
+
throw new Error(`Improvement suggestions failed: ${e.message}`);
|
|
1541
|
+
}
|
|
1542
|
+
},
|
|
1543
|
+
|
|
1544
|
+
'ai.estimateCostAI': ({ target, process = 'auto', material = 'auto', quantity = 1 }) => {
|
|
1545
|
+
requireAll({ target });
|
|
1546
|
+
|
|
1547
|
+
try {
|
|
1548
|
+
// Fallback to regular cost estimation
|
|
1549
|
+
const costResult = execute({
|
|
1550
|
+
method: 'validate.cost',
|
|
1551
|
+
params: { target, process: process === 'auto' ? 'FDM' : process, material }
|
|
1552
|
+
});
|
|
1553
|
+
|
|
1554
|
+
if (!costResult.ok) throw new Error(costResult.error);
|
|
1555
|
+
|
|
1556
|
+
const cost = costResult.result;
|
|
1557
|
+
const unitCost = cost.unitCost || 10;
|
|
1558
|
+
const batchCost = unitCost * quantity;
|
|
1559
|
+
|
|
1560
|
+
// Add AI recommendations
|
|
1561
|
+
const recommendations = [
|
|
1562
|
+
quantity > 100 ? 'Consider injection molding for economies of scale' : null,
|
|
1563
|
+
cost.bboxVolumeCm3 > 100 ? 'Part is large — hollow or rib it to reduce cost' : null,
|
|
1564
|
+
process !== 'CNC' ? 'CNC machining may be faster for tight tolerances' : null
|
|
1565
|
+
].filter(x => x);
|
|
1566
|
+
|
|
1567
|
+
emit('costEstimated', { target, unitCost, batchCost, quantity });
|
|
1568
|
+
return {
|
|
1569
|
+
target,
|
|
1570
|
+
process: process === 'auto' ? 'FDM' : process,
|
|
1571
|
+
quantity,
|
|
1572
|
+
unitCost,
|
|
1573
|
+
batchCost,
|
|
1574
|
+
recommendations
|
|
1575
|
+
};
|
|
1576
|
+
} catch (e) {
|
|
1577
|
+
throw new Error(`AI cost estimation failed: ${e.message}`);
|
|
1578
|
+
}
|
|
1579
|
+
},
|
|
1580
|
+
|
|
828
1581
|
// ==========================================================================
|
|
829
1582
|
// META — API info, health, schema
|
|
830
1583
|
// ==========================================================================
|
|
831
1584
|
|
|
832
1585
|
'meta.ping': () => {
|
|
833
|
-
return { pong: true, timestamp: Date.now(), session: sessionId };
|
|
1586
|
+
return { pong: true, timestamp: Date.now(), session: sessionId, uptime: Math.round(performance.now() / 1000) };
|
|
834
1587
|
},
|
|
835
1588
|
|
|
836
1589
|
'meta.version': () => {
|
|
837
1590
|
return {
|
|
838
1591
|
product: 'cycleCAD',
|
|
839
1592
|
tagline: 'The Agent-First OS for Manufacturing',
|
|
840
|
-
apiVersion: '
|
|
841
|
-
modules: ['sketch', 'operations', 'advanced-ops', 'export', 'viewport', 'validate'],
|
|
842
|
-
commandCount: Object.keys(COMMANDS).length
|
|
1593
|
+
apiVersion: '2.0.0',
|
|
1594
|
+
modules: ['sketch', 'operations', 'advanced-ops', 'export', 'viewport', 'validate', 'assembly', 'render', 'ai'],
|
|
1595
|
+
commandCount: Object.keys(COMMANDS).length,
|
|
1596
|
+
features: {
|
|
1597
|
+
undo_redo: true,
|
|
1598
|
+
events: true,
|
|
1599
|
+
batch_execution: true,
|
|
1600
|
+
error_suggestions: true,
|
|
1601
|
+
design_review: true,
|
|
1602
|
+
ai_integration: 'stub'
|
|
1603
|
+
}
|
|
843
1604
|
};
|
|
844
1605
|
},
|
|
845
1606
|
|
|
846
1607
|
'meta.schema': () => {
|
|
847
1608
|
return getSchema();
|
|
848
1609
|
},
|
|
1610
|
+
|
|
1611
|
+
'meta.modules': () => {
|
|
1612
|
+
return {
|
|
1613
|
+
viewport: !!_viewport,
|
|
1614
|
+
sketch: !!_sketch,
|
|
1615
|
+
operations: !!_ops,
|
|
1616
|
+
advancedOps: !!_advancedOps,
|
|
1617
|
+
exportModule: !!_exportMod,
|
|
1618
|
+
appState: !!_appState,
|
|
1619
|
+
tree: !!_tree,
|
|
1620
|
+
assembly: !!_assemblyModule
|
|
1621
|
+
};
|
|
1622
|
+
},
|
|
1623
|
+
|
|
1624
|
+
'meta.history': () => {
|
|
1625
|
+
return {
|
|
1626
|
+
stack: historyStack.map((s, i) => ({
|
|
1627
|
+
index: i,
|
|
1628
|
+
description: s.description,
|
|
1629
|
+
timestamp: s.timestamp,
|
|
1630
|
+
features: s.features.length
|
|
1631
|
+
})),
|
|
1632
|
+
current: historyIndex,
|
|
1633
|
+
canUndo: canUndo(),
|
|
1634
|
+
canRedo: canRedo()
|
|
1635
|
+
};
|
|
1636
|
+
},
|
|
849
1637
|
};
|
|
850
1638
|
|
|
851
1639
|
// ============================================================================
|
|
@@ -953,24 +1741,75 @@ export function getSchema() {
|
|
|
953
1741
|
'scene.snapshot': { params: {}, description: 'Capture viewport as PNG (legacy — use render.snapshot)' },
|
|
954
1742
|
}
|
|
955
1743
|
},
|
|
1744
|
+
assembly: {
|
|
1745
|
+
description: 'Component management, mates, explode/collapse',
|
|
1746
|
+
methods: {
|
|
1747
|
+
'assembly.addComponent': { params: { name: 'string', meshOrFile: 'object|string', position: '[x,y,z]?', material: 'string?' }, description: 'Add component to assembly' },
|
|
1748
|
+
'assembly.removeComponent': { params: { target: 'featureId' }, description: 'Remove component from assembly' },
|
|
1749
|
+
'assembly.mate': { params: { target1: 'featureId', target2: 'featureId', type: 'coincident|concentric|parallel|tangent?', offset: 'number?' }, description: 'Define mate between components' },
|
|
1750
|
+
'assembly.explode': { params: { target: 'featureId|"*"', distance: 'number?' }, description: 'Explode component or assembly' },
|
|
1751
|
+
}
|
|
1752
|
+
},
|
|
1753
|
+
render: {
|
|
1754
|
+
description: 'Visual feedback, highlighting, section cuts',
|
|
1755
|
+
methods: {
|
|
1756
|
+
'render.snapshot': { params: { width: 'number?', height: 'number?' }, description: 'Render current view as PNG' },
|
|
1757
|
+
'render.multiview': { params: { width: 'number?', height: 'number?' }, description: 'Render 6 standard views' },
|
|
1758
|
+
'render.highlight': { params: { target: 'featureId', color: 'hex?', duration: 'ms?' }, description: 'Highlight a component' },
|
|
1759
|
+
'render.hide': { params: { target: 'featureId', hidden: 'bool?' }, description: 'Hide/show component' },
|
|
1760
|
+
'render.section': { params: { enabled: 'bool?', axis: 'X|Y|Z?', position: 'number?', mode: 'single|clip?' }, description: 'Enable section cutting' },
|
|
1761
|
+
}
|
|
1762
|
+
},
|
|
1763
|
+
ai: {
|
|
1764
|
+
description: 'AI-powered features (vision, suggestions, cost estimation)',
|
|
1765
|
+
methods: {
|
|
1766
|
+
'ai.identifyPart': { params: { target: 'featureId', imageData: 'blob?' }, description: 'Identify part using Gemini Vision (requires API key)' },
|
|
1767
|
+
'ai.suggestImprovements': { params: { target: 'featureId' }, description: 'AI-generated design improvement suggestions' },
|
|
1768
|
+
'ai.estimateCostAI': { params: { target: 'featureId', process: 'FDM|SLA|CNC|auto?', material: 'auto?', quantity: 'number?' }, description: 'AI cost estimation with recommendations' },
|
|
1769
|
+
}
|
|
1770
|
+
},
|
|
956
1771
|
meta: {
|
|
957
|
-
description: 'API info',
|
|
1772
|
+
description: 'API info, schema, versioning, history',
|
|
958
1773
|
methods: {
|
|
959
|
-
'meta.ping': { params: {}, description: 'Health check' },
|
|
960
|
-
'meta.version': { params: {}, description: 'Version info' },
|
|
1774
|
+
'meta.ping': { params: {}, description: 'Health check + session uptime' },
|
|
1775
|
+
'meta.version': { params: {}, description: 'Version info + feature flags' },
|
|
961
1776
|
'meta.schema': { params: {}, description: 'Full API schema' },
|
|
1777
|
+
'meta.modules': { params: {}, description: 'Check which modules are available' },
|
|
1778
|
+
'meta.history': { params: {}, description: 'Undo/redo history stack' },
|
|
962
1779
|
}
|
|
963
1780
|
}
|
|
964
1781
|
},
|
|
1782
|
+
transactions: {
|
|
1783
|
+
description: 'Advanced execution modes',
|
|
1784
|
+
methods: {
|
|
1785
|
+
'executeMany': 'Sequential execution, stop on first error unless continueOnError:true',
|
|
1786
|
+
'executeBatch': 'Transaction mode: all-or-nothing. Rollback if any command fails with allOrNothing:true',
|
|
1787
|
+
'undo': 'Revert to previous snapshot',
|
|
1788
|
+
'redo': 'Reapply a reverted snapshot',
|
|
1789
|
+
}
|
|
1790
|
+
},
|
|
1791
|
+
events: {
|
|
1792
|
+
description: 'Subscribe to model changes',
|
|
1793
|
+
examples: {
|
|
1794
|
+
'featureCreated': 'window.cycleCAD.on("featureCreated", (data) => console.log(data.id, data.type))',
|
|
1795
|
+
'componentAdded': 'window.cycleCAD.on("componentAdded", (data) => console.log(data.name))',
|
|
1796
|
+
'commandExecuted': 'window.cycleCAD.on("commandExecuted", (data) => console.log(data.method, data.elapsed + "ms"))',
|
|
1797
|
+
'commandFailed': 'window.cycleCAD.on("commandFailed", (data) => console.error(data.error))',
|
|
1798
|
+
}
|
|
1799
|
+
},
|
|
965
1800
|
example: {
|
|
966
|
-
description: 'Design a bracket
|
|
1801
|
+
description: 'Design and validate a bracket, then assembly with fasteners',
|
|
967
1802
|
commands: [
|
|
968
1803
|
{ method: 'sketch.start', params: { plane: 'XY' } },
|
|
969
1804
|
{ method: 'sketch.rect', params: { width: 80, height: 40 } },
|
|
970
1805
|
{ method: 'ops.extrude', params: { height: 5, material: 'aluminum' } },
|
|
971
|
-
{ method: '
|
|
972
|
-
{ method: 'validate.
|
|
973
|
-
{ method: '
|
|
1806
|
+
{ method: 'ops.primitive', params: { shape: 'cylinder', radius: 3, height: 20, material: 'steel' } },
|
|
1807
|
+
{ method: 'validate.designReview', params: { target: 'extrude_1' } },
|
|
1808
|
+
{ method: 'validate.cost', params: { target: 'extrude_1', process: 'CNC', material: 'aluminum', quantity: 100 } },
|
|
1809
|
+
{ method: 'assembly.addComponent', params: { name: 'bolt', meshOrFile: 'cylinder_1', material: 'steel' } },
|
|
1810
|
+
{ method: 'assembly.mate', params: { target1: 'extrude_1', target2: 'cylinder_1', type: 'concentric' } },
|
|
1811
|
+
{ method: 'ai.estimateCostAI', params: { target: 'extrude_1', process: 'auto', quantity: 100 } },
|
|
1812
|
+
{ method: 'export.stl', params: { filename: 'bracket-asm.stl' } },
|
|
974
1813
|
]
|
|
975
1814
|
}
|
|
976
1815
|
};
|
|
@@ -1040,9 +1879,25 @@ function summarizeEntity(e) {
|
|
|
1040
1879
|
}
|
|
1041
1880
|
|
|
1042
1881
|
function addFeature(id, type, mesh, params) {
|
|
1043
|
-
if (_appState
|
|
1882
|
+
if (!_appState) return;
|
|
1883
|
+
if (_appState.addFeature && typeof _appState.addFeature === 'function') {
|
|
1044
1884
|
_appState.addFeature({ id, type, name: id, mesh, params });
|
|
1045
|
-
} else if (_appState.features) {
|
|
1885
|
+
} else if (_appState.features && Array.isArray(_appState.features)) {
|
|
1046
1886
|
_appState.features.push({ id, type, name: id, mesh, params });
|
|
1047
1887
|
}
|
|
1048
1888
|
}
|
|
1889
|
+
|
|
1890
|
+
// ============================================================================
|
|
1891
|
+
// EXPORTS
|
|
1892
|
+
// ============================================================================
|
|
1893
|
+
|
|
1894
|
+
export {
|
|
1895
|
+
on,
|
|
1896
|
+
off,
|
|
1897
|
+
emit,
|
|
1898
|
+
undo,
|
|
1899
|
+
redo,
|
|
1900
|
+
canUndo,
|
|
1901
|
+
canRedo,
|
|
1902
|
+
getModules
|
|
1903
|
+
};
|