cyclecad 0.1.5 → 0.1.8
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/CLAUDE.md +165 -34
- package/app/agent-demo.html +1049 -0
- package/app/index.html +15 -0
- package/app/js/agent-api.js +1048 -0
- package/app/mobile.html +1276 -0
- package/brand-identity.html +918 -0
- package/competitive-analysis.html +629 -0
- package/cycleCAD-Investor-Deck.pptx +0 -0
- package/index.html +746 -776
- package/logo-concepts.html +167 -0
- package/package.json +1 -1
- package/~$cycleCAD-Investor-Deck.pptx +0 -0
|
@@ -0,0 +1,1048 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent-api.js — The Agent-First API for cycleCAD
|
|
3
|
+
*
|
|
4
|
+
* This is the core differentiator: cycleCAD is not a GUI app with an API bolted on.
|
|
5
|
+
* The API IS the product. AI agents are the only interface.
|
|
6
|
+
*
|
|
7
|
+
* Any LLM (GPT, Claude, Gemini, Llama, Mistral) can design, validate, and
|
|
8
|
+
* manufacture through this API. Every operation is a JSON command.
|
|
9
|
+
* Every command returns a structured result.
|
|
10
|
+
*
|
|
11
|
+
* Architecture:
|
|
12
|
+
* Agent sends JSON command → agent-api.js dispatches → cycleCAD modules execute
|
|
13
|
+
* → structured JSON result returned → agent decides next action
|
|
14
|
+
*
|
|
15
|
+
* Protocol: JSON-RPC 2.0 style
|
|
16
|
+
* { method: "sketch.rect", params: { width: 50, height: 30 } }
|
|
17
|
+
* → { ok: true, result: { entityId: "rect_1", ... } }
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js';
|
|
21
|
+
|
|
22
|
+
// ============================================================================
|
|
23
|
+
// Module references — set during init
|
|
24
|
+
// ============================================================================
|
|
25
|
+
let _viewport = null;
|
|
26
|
+
let _sketch = null;
|
|
27
|
+
let _ops = null;
|
|
28
|
+
let _advancedOps = null;
|
|
29
|
+
let _exportMod = null;
|
|
30
|
+
let _appState = null;
|
|
31
|
+
|
|
32
|
+
// ============================================================================
|
|
33
|
+
// Session state
|
|
34
|
+
// ============================================================================
|
|
35
|
+
let sessionId = null;
|
|
36
|
+
let commandLog = [];
|
|
37
|
+
let featureIndex = 0;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Initialize the Agent API with references to all cycleCAD modules
|
|
41
|
+
*/
|
|
42
|
+
export function initAgentAPI({ viewport, sketch, operations, advancedOps, exportModule, appState }) {
|
|
43
|
+
_viewport = viewport;
|
|
44
|
+
_sketch = sketch;
|
|
45
|
+
_ops = operations;
|
|
46
|
+
_advancedOps = advancedOps;
|
|
47
|
+
_exportMod = exportModule;
|
|
48
|
+
_appState = appState;
|
|
49
|
+
sessionId = crypto.randomUUID();
|
|
50
|
+
commandLog = [];
|
|
51
|
+
featureIndex = 0;
|
|
52
|
+
console.log(`[Agent API] Initialized. Session: ${sessionId}`);
|
|
53
|
+
|
|
54
|
+
// Expose globally for external agent access
|
|
55
|
+
window.cycleCAD = { execute, executeMany, getSchema, getState, getSession };
|
|
56
|
+
|
|
57
|
+
return { sessionId };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ============================================================================
|
|
61
|
+
// COMMAND DISPATCH
|
|
62
|
+
// ============================================================================
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Execute a single agent command
|
|
66
|
+
* @param {Object} cmd - { method: string, params: object }
|
|
67
|
+
* @returns {Object} - { ok: boolean, result?: any, error?: string }
|
|
68
|
+
*/
|
|
69
|
+
export function execute(cmd) {
|
|
70
|
+
const start = performance.now();
|
|
71
|
+
try {
|
|
72
|
+
if (!cmd || !cmd.method) {
|
|
73
|
+
return err('Missing "method" field');
|
|
74
|
+
}
|
|
75
|
+
const handler = COMMANDS[cmd.method];
|
|
76
|
+
if (!handler) {
|
|
77
|
+
return err(`Unknown method: "${cmd.method}". Use getSchema() to see available commands.`);
|
|
78
|
+
}
|
|
79
|
+
const result = handler(cmd.params || {});
|
|
80
|
+
const elapsed = Math.round(performance.now() - start);
|
|
81
|
+
const entry = { method: cmd.method, params: cmd.params, elapsed, ok: true };
|
|
82
|
+
commandLog.push(entry);
|
|
83
|
+
return { ok: true, result, elapsed };
|
|
84
|
+
} catch (e) {
|
|
85
|
+
const elapsed = Math.round(performance.now() - start);
|
|
86
|
+
commandLog.push({ method: cmd.method, params: cmd.params, elapsed, ok: false, error: e.message });
|
|
87
|
+
return err(e.message);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Execute a sequence of commands (pipeline)
|
|
93
|
+
* Stops on first error unless continueOnError is set
|
|
94
|
+
* @param {Array<Object>} cmds - Array of { method, params }
|
|
95
|
+
* @param {Object} opts - { continueOnError: false }
|
|
96
|
+
* @returns {Object} - { ok, results: Array, errors: Array }
|
|
97
|
+
*/
|
|
98
|
+
export function executeMany(cmds, opts = {}) {
|
|
99
|
+
const results = [];
|
|
100
|
+
const errors = [];
|
|
101
|
+
for (let i = 0; i < cmds.length; i++) {
|
|
102
|
+
const r = execute(cmds[i]);
|
|
103
|
+
results.push(r);
|
|
104
|
+
if (!r.ok) {
|
|
105
|
+
errors.push({ index: i, method: cmds[i].method, error: r.error });
|
|
106
|
+
if (!opts.continueOnError) break;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return { ok: errors.length === 0, results, errors };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function err(msg) { return { ok: false, error: msg }; }
|
|
113
|
+
function nextId(prefix) { return `${prefix}_${++featureIndex}`; }
|
|
114
|
+
|
|
115
|
+
// ============================================================================
|
|
116
|
+
// HELPER: get mesh from features
|
|
117
|
+
// ============================================================================
|
|
118
|
+
function getFeatureMesh(id) {
|
|
119
|
+
if (!_appState) return null;
|
|
120
|
+
const features = _appState.getFeatures ? _appState.getFeatures() : (_appState.features || []);
|
|
121
|
+
const f = features.find(f => f.id === id || f.name === id);
|
|
122
|
+
return f ? f.mesh : null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function getAllFeatures() {
|
|
126
|
+
if (!_appState) return [];
|
|
127
|
+
return _appState.getFeatures ? _appState.getFeatures() : (_appState.features || []);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ============================================================================
|
|
131
|
+
// COMMAND REGISTRY
|
|
132
|
+
// ============================================================================
|
|
133
|
+
|
|
134
|
+
const COMMANDS = {
|
|
135
|
+
|
|
136
|
+
// ==========================================================================
|
|
137
|
+
// SKETCH — 2D drawing on a plane
|
|
138
|
+
// ==========================================================================
|
|
139
|
+
|
|
140
|
+
'sketch.start': ({ plane = 'XY' }) => {
|
|
141
|
+
const cam = _viewport.getCamera();
|
|
142
|
+
const ctrl = _viewport.getControls();
|
|
143
|
+
_sketch.startSketch(plane, cam, ctrl);
|
|
144
|
+
return { plane, status: 'active' };
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
'sketch.end': () => {
|
|
148
|
+
const entities = _sketch.getEntities();
|
|
149
|
+
_sketch.endSketch();
|
|
150
|
+
return { entityCount: entities.length, entities: entities.map(summarizeEntity) };
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
'sketch.line': ({ x1, y1, x2, y2 }) => {
|
|
154
|
+
requireAll({ x1, y1, x2, y2 });
|
|
155
|
+
const entities = _sketch.getEntities();
|
|
156
|
+
entities.push({ type: 'line', x1, y1, x2, y2, id: nextId('line') });
|
|
157
|
+
return { id: entities[entities.length - 1].id, type: 'line', from: [x1, y1], to: [x2, y2] };
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
'sketch.rect': ({ x = 0, y = 0, width, height }) => {
|
|
161
|
+
requireAll({ width, height });
|
|
162
|
+
const entities = _sketch.getEntities();
|
|
163
|
+
const id = nextId('rect');
|
|
164
|
+
// Rectangle = 4 lines
|
|
165
|
+
entities.push({ type: 'line', x1: x, y1: y, x2: x + width, y2: y, id: id + '_t' });
|
|
166
|
+
entities.push({ type: 'line', x1: x + width, y1: y, x2: x + width, y2: y + height, id: id + '_r' });
|
|
167
|
+
entities.push({ type: 'line', x1: x + width, y1: y + height, x2: x, y2: y + height, id: id + '_b' });
|
|
168
|
+
entities.push({ type: 'line', x1: x, y1: y + height, x2: x, y2: y, id: id + '_l' });
|
|
169
|
+
return { id, type: 'rect', origin: [x, y], width, height, edges: 4 };
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
'sketch.circle': ({ cx = 0, cy = 0, radius }) => {
|
|
173
|
+
requireAll({ radius });
|
|
174
|
+
const entities = _sketch.getEntities();
|
|
175
|
+
const id = nextId('circle');
|
|
176
|
+
entities.push({ type: 'circle', cx, cy, radius, id });
|
|
177
|
+
return { id, type: 'circle', center: [cx, cy], radius };
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
'sketch.arc': ({ cx = 0, cy = 0, radius, startAngle = 0, endAngle = Math.PI }) => {
|
|
181
|
+
requireAll({ radius });
|
|
182
|
+
const entities = _sketch.getEntities();
|
|
183
|
+
const id = nextId('arc');
|
|
184
|
+
entities.push({ type: 'arc', cx, cy, radius, startAngle, endAngle, id });
|
|
185
|
+
return { id, type: 'arc', center: [cx, cy], radius, startAngle, endAngle };
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
'sketch.clear': () => {
|
|
189
|
+
_sketch.clearSketch();
|
|
190
|
+
return { cleared: true };
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
'sketch.entities': () => {
|
|
194
|
+
return { entities: _sketch.getEntities().map(summarizeEntity) };
|
|
195
|
+
},
|
|
196
|
+
|
|
197
|
+
// ==========================================================================
|
|
198
|
+
// OPERATIONS — 3D modeling
|
|
199
|
+
// ==========================================================================
|
|
200
|
+
|
|
201
|
+
'ops.extrude': ({ height, symmetric = false, material = 'steel' }) => {
|
|
202
|
+
requireAll({ height });
|
|
203
|
+
const entities = _sketch.getEntities();
|
|
204
|
+
if (entities.length === 0) throw new Error('No sketch entities to extrude. Use sketch.* commands first.');
|
|
205
|
+
const mesh = _ops.extrudeProfile(entities, height, { symmetric, material: _ops.createMaterial(material) });
|
|
206
|
+
const id = nextId('extrude');
|
|
207
|
+
mesh.name = id;
|
|
208
|
+
_viewport.addToScene(mesh);
|
|
209
|
+
addFeature(id, 'extrude', mesh, { height, symmetric, material });
|
|
210
|
+
return { id, type: 'extrude', height, material, bbox: getBBox(mesh) };
|
|
211
|
+
},
|
|
212
|
+
|
|
213
|
+
'ops.revolve': ({ axis = 'Y', angle = 360, material = 'steel' }) => {
|
|
214
|
+
const entities = _sketch.getEntities();
|
|
215
|
+
if (entities.length === 0) throw new Error('No sketch entities to revolve.');
|
|
216
|
+
const mesh = _ops.revolveProfile(entities, { type: axis }, { angle: THREE.MathUtils.degToRad(angle), material: _ops.createMaterial(material) });
|
|
217
|
+
const id = nextId('revolve');
|
|
218
|
+
mesh.name = id;
|
|
219
|
+
_viewport.addToScene(mesh);
|
|
220
|
+
addFeature(id, 'revolve', mesh, { axis, angle, material });
|
|
221
|
+
return { id, type: 'revolve', axis, angle, material, bbox: getBBox(mesh) };
|
|
222
|
+
},
|
|
223
|
+
|
|
224
|
+
'ops.primitive': ({ shape, width, height, depth, radius, segments, material = 'steel' }) => {
|
|
225
|
+
requireAll({ shape });
|
|
226
|
+
const mesh = _ops.createPrimitive(shape, { width, height, depth, radius, segments }, { material: _ops.createMaterial(material) });
|
|
227
|
+
const id = nextId(shape);
|
|
228
|
+
mesh.name = id;
|
|
229
|
+
_viewport.addToScene(mesh);
|
|
230
|
+
addFeature(id, 'primitive', mesh, { shape, width, height, depth, radius, material });
|
|
231
|
+
return { id, type: 'primitive', shape, material, bbox: getBBox(mesh) };
|
|
232
|
+
},
|
|
233
|
+
|
|
234
|
+
'ops.fillet': ({ target, radius = 0.1 }) => {
|
|
235
|
+
requireAll({ target });
|
|
236
|
+
const mesh = getFeatureMesh(target);
|
|
237
|
+
if (!mesh) throw new Error(`Feature "${target}" not found`);
|
|
238
|
+
const result = _ops.fillet(mesh, 'all', radius);
|
|
239
|
+
return { target, radius, applied: true };
|
|
240
|
+
},
|
|
241
|
+
|
|
242
|
+
'ops.chamfer': ({ target, distance = 0.1 }) => {
|
|
243
|
+
requireAll({ target });
|
|
244
|
+
const mesh = getFeatureMesh(target);
|
|
245
|
+
if (!mesh) throw new Error(`Feature "${target}" not found`);
|
|
246
|
+
_ops.chamfer(mesh, 'all', distance);
|
|
247
|
+
return { target, distance, applied: true };
|
|
248
|
+
},
|
|
249
|
+
|
|
250
|
+
'ops.boolean': ({ operation, targetA, targetB }) => {
|
|
251
|
+
requireAll({ operation, targetA, targetB });
|
|
252
|
+
const meshA = getFeatureMesh(targetA);
|
|
253
|
+
const meshB = getFeatureMesh(targetB);
|
|
254
|
+
if (!meshA) throw new Error(`Feature "${targetA}" not found`);
|
|
255
|
+
if (!meshB) throw new Error(`Feature "${targetB}" not found`);
|
|
256
|
+
let result;
|
|
257
|
+
if (operation === 'union') result = _ops.booleanUnion(meshA, meshB);
|
|
258
|
+
else if (operation === 'cut') result = _ops.booleanCut(meshA, meshB);
|
|
259
|
+
else if (operation === 'intersect') result = _ops.booleanIntersect(meshA, meshB);
|
|
260
|
+
else throw new Error(`Unknown boolean op: "${operation}". Use union|cut|intersect.`);
|
|
261
|
+
const id = nextId('bool');
|
|
262
|
+
result.name = id;
|
|
263
|
+
addFeature(id, 'boolean', result, { operation, targetA, targetB });
|
|
264
|
+
return { id, operation, bbox: getBBox(result) };
|
|
265
|
+
},
|
|
266
|
+
|
|
267
|
+
'ops.shell': ({ target, thickness = 0.1 }) => {
|
|
268
|
+
requireAll({ target });
|
|
269
|
+
const mesh = getFeatureMesh(target);
|
|
270
|
+
if (!mesh) throw new Error(`Feature "${target}" not found`);
|
|
271
|
+
_ops.createShell(mesh, thickness);
|
|
272
|
+
return { target, thickness, applied: true };
|
|
273
|
+
},
|
|
274
|
+
|
|
275
|
+
'ops.pattern': ({ target, type = 'rect', count = 3, spacing = 1 }) => {
|
|
276
|
+
requireAll({ target });
|
|
277
|
+
const mesh = getFeatureMesh(target);
|
|
278
|
+
if (!mesh) throw new Error(`Feature "${target}" not found`);
|
|
279
|
+
const clones = _ops.createPattern(mesh, type, count, spacing);
|
|
280
|
+
return { target, type, count, spacing, created: clones.length };
|
|
281
|
+
},
|
|
282
|
+
|
|
283
|
+
'ops.material': ({ target, material }) => {
|
|
284
|
+
requireAll({ target, material });
|
|
285
|
+
const mesh = getFeatureMesh(target);
|
|
286
|
+
if (!mesh) throw new Error(`Feature "${target}" not found`);
|
|
287
|
+
mesh.material = _ops.createMaterial(material);
|
|
288
|
+
return { target, material, applied: true };
|
|
289
|
+
},
|
|
290
|
+
|
|
291
|
+
// ==========================================================================
|
|
292
|
+
// ADVANCED OPS — sweep, loft, sheet metal, spring, thread
|
|
293
|
+
// ==========================================================================
|
|
294
|
+
|
|
295
|
+
'ops.sweep': ({ profile, path, twist = 0, scale = 1 }) => {
|
|
296
|
+
requireAll({ profile, path });
|
|
297
|
+
const mesh = _advancedOps.createSweep(profile, path, { twist, scale });
|
|
298
|
+
const id = nextId('sweep');
|
|
299
|
+
mesh.name = id;
|
|
300
|
+
_viewport.addToScene(mesh);
|
|
301
|
+
addFeature(id, 'sweep', mesh, { profile, path, twist, scale });
|
|
302
|
+
return { id, type: 'sweep', bbox: getBBox(mesh) };
|
|
303
|
+
},
|
|
304
|
+
|
|
305
|
+
'ops.loft': ({ profiles }) => {
|
|
306
|
+
requireAll({ profiles });
|
|
307
|
+
const mesh = _advancedOps.createLoft(profiles);
|
|
308
|
+
const id = nextId('loft');
|
|
309
|
+
mesh.name = id;
|
|
310
|
+
_viewport.addToScene(mesh);
|
|
311
|
+
addFeature(id, 'loft', mesh, { profiles });
|
|
312
|
+
return { id, type: 'loft', bbox: getBBox(mesh) };
|
|
313
|
+
},
|
|
314
|
+
|
|
315
|
+
'ops.spring': ({ radius = 5, wireRadius = 0.5, height = 20, turns = 5, material = 'steel' }) => {
|
|
316
|
+
const mesh = _advancedOps.createSpring(radius, wireRadius, height, turns, { material: _ops.createMaterial(material) });
|
|
317
|
+
const id = nextId('spring');
|
|
318
|
+
mesh.name = id;
|
|
319
|
+
_viewport.addToScene(mesh);
|
|
320
|
+
addFeature(id, 'spring', mesh, { radius, wireRadius, height, turns, material });
|
|
321
|
+
return { id, type: 'spring', bbox: getBBox(mesh) };
|
|
322
|
+
},
|
|
323
|
+
|
|
324
|
+
'ops.thread': ({ outerRadius = 5, innerRadius = 4.2, pitch = 1, length = 10, material = 'steel' }) => {
|
|
325
|
+
const mesh = _advancedOps.createThread(outerRadius, innerRadius, pitch, length, { material: _ops.createMaterial(material) });
|
|
326
|
+
const id = nextId('thread');
|
|
327
|
+
mesh.name = id;
|
|
328
|
+
_viewport.addToScene(mesh);
|
|
329
|
+
addFeature(id, 'thread', mesh, { outerRadius, innerRadius, pitch, length, material });
|
|
330
|
+
return { id, type: 'thread', bbox: getBBox(mesh) };
|
|
331
|
+
},
|
|
332
|
+
|
|
333
|
+
'ops.bend': ({ target, angle = 90, radius = 2 }) => {
|
|
334
|
+
requireAll({ target });
|
|
335
|
+
const mesh = getFeatureMesh(target);
|
|
336
|
+
if (!mesh) throw new Error(`Feature "${target}" not found`);
|
|
337
|
+
_advancedOps.createBend(mesh, null, THREE.MathUtils.degToRad(angle), radius);
|
|
338
|
+
return { target, angle, radius, applied: true };
|
|
339
|
+
},
|
|
340
|
+
|
|
341
|
+
// ==========================================================================
|
|
342
|
+
// TRANSFORM — move, rotate, scale
|
|
343
|
+
// ==========================================================================
|
|
344
|
+
|
|
345
|
+
'transform.move': ({ target, x = 0, y = 0, z = 0 }) => {
|
|
346
|
+
requireAll({ target });
|
|
347
|
+
const mesh = getFeatureMesh(target);
|
|
348
|
+
if (!mesh) throw new Error(`Feature "${target}" not found`);
|
|
349
|
+
mesh.position.set(
|
|
350
|
+
mesh.position.x + x,
|
|
351
|
+
mesh.position.y + y,
|
|
352
|
+
mesh.position.z + z
|
|
353
|
+
);
|
|
354
|
+
return { target, position: [mesh.position.x, mesh.position.y, mesh.position.z] };
|
|
355
|
+
},
|
|
356
|
+
|
|
357
|
+
'transform.rotate': ({ target, x = 0, y = 0, z = 0 }) => {
|
|
358
|
+
requireAll({ target });
|
|
359
|
+
const mesh = getFeatureMesh(target);
|
|
360
|
+
if (!mesh) throw new Error(`Feature "${target}" not found`);
|
|
361
|
+
mesh.rotation.x += THREE.MathUtils.degToRad(x);
|
|
362
|
+
mesh.rotation.y += THREE.MathUtils.degToRad(y);
|
|
363
|
+
mesh.rotation.z += THREE.MathUtils.degToRad(z);
|
|
364
|
+
return { target, rotation: [
|
|
365
|
+
THREE.MathUtils.radToDeg(mesh.rotation.x),
|
|
366
|
+
THREE.MathUtils.radToDeg(mesh.rotation.y),
|
|
367
|
+
THREE.MathUtils.radToDeg(mesh.rotation.z)
|
|
368
|
+
] };
|
|
369
|
+
},
|
|
370
|
+
|
|
371
|
+
'transform.scale': ({ target, x = 1, y = 1, z = 1 }) => {
|
|
372
|
+
requireAll({ target });
|
|
373
|
+
const mesh = getFeatureMesh(target);
|
|
374
|
+
if (!mesh) throw new Error(`Feature "${target}" not found`);
|
|
375
|
+
mesh.scale.set(mesh.scale.x * x, mesh.scale.y * y, mesh.scale.z * z);
|
|
376
|
+
return { target, scale: [mesh.scale.x, mesh.scale.y, mesh.scale.z] };
|
|
377
|
+
},
|
|
378
|
+
|
|
379
|
+
// ==========================================================================
|
|
380
|
+
// VIEWPORT — camera, view, display
|
|
381
|
+
// ==========================================================================
|
|
382
|
+
|
|
383
|
+
'view.set': ({ view = 'isometric' }) => {
|
|
384
|
+
_viewport.setView(view);
|
|
385
|
+
return { view };
|
|
386
|
+
},
|
|
387
|
+
|
|
388
|
+
'view.fit': ({ target }) => {
|
|
389
|
+
if (target) {
|
|
390
|
+
const mesh = getFeatureMesh(target);
|
|
391
|
+
if (mesh) _viewport.fitToObject(mesh);
|
|
392
|
+
} else {
|
|
393
|
+
_viewport.setView('isometric');
|
|
394
|
+
}
|
|
395
|
+
return { fitted: true };
|
|
396
|
+
},
|
|
397
|
+
|
|
398
|
+
'view.wireframe': ({ enabled = true }) => {
|
|
399
|
+
_viewport.toggleWireframe(enabled);
|
|
400
|
+
return { wireframe: enabled };
|
|
401
|
+
},
|
|
402
|
+
|
|
403
|
+
'view.grid': ({ visible = true }) => {
|
|
404
|
+
_viewport.toggleGrid(visible);
|
|
405
|
+
return { grid: visible };
|
|
406
|
+
},
|
|
407
|
+
|
|
408
|
+
// ==========================================================================
|
|
409
|
+
// EXPORT — STL, OBJ, glTF, JSON
|
|
410
|
+
// ==========================================================================
|
|
411
|
+
|
|
412
|
+
'export.stl': ({ filename = 'agent-output.stl', binary = true }) => {
|
|
413
|
+
const features = getAllFeatures();
|
|
414
|
+
if (features.length === 0) throw new Error('No features to export');
|
|
415
|
+
if (binary) {
|
|
416
|
+
_exportMod.exportSTLBinary(features, filename);
|
|
417
|
+
} else {
|
|
418
|
+
_exportMod.exportSTL(features, filename);
|
|
419
|
+
}
|
|
420
|
+
return { format: 'stl', binary, filename, featureCount: features.length };
|
|
421
|
+
},
|
|
422
|
+
|
|
423
|
+
'export.obj': ({ filename = 'agent-output.obj' }) => {
|
|
424
|
+
const features = getAllFeatures();
|
|
425
|
+
if (features.length === 0) throw new Error('No features to export');
|
|
426
|
+
_exportMod.exportOBJ(features, filename);
|
|
427
|
+
return { format: 'obj', filename, featureCount: features.length };
|
|
428
|
+
},
|
|
429
|
+
|
|
430
|
+
'export.gltf': ({ filename = 'agent-output.gltf' }) => {
|
|
431
|
+
const features = getAllFeatures();
|
|
432
|
+
if (features.length === 0) throw new Error('No features to export');
|
|
433
|
+
_exportMod.exportGLTF(features, filename);
|
|
434
|
+
return { format: 'gltf', filename, featureCount: features.length };
|
|
435
|
+
},
|
|
436
|
+
|
|
437
|
+
'export.json': ({ filename = 'agent-output.cyclecad.json' }) => {
|
|
438
|
+
const features = getAllFeatures();
|
|
439
|
+
if (features.length === 0) throw new Error('No features to export');
|
|
440
|
+
_exportMod.exportJSON(features, filename);
|
|
441
|
+
return { format: 'json', filename, featureCount: features.length };
|
|
442
|
+
},
|
|
443
|
+
|
|
444
|
+
// ==========================================================================
|
|
445
|
+
// QUERY — inspect the model, features, dimensions
|
|
446
|
+
// ==========================================================================
|
|
447
|
+
|
|
448
|
+
'query.features': () => {
|
|
449
|
+
const features = getAllFeatures();
|
|
450
|
+
return {
|
|
451
|
+
count: features.length,
|
|
452
|
+
features: features.map(f => ({
|
|
453
|
+
id: f.id,
|
|
454
|
+
type: f.type,
|
|
455
|
+
name: f.name || f.id,
|
|
456
|
+
bbox: f.mesh ? getBBox(f.mesh) : null,
|
|
457
|
+
params: f.params || {}
|
|
458
|
+
}))
|
|
459
|
+
};
|
|
460
|
+
},
|
|
461
|
+
|
|
462
|
+
'query.bbox': ({ target }) => {
|
|
463
|
+
requireAll({ target });
|
|
464
|
+
const mesh = getFeatureMesh(target);
|
|
465
|
+
if (!mesh) throw new Error(`Feature "${target}" not found`);
|
|
466
|
+
return getBBox(mesh);
|
|
467
|
+
},
|
|
468
|
+
|
|
469
|
+
'query.materials': () => {
|
|
470
|
+
return { materials: Object.keys(_ops.getMaterialPresets()) };
|
|
471
|
+
},
|
|
472
|
+
|
|
473
|
+
'query.session': () => {
|
|
474
|
+
return getSession();
|
|
475
|
+
},
|
|
476
|
+
|
|
477
|
+
'query.log': ({ last = 20 }) => {
|
|
478
|
+
return { commands: commandLog.slice(-last) };
|
|
479
|
+
},
|
|
480
|
+
|
|
481
|
+
// ==========================================================================
|
|
482
|
+
// VALIDATE — DFM checks, measurements, analysis
|
|
483
|
+
// ==========================================================================
|
|
484
|
+
|
|
485
|
+
'validate.dimensions': ({ target }) => {
|
|
486
|
+
requireAll({ target });
|
|
487
|
+
const mesh = getFeatureMesh(target);
|
|
488
|
+
if (!mesh) throw new Error(`Feature "${target}" not found`);
|
|
489
|
+
const bbox = getBBox(mesh);
|
|
490
|
+
const volume = bbox.width * bbox.height * bbox.depth;
|
|
491
|
+
return {
|
|
492
|
+
target,
|
|
493
|
+
width: bbox.width,
|
|
494
|
+
height: bbox.height,
|
|
495
|
+
depth: bbox.depth,
|
|
496
|
+
volume: round(volume),
|
|
497
|
+
diagonal: round(Math.sqrt(bbox.width ** 2 + bbox.height ** 2 + bbox.depth ** 2)),
|
|
498
|
+
fitsInPrintBed: bbox.width <= 250 && bbox.height <= 250 && bbox.depth <= 250,
|
|
499
|
+
printBedSize: [250, 250, 250]
|
|
500
|
+
};
|
|
501
|
+
},
|
|
502
|
+
|
|
503
|
+
'validate.wallThickness': ({ target, minWall = 0.8 }) => {
|
|
504
|
+
requireAll({ target });
|
|
505
|
+
const mesh = getFeatureMesh(target);
|
|
506
|
+
if (!mesh) throw new Error(`Feature "${target}" not found`);
|
|
507
|
+
const bbox = getBBox(mesh);
|
|
508
|
+
const minDim = Math.min(bbox.width, bbox.height, bbox.depth);
|
|
509
|
+
return {
|
|
510
|
+
target,
|
|
511
|
+
minDimension: round(minDim),
|
|
512
|
+
minWallRequired: minWall,
|
|
513
|
+
passes: minDim >= minWall,
|
|
514
|
+
recommendation: minDim < minWall ? `Minimum dimension ${round(minDim)}mm is below ${minWall}mm wall threshold` : 'OK'
|
|
515
|
+
};
|
|
516
|
+
},
|
|
517
|
+
|
|
518
|
+
'validate.printability': ({ target, process = 'FDM' }) => {
|
|
519
|
+
requireAll({ target });
|
|
520
|
+
const mesh = getFeatureMesh(target);
|
|
521
|
+
if (!mesh) throw new Error(`Feature "${target}" not found`);
|
|
522
|
+
const bbox = getBBox(mesh);
|
|
523
|
+
const volume = bbox.width * bbox.height * bbox.depth;
|
|
524
|
+
const issues = [];
|
|
525
|
+
|
|
526
|
+
// Size checks
|
|
527
|
+
const maxPrint = process === 'FDM' ? 250 : process === 'SLA' ? 150 : 300;
|
|
528
|
+
if (bbox.width > maxPrint || bbox.height > maxPrint || bbox.depth > maxPrint) {
|
|
529
|
+
issues.push(`Exceeds ${process} build volume (${maxPrint}mm)`);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Wall thickness
|
|
533
|
+
const minDim = Math.min(bbox.width, bbox.height, bbox.depth);
|
|
534
|
+
const minWall = process === 'FDM' ? 0.8 : process === 'SLA' ? 0.3 : 1.0;
|
|
535
|
+
if (minDim < minWall) {
|
|
536
|
+
issues.push(`Min dimension ${round(minDim)}mm below ${minWall}mm ${process} minimum`);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Aspect ratio (stability)
|
|
540
|
+
const maxDim = Math.max(bbox.width, bbox.height, bbox.depth);
|
|
541
|
+
if (maxDim / Math.max(minDim, 0.1) > 15) {
|
|
542
|
+
issues.push('High aspect ratio — may need supports or orientation change');
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
return {
|
|
546
|
+
target, process,
|
|
547
|
+
printable: issues.length === 0,
|
|
548
|
+
issues,
|
|
549
|
+
estimatedTime: `${Math.ceil(volume / 1000)}min`,
|
|
550
|
+
estimatedMaterial: `${round(volume * 0.0012)}g`,
|
|
551
|
+
orientation: bbox.height > bbox.width ? 'Consider rotating 90° for better bed adhesion' : 'OK'
|
|
552
|
+
};
|
|
553
|
+
},
|
|
554
|
+
|
|
555
|
+
'validate.cost': ({ target, process = 'FDM', material = 'PLA' }) => {
|
|
556
|
+
requireAll({ target });
|
|
557
|
+
const mesh = getFeatureMesh(target);
|
|
558
|
+
if (!mesh) throw new Error(`Feature "${target}" not found`);
|
|
559
|
+
const bbox = getBBox(mesh);
|
|
560
|
+
const volume = bbox.width * bbox.height * bbox.depth;
|
|
561
|
+
|
|
562
|
+
// Cost estimation per process
|
|
563
|
+
const rates = {
|
|
564
|
+
FDM: { perCm3: 0.05, setup: 2, perHour: 3 },
|
|
565
|
+
SLA: { perCm3: 0.15, setup: 5, perHour: 8 },
|
|
566
|
+
CNC: { perCm3: 0.30, setup: 25, perHour: 45 },
|
|
567
|
+
injection: { perCm3: 0.02, setup: 5000, perHour: 100 }
|
|
568
|
+
};
|
|
569
|
+
const rate = rates[process] || rates.FDM;
|
|
570
|
+
const volumeCm3 = volume / 1000;
|
|
571
|
+
const hours = volumeCm3 / 10;
|
|
572
|
+
const materialCost = round(volumeCm3 * rate.perCm3);
|
|
573
|
+
const machineCost = round(hours * rate.perHour);
|
|
574
|
+
const totalUnit = round(materialCost + machineCost + rate.setup);
|
|
575
|
+
|
|
576
|
+
return {
|
|
577
|
+
target, process, material,
|
|
578
|
+
volumeCm3: round(volumeCm3),
|
|
579
|
+
materialCost,
|
|
580
|
+
machineCost,
|
|
581
|
+
setupCost: rate.setup,
|
|
582
|
+
unitCost: totalUnit,
|
|
583
|
+
batchOf100: round((materialCost + machineCost) * 100 + rate.setup),
|
|
584
|
+
recommendation: process === 'injection' && volumeCm3 < 50
|
|
585
|
+
? 'Volume too low for injection molding — consider FDM or CNC'
|
|
586
|
+
: 'OK'
|
|
587
|
+
};
|
|
588
|
+
},
|
|
589
|
+
|
|
590
|
+
// ==========================================================================
|
|
591
|
+
// SCENE — full scene management
|
|
592
|
+
// ==========================================================================
|
|
593
|
+
|
|
594
|
+
'scene.clear': () => {
|
|
595
|
+
const features = getAllFeatures();
|
|
596
|
+
features.forEach(f => {
|
|
597
|
+
if (f.mesh) _viewport.removeFromScene(f.mesh);
|
|
598
|
+
});
|
|
599
|
+
if (_appState.clearFeatures) _appState.clearFeatures();
|
|
600
|
+
else if (_appState.features) _appState.features.length = 0;
|
|
601
|
+
_sketch.clearSketch();
|
|
602
|
+
featureIndex = 0;
|
|
603
|
+
return { cleared: true, removed: features.length };
|
|
604
|
+
},
|
|
605
|
+
|
|
606
|
+
'scene.snapshot': () => {
|
|
607
|
+
const renderer = _viewport.getRenderer();
|
|
608
|
+
if (!renderer) throw new Error('Renderer not available');
|
|
609
|
+
renderer.render(_viewport.getScene(), _viewport.getCamera());
|
|
610
|
+
const dataUrl = renderer.domElement.toDataURL('image/png');
|
|
611
|
+
return { format: 'png', dataUrl, width: renderer.domElement.width, height: renderer.domElement.height };
|
|
612
|
+
},
|
|
613
|
+
|
|
614
|
+
// ==========================================================================
|
|
615
|
+
// RENDER — Visual feedback loop (CAD Agent pattern)
|
|
616
|
+
// Agent sends commands → gets PNG renders back → evaluates → iterates
|
|
617
|
+
// ==========================================================================
|
|
618
|
+
|
|
619
|
+
'render.snapshot': ({ width = 800, height = 600 }) => {
|
|
620
|
+
const renderer = _viewport.getRenderer();
|
|
621
|
+
const scene = _viewport.getScene();
|
|
622
|
+
const cam = _viewport.getCamera();
|
|
623
|
+
if (!renderer) throw new Error('Renderer not available');
|
|
624
|
+
// Render at requested resolution
|
|
625
|
+
const origW = renderer.domElement.width;
|
|
626
|
+
const origH = renderer.domElement.height;
|
|
627
|
+
renderer.setSize(width, height);
|
|
628
|
+
cam.aspect = width / height;
|
|
629
|
+
cam.updateProjectionMatrix();
|
|
630
|
+
renderer.render(scene, cam);
|
|
631
|
+
const dataUrl = renderer.domElement.toDataURL('image/png');
|
|
632
|
+
// Restore
|
|
633
|
+
renderer.setSize(origW, origH);
|
|
634
|
+
cam.aspect = origW / origH;
|
|
635
|
+
cam.updateProjectionMatrix();
|
|
636
|
+
return { format: 'png', width, height, dataUrl };
|
|
637
|
+
},
|
|
638
|
+
|
|
639
|
+
'render.multiview': ({ width = 400, height = 300 }) => {
|
|
640
|
+
const renderer = _viewport.getRenderer();
|
|
641
|
+
const scene = _viewport.getScene();
|
|
642
|
+
const cam = _viewport.getCamera();
|
|
643
|
+
if (!renderer) throw new Error('Renderer not available');
|
|
644
|
+
const origPos = cam.position.clone();
|
|
645
|
+
const origTarget = new THREE.Vector3();
|
|
646
|
+
// Capture 6 standard views
|
|
647
|
+
const views = {
|
|
648
|
+
front: [0, 0, 3000], back: [0, 0, -3000],
|
|
649
|
+
right: [3000, 0, 0], left: [-3000, 0, 0],
|
|
650
|
+
top: [0, 3000, 0], isometric: [2000, 1500, 2000]
|
|
651
|
+
};
|
|
652
|
+
const renders = {};
|
|
653
|
+
const origW = renderer.domElement.width;
|
|
654
|
+
const origH = renderer.domElement.height;
|
|
655
|
+
renderer.setSize(width, height);
|
|
656
|
+
cam.aspect = width / height;
|
|
657
|
+
cam.updateProjectionMatrix();
|
|
658
|
+
for (const [name, pos] of Object.entries(views)) {
|
|
659
|
+
cam.position.set(...pos);
|
|
660
|
+
cam.lookAt(0, 0, 0);
|
|
661
|
+
renderer.render(scene, cam);
|
|
662
|
+
renders[name] = renderer.domElement.toDataURL('image/png');
|
|
663
|
+
}
|
|
664
|
+
// Restore camera
|
|
665
|
+
cam.position.copy(origPos);
|
|
666
|
+
renderer.setSize(origW, origH);
|
|
667
|
+
cam.aspect = origW / origH;
|
|
668
|
+
cam.updateProjectionMatrix();
|
|
669
|
+
return { viewCount: 6, width, height, views: Object.keys(renders), renders };
|
|
670
|
+
},
|
|
671
|
+
|
|
672
|
+
// ==========================================================================
|
|
673
|
+
// ENGINEERING CALCS — mass, surface area, center of mass
|
|
674
|
+
// ==========================================================================
|
|
675
|
+
|
|
676
|
+
'validate.mass': ({ target, material = 'steel' }) => {
|
|
677
|
+
requireAll({ target });
|
|
678
|
+
const mesh = getFeatureMesh(target);
|
|
679
|
+
if (!mesh) throw new Error(`Feature "${target}" not found`);
|
|
680
|
+
const bbox = getBBox(mesh);
|
|
681
|
+
const volumeCm3 = (bbox.width * bbox.height * bbox.depth) / 1000;
|
|
682
|
+
// Material densities (g/cm³)
|
|
683
|
+
const densities = { steel: 7.85, aluminum: 2.70, plastic: 1.04, brass: 8.50, titanium: 4.51, nylon: 1.14, copper: 8.96, wood: 0.6, pla: 1.24, abs: 1.05 };
|
|
684
|
+
const density = densities[material] || densities.steel;
|
|
685
|
+
// Approximate fill factor (solid shapes ~0.65 of bbox, complex shapes less)
|
|
686
|
+
const fillFactor = 0.65;
|
|
687
|
+
const massG = round(volumeCm3 * density * fillFactor);
|
|
688
|
+
return {
|
|
689
|
+
target, material,
|
|
690
|
+
density: density + ' g/cm³',
|
|
691
|
+
bboxVolumeCm3: round(volumeCm3),
|
|
692
|
+
estimatedFill: fillFactor,
|
|
693
|
+
massGrams: massG,
|
|
694
|
+
massKg: round(massG / 1000),
|
|
695
|
+
massLbs: round(massG / 453.592)
|
|
696
|
+
};
|
|
697
|
+
},
|
|
698
|
+
|
|
699
|
+
'validate.surfaceArea': ({ target }) => {
|
|
700
|
+
requireAll({ target });
|
|
701
|
+
const mesh = getFeatureMesh(target);
|
|
702
|
+
if (!mesh) throw new Error(`Feature "${target}" not found`);
|
|
703
|
+
// Calculate surface area from mesh triangles
|
|
704
|
+
const geo = mesh.geometry;
|
|
705
|
+
if (!geo) throw new Error('No geometry on mesh');
|
|
706
|
+
const pos = geo.getAttribute('position');
|
|
707
|
+
const idx = geo.getIndex();
|
|
708
|
+
let area = 0;
|
|
709
|
+
const vA = new THREE.Vector3(), vB = new THREE.Vector3(), vC = new THREE.Vector3();
|
|
710
|
+
const triCount = idx ? idx.count / 3 : pos.count / 3;
|
|
711
|
+
for (let i = 0; i < triCount; i++) {
|
|
712
|
+
if (idx) {
|
|
713
|
+
vA.fromBufferAttribute(pos, idx.getX(i * 3));
|
|
714
|
+
vB.fromBufferAttribute(pos, idx.getX(i * 3 + 1));
|
|
715
|
+
vC.fromBufferAttribute(pos, idx.getX(i * 3 + 2));
|
|
716
|
+
} else {
|
|
717
|
+
vA.fromBufferAttribute(pos, i * 3);
|
|
718
|
+
vB.fromBufferAttribute(pos, i * 3 + 1);
|
|
719
|
+
vC.fromBufferAttribute(pos, i * 3 + 2);
|
|
720
|
+
}
|
|
721
|
+
const edge1 = new THREE.Vector3().subVectors(vB, vA);
|
|
722
|
+
const edge2 = new THREE.Vector3().subVectors(vC, vA);
|
|
723
|
+
area += edge1.cross(edge2).length() * 0.5;
|
|
724
|
+
}
|
|
725
|
+
// Apply world transforms
|
|
726
|
+
const scale = mesh.scale;
|
|
727
|
+
area *= scale.x * scale.y; // approximate for uniform scale
|
|
728
|
+
return {
|
|
729
|
+
target,
|
|
730
|
+
surfaceAreaMm2: round(area),
|
|
731
|
+
surfaceAreaCm2: round(area / 100),
|
|
732
|
+
surfaceAreaM2: round(area / 1000000),
|
|
733
|
+
triangles: triCount
|
|
734
|
+
};
|
|
735
|
+
},
|
|
736
|
+
|
|
737
|
+
'validate.centerOfMass': ({ target }) => {
|
|
738
|
+
requireAll({ target });
|
|
739
|
+
const mesh = getFeatureMesh(target);
|
|
740
|
+
if (!mesh) throw new Error(`Feature "${target}" not found`);
|
|
741
|
+
const geo = mesh.geometry;
|
|
742
|
+
if (!geo) throw new Error('No geometry on mesh');
|
|
743
|
+
geo.computeBoundingBox();
|
|
744
|
+
const center = new THREE.Vector3();
|
|
745
|
+
geo.boundingBox.getCenter(center);
|
|
746
|
+
// Apply mesh world transform
|
|
747
|
+
center.applyMatrix4(mesh.matrixWorld);
|
|
748
|
+
return {
|
|
749
|
+
target,
|
|
750
|
+
centerOfMass: [round(center.x), round(center.y), round(center.z)],
|
|
751
|
+
note: 'Assumes uniform density — geometric centroid of mesh'
|
|
752
|
+
};
|
|
753
|
+
},
|
|
754
|
+
|
|
755
|
+
// ==========================================================================
|
|
756
|
+
// DESIGN REVIEW — auto-analyze CAD for issues (Matsuo Lab pattern)
|
|
757
|
+
// ==========================================================================
|
|
758
|
+
|
|
759
|
+
'validate.designReview': ({ target }) => {
|
|
760
|
+
requireAll({ target });
|
|
761
|
+
const mesh = getFeatureMesh(target);
|
|
762
|
+
if (!mesh) throw new Error(`Feature "${target}" not found`);
|
|
763
|
+
const bbox = getBBox(mesh);
|
|
764
|
+
const issues = [];
|
|
765
|
+
const warnings = [];
|
|
766
|
+
const passed = [];
|
|
767
|
+
const volume = bbox.width * bbox.height * bbox.depth;
|
|
768
|
+
const minDim = Math.min(bbox.width, bbox.height, bbox.depth);
|
|
769
|
+
const maxDim = Math.max(bbox.width, bbox.height, bbox.depth);
|
|
770
|
+
const aspectRatio = maxDim / Math.max(minDim, 0.01);
|
|
771
|
+
|
|
772
|
+
// 1. Geometry sanity
|
|
773
|
+
const geo = mesh.geometry;
|
|
774
|
+
const triCount = geo.getIndex() ? geo.getIndex().count / 3 : geo.getAttribute('position').count / 3;
|
|
775
|
+
if (triCount < 4) issues.push({ severity: 'error', check: 'geometry', msg: 'Too few triangles — degenerate geometry' });
|
|
776
|
+
else passed.push('Geometry: valid mesh');
|
|
777
|
+
|
|
778
|
+
if (triCount > 500000) warnings.push({ severity: 'warn', check: 'complexity', msg: `High triangle count (${triCount}) — may impact performance` });
|
|
779
|
+
else passed.push(`Complexity: ${triCount} triangles OK`);
|
|
780
|
+
|
|
781
|
+
// 2. Dimensions
|
|
782
|
+
if (minDim < 0.1) issues.push({ severity: 'error', check: 'dimensions', msg: `Minimum dimension ${round(minDim)}mm — likely too thin to manufacture` });
|
|
783
|
+
else if (minDim < 0.8) warnings.push({ severity: 'warn', check: 'wallThickness', msg: `Min dimension ${round(minDim)}mm — below FDM minimum (0.8mm)` });
|
|
784
|
+
else passed.push(`Wall thickness: ${round(minDim)}mm OK`);
|
|
785
|
+
|
|
786
|
+
// 3. Aspect ratio
|
|
787
|
+
if (aspectRatio > 20) issues.push({ severity: 'error', check: 'aspectRatio', msg: `Extreme aspect ratio ${round(aspectRatio)}:1 — part will be fragile or warp` });
|
|
788
|
+
else if (aspectRatio > 10) warnings.push({ severity: 'warn', check: 'aspectRatio', msg: `High aspect ratio ${round(aspectRatio)}:1 — consider supports` });
|
|
789
|
+
else passed.push(`Aspect ratio: ${round(aspectRatio)}:1 OK`);
|
|
790
|
+
|
|
791
|
+
// 4. Size
|
|
792
|
+
if (maxDim > 500) warnings.push({ severity: 'warn', check: 'size', msg: `Part exceeds 500mm (${round(maxDim)}mm) — check build volume for your process` });
|
|
793
|
+
else passed.push(`Size: ${round(maxDim)}mm max dimension OK`);
|
|
794
|
+
|
|
795
|
+
// 5. Volume check
|
|
796
|
+
if (volume < 1) issues.push({ severity: 'error', check: 'volume', msg: 'Near-zero volume — possibly a surface, not a solid' });
|
|
797
|
+
else passed.push(`Volume: ${round(volume / 1000)}cm³ OK`);
|
|
798
|
+
|
|
799
|
+
// 6. Origin check (center of mass far from origin)
|
|
800
|
+
geo.computeBoundingBox();
|
|
801
|
+
const center = new THREE.Vector3();
|
|
802
|
+
geo.boundingBox.getCenter(center);
|
|
803
|
+
const distFromOrigin = center.length();
|
|
804
|
+
if (distFromOrigin > maxDim * 5) warnings.push({ severity: 'warn', check: 'origin', msg: `Part center is ${round(distFromOrigin)}mm from origin — may cause precision issues` });
|
|
805
|
+
else passed.push('Origin: part near world origin');
|
|
806
|
+
|
|
807
|
+
// Score
|
|
808
|
+
const score = issues.length === 0 && warnings.length === 0 ? 'A'
|
|
809
|
+
: issues.length === 0 && warnings.length <= 2 ? 'B'
|
|
810
|
+
: issues.length === 0 ? 'C'
|
|
811
|
+
: 'F';
|
|
812
|
+
|
|
813
|
+
return {
|
|
814
|
+
target,
|
|
815
|
+
score,
|
|
816
|
+
summary: `${passed.length} passed, ${warnings.length} warnings, ${issues.length} errors`,
|
|
817
|
+
passed,
|
|
818
|
+
warnings,
|
|
819
|
+
issues,
|
|
820
|
+
recommendation: issues.length > 0
|
|
821
|
+
? 'Fix errors before manufacturing'
|
|
822
|
+
: warnings.length > 0
|
|
823
|
+
? 'Review warnings — part may need adjustments'
|
|
824
|
+
: 'Design review passed — ready for manufacturing'
|
|
825
|
+
};
|
|
826
|
+
},
|
|
827
|
+
|
|
828
|
+
// ==========================================================================
|
|
829
|
+
// META — API info, health, schema
|
|
830
|
+
// ==========================================================================
|
|
831
|
+
|
|
832
|
+
'meta.ping': () => {
|
|
833
|
+
return { pong: true, timestamp: Date.now(), session: sessionId };
|
|
834
|
+
},
|
|
835
|
+
|
|
836
|
+
'meta.version': () => {
|
|
837
|
+
return {
|
|
838
|
+
product: 'cycleCAD',
|
|
839
|
+
tagline: 'The Agent-First OS for Manufacturing',
|
|
840
|
+
apiVersion: '1.0.0',
|
|
841
|
+
modules: ['sketch', 'operations', 'advanced-ops', 'export', 'viewport', 'validate'],
|
|
842
|
+
commandCount: Object.keys(COMMANDS).length
|
|
843
|
+
};
|
|
844
|
+
},
|
|
845
|
+
|
|
846
|
+
'meta.schema': () => {
|
|
847
|
+
return getSchema();
|
|
848
|
+
},
|
|
849
|
+
};
|
|
850
|
+
|
|
851
|
+
// ============================================================================
|
|
852
|
+
// SCHEMA — self-describing API for agent discovery
|
|
853
|
+
// ============================================================================
|
|
854
|
+
|
|
855
|
+
export function getSchema() {
|
|
856
|
+
return {
|
|
857
|
+
description: 'cycleCAD Agent API — The Agent-First OS for Manufacturing',
|
|
858
|
+
version: '1.0.0',
|
|
859
|
+
protocol: 'JSON commands via window.cycleCAD.execute({ method, params })',
|
|
860
|
+
namespaces: {
|
|
861
|
+
sketch: {
|
|
862
|
+
description: '2D drawing on a construction plane',
|
|
863
|
+
methods: {
|
|
864
|
+
'sketch.start': { params: { plane: 'XY|XZ|YZ' }, description: 'Start sketch mode' },
|
|
865
|
+
'sketch.end': { params: {}, description: 'End sketch, return entities' },
|
|
866
|
+
'sketch.line': { params: { x1: 'number', y1: 'number', x2: 'number', y2: 'number' }, description: 'Draw a line' },
|
|
867
|
+
'sketch.rect': { params: { x: 'number?', y: 'number?', width: 'number', height: 'number' }, description: 'Draw a rectangle' },
|
|
868
|
+
'sketch.circle': { params: { cx: 'number?', cy: 'number?', radius: 'number' }, description: 'Draw a circle' },
|
|
869
|
+
'sketch.arc': { params: { cx: 'number?', cy: 'number?', radius: 'number', startAngle: 'number?', endAngle: 'number?' }, description: 'Draw an arc' },
|
|
870
|
+
'sketch.clear': { params: {}, description: 'Clear all sketch entities' },
|
|
871
|
+
'sketch.entities': { params: {}, description: 'List current sketch entities' },
|
|
872
|
+
}
|
|
873
|
+
},
|
|
874
|
+
ops: {
|
|
875
|
+
description: '3D modeling operations',
|
|
876
|
+
methods: {
|
|
877
|
+
'ops.extrude': { params: { height: 'number', symmetric: 'bool?', material: 'string?' }, description: 'Extrude sketch into 3D solid' },
|
|
878
|
+
'ops.revolve': { params: { axis: 'X|Y|Z?', angle: 'degrees?', material: 'string?' }, description: 'Revolve sketch around axis' },
|
|
879
|
+
'ops.primitive': { params: { shape: 'box|sphere|cylinder|cone|torus', width: 'n?', height: 'n?', depth: 'n?', radius: 'n?', material: 'string?' }, description: 'Create a primitive shape' },
|
|
880
|
+
'ops.fillet': { params: { target: 'featureId', radius: 'number?' }, description: 'Fillet edges' },
|
|
881
|
+
'ops.chamfer': { params: { target: 'featureId', distance: 'number?' }, description: 'Chamfer edges' },
|
|
882
|
+
'ops.boolean': { params: { operation: 'union|cut|intersect', targetA: 'featureId', targetB: 'featureId' }, description: 'Boolean operation' },
|
|
883
|
+
'ops.shell': { params: { target: 'featureId', thickness: 'number?' }, description: 'Shell (hollow) a solid' },
|
|
884
|
+
'ops.pattern': { params: { target: 'featureId', type: 'rect|circular?', count: 'number?', spacing: 'number?' }, description: 'Pattern repeat' },
|
|
885
|
+
'ops.material': { params: { target: 'featureId', material: 'string' }, description: 'Change material' },
|
|
886
|
+
'ops.sweep': { params: { profile: 'object', path: 'object', twist: 'number?', scale: 'number?' }, description: 'Sweep profile along path' },
|
|
887
|
+
'ops.loft': { params: { profiles: 'array' }, description: 'Loft between profiles' },
|
|
888
|
+
'ops.spring': { params: { radius: 'n?', wireRadius: 'n?', height: 'n?', turns: 'n?', material: 'string?' }, description: 'Generate a spring' },
|
|
889
|
+
'ops.thread': { params: { outerRadius: 'n?', innerRadius: 'n?', pitch: 'n?', length: 'n?', material: 'string?' }, description: 'Generate a thread' },
|
|
890
|
+
'ops.bend': { params: { target: 'featureId', angle: 'degrees?', radius: 'number?' }, description: 'Sheet metal bend' },
|
|
891
|
+
}
|
|
892
|
+
},
|
|
893
|
+
transform: {
|
|
894
|
+
description: 'Move, rotate, scale features',
|
|
895
|
+
methods: {
|
|
896
|
+
'transform.move': { params: { target: 'featureId', x: 'n?', y: 'n?', z: 'n?' }, description: 'Translate' },
|
|
897
|
+
'transform.rotate': { params: { target: 'featureId', x: 'deg?', y: 'deg?', z: 'deg?' }, description: 'Rotate' },
|
|
898
|
+
'transform.scale': { params: { target: 'featureId', x: 'n?', y: 'n?', z: 'n?' }, description: 'Scale' },
|
|
899
|
+
}
|
|
900
|
+
},
|
|
901
|
+
view: {
|
|
902
|
+
description: 'Camera and display',
|
|
903
|
+
methods: {
|
|
904
|
+
'view.set': { params: { view: 'front|back|left|right|top|bottom|isometric' }, description: 'Set camera view' },
|
|
905
|
+
'view.fit': { params: { target: 'featureId?' }, description: 'Fit view to object' },
|
|
906
|
+
'view.wireframe': { params: { enabled: 'bool?' }, description: 'Toggle wireframe' },
|
|
907
|
+
'view.grid': { params: { visible: 'bool?' }, description: 'Toggle grid' },
|
|
908
|
+
}
|
|
909
|
+
},
|
|
910
|
+
export: {
|
|
911
|
+
description: 'Export model files',
|
|
912
|
+
methods: {
|
|
913
|
+
'export.stl': { params: { filename: 'string?', binary: 'bool?' }, description: 'Export STL' },
|
|
914
|
+
'export.obj': { params: { filename: 'string?' }, description: 'Export OBJ' },
|
|
915
|
+
'export.gltf': { params: { filename: 'string?' }, description: 'Export glTF' },
|
|
916
|
+
'export.json': { params: { filename: 'string?' }, description: 'Export cycleCAD JSON' },
|
|
917
|
+
}
|
|
918
|
+
},
|
|
919
|
+
validate: {
|
|
920
|
+
description: 'DFM checks, cost estimation, engineering analysis, design review',
|
|
921
|
+
methods: {
|
|
922
|
+
'validate.dimensions': { params: { target: 'featureId' }, description: 'Get part dimensions' },
|
|
923
|
+
'validate.wallThickness': { params: { target: 'featureId', minWall: 'mm?' }, description: 'Check wall thickness' },
|
|
924
|
+
'validate.printability': { params: { target: 'featureId', process: 'FDM|SLA|CNC?' }, description: 'Check printability' },
|
|
925
|
+
'validate.cost': { params: { target: 'featureId', process: 'FDM|SLA|CNC|injection?', material: 'string?' }, description: 'Estimate manufacturing cost' },
|
|
926
|
+
'validate.mass': { params: { target: 'featureId', material: 'string?' }, description: 'Estimate part mass (weight)' },
|
|
927
|
+
'validate.surfaceArea': { params: { target: 'featureId' }, description: 'Calculate surface area from mesh' },
|
|
928
|
+
'validate.centerOfMass': { params: { target: 'featureId' }, description: 'Get center of mass (geometric centroid)' },
|
|
929
|
+
'validate.designReview': { params: { target: 'featureId' }, description: 'Auto-analyze for manufacturing issues — scored A/B/C/F' },
|
|
930
|
+
}
|
|
931
|
+
},
|
|
932
|
+
render: {
|
|
933
|
+
description: 'Visual feedback loop — agents see what they built',
|
|
934
|
+
methods: {
|
|
935
|
+
'render.snapshot': { params: { width: 'number?', height: 'number?' }, description: 'Render current view as PNG at specified resolution' },
|
|
936
|
+
'render.multiview': { params: { width: 'number?', height: 'number?' }, description: 'Render 6 standard views (front/back/left/right/top/isometric) as PNGs' },
|
|
937
|
+
}
|
|
938
|
+
},
|
|
939
|
+
query: {
|
|
940
|
+
description: 'Inspect model state',
|
|
941
|
+
methods: {
|
|
942
|
+
'query.features': { params: {}, description: 'List all features' },
|
|
943
|
+
'query.bbox': { params: { target: 'featureId' }, description: 'Get bounding box' },
|
|
944
|
+
'query.materials': { params: {}, description: 'List available materials' },
|
|
945
|
+
'query.session': { params: {}, description: 'Session info' },
|
|
946
|
+
'query.log': { params: { last: 'number?' }, description: 'Recent command log' },
|
|
947
|
+
}
|
|
948
|
+
},
|
|
949
|
+
scene: {
|
|
950
|
+
description: 'Scene management',
|
|
951
|
+
methods: {
|
|
952
|
+
'scene.clear': { params: {}, description: 'Clear all features' },
|
|
953
|
+
'scene.snapshot': { params: {}, description: 'Capture viewport as PNG (legacy — use render.snapshot)' },
|
|
954
|
+
}
|
|
955
|
+
},
|
|
956
|
+
meta: {
|
|
957
|
+
description: 'API info',
|
|
958
|
+
methods: {
|
|
959
|
+
'meta.ping': { params: {}, description: 'Health check' },
|
|
960
|
+
'meta.version': { params: {}, description: 'Version info' },
|
|
961
|
+
'meta.schema': { params: {}, description: 'Full API schema' },
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
},
|
|
965
|
+
example: {
|
|
966
|
+
description: 'Design a bracket with 4 bolt holes',
|
|
967
|
+
commands: [
|
|
968
|
+
{ method: 'sketch.start', params: { plane: 'XY' } },
|
|
969
|
+
{ method: 'sketch.rect', params: { width: 80, height: 40 } },
|
|
970
|
+
{ method: 'ops.extrude', params: { height: 5, material: 'aluminum' } },
|
|
971
|
+
{ method: 'validate.printability', params: { target: 'extrude_1', process: 'CNC' } },
|
|
972
|
+
{ method: 'validate.cost', params: { target: 'extrude_1', process: 'CNC', material: 'aluminum' } },
|
|
973
|
+
{ method: 'export.stl', params: { filename: 'bracket.stl' } },
|
|
974
|
+
]
|
|
975
|
+
}
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
// ============================================================================
|
|
980
|
+
// SESSION STATE
|
|
981
|
+
// ============================================================================
|
|
982
|
+
|
|
983
|
+
export function getState() {
|
|
984
|
+
return {
|
|
985
|
+
session: sessionId,
|
|
986
|
+
featureCount: getAllFeatures().length,
|
|
987
|
+
commandCount: commandLog.length,
|
|
988
|
+
features: getAllFeatures().map(f => ({
|
|
989
|
+
id: f.id, type: f.type, bbox: f.mesh ? getBBox(f.mesh) : null
|
|
990
|
+
}))
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
export function getSession() {
|
|
995
|
+
return {
|
|
996
|
+
sessionId,
|
|
997
|
+
commandsExecuted: commandLog.length,
|
|
998
|
+
featureCount: getAllFeatures().length,
|
|
999
|
+
uptime: Math.round(performance.now() / 1000),
|
|
1000
|
+
api: `window.cycleCAD.execute({ method: "meta.ping" })`
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
// ============================================================================
|
|
1005
|
+
// UTILS
|
|
1006
|
+
// ============================================================================
|
|
1007
|
+
|
|
1008
|
+
function requireAll(params) {
|
|
1009
|
+
for (const [key, val] of Object.entries(params)) {
|
|
1010
|
+
if (val === undefined || val === null) {
|
|
1011
|
+
throw new Error(`Required parameter "${key}" is missing`);
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
function getBBox(mesh) {
|
|
1017
|
+
const box = new THREE.Box3().setFromObject(mesh);
|
|
1018
|
+
const size = new THREE.Vector3();
|
|
1019
|
+
box.getSize(size);
|
|
1020
|
+
const center = new THREE.Vector3();
|
|
1021
|
+
box.getCenter(center);
|
|
1022
|
+
return {
|
|
1023
|
+
width: round(size.x),
|
|
1024
|
+
height: round(size.y),
|
|
1025
|
+
depth: round(size.z),
|
|
1026
|
+
center: [round(center.x), round(center.y), round(center.z)],
|
|
1027
|
+
min: [round(box.min.x), round(box.min.y), round(box.min.z)],
|
|
1028
|
+
max: [round(box.max.x), round(box.max.y), round(box.max.z)]
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
function round(n) { return Math.round(n * 100) / 100; }
|
|
1033
|
+
|
|
1034
|
+
function summarizeEntity(e) {
|
|
1035
|
+
const s = { type: e.type, id: e.id };
|
|
1036
|
+
if (e.type === 'line') { s.from = [e.x1, e.y1]; s.to = [e.x2, e.y2]; }
|
|
1037
|
+
if (e.type === 'circle') { s.center = [e.cx, e.cy]; s.radius = e.radius; }
|
|
1038
|
+
if (e.type === 'arc') { s.center = [e.cx, e.cy]; s.radius = e.radius; }
|
|
1039
|
+
return s;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
function addFeature(id, type, mesh, params) {
|
|
1043
|
+
if (_appState.addFeature) {
|
|
1044
|
+
_appState.addFeature({ id, type, name: id, mesh, params });
|
|
1045
|
+
} else if (_appState.features) {
|
|
1046
|
+
_appState.features.push({ id, type, name: id, mesh, params });
|
|
1047
|
+
}
|
|
1048
|
+
}
|