Skip to content

Commit 2ff9047

Browse files
zaloclaude
andcommitted
Add class refactor, OpenSCAD mode, and agent API
Phase 1 — Class-based refactor: Extract subsystems from monolithic CascadeMain.js (652→337 lines) into MessageBus, EditorManager, ConsoleManager, and GUIManager classes. CascadeView/CascadeViewHandles now accept MessageBus instead of raw messageHandlers. Worker propagates requestId for Promise-based request/response. Phase 2 — OpenSCAD parsing mode: Add openscad-parser dependency with postinstall CJS→ESM bundling. Create OpenSCADTranspiler (660 lines) that parses OpenSCAD and generates CascadeStudio JS, covering primitives, transforms, booleans, extrusions, control flow, and module/function declarations. Create OpenSCADMonaco (355 lines) with Monarch tokenizer, completion, diagnostics, hover, and go-to-definition. Add mode toggle dropdown in the top nav. Phase 3 — CLI agent integration: Create window.CascadeAPI with setCode/getCode/evaluate, getSTEP/getSTL/getOBJ, screenshot, getConsoleLog/getErrors, and self-documenting getCapabilities/ getExamples methods. Add <meta name="cascade-api"> for agent discovery. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 605ed8a commit 2ff9047

19 files changed

Lines changed: 7923 additions & 404 deletions

index.html

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
<meta name="keywords" content="SCAD, OpenSCAD, CAD, OpenCascade, Scripting">
99
<meta name="author" content="Johnathon Selstad">
1010
<meta name="viewport" content="width=device-width, initial-scale=1.0">
11+
<meta name="cascade-api" content="window.CascadeAPI">
1112
<meta name="theme-color" content="#1e1e1e">
1213

1314
<!-- Service Worker for offline access (must be first) -->
@@ -67,6 +68,10 @@ <h1 hidden></h1>
6768
<input id="files" name="files" type="file" accept=".iges,.step,.igs,.stp,.stl" multiple style="display:none;" oninput="window.loadFiles();"/>
6869
</label>
6970
<a href="#" title="Clears the external step/iges/stl files stored in the project." onmouseup="window.clearExternalFiles();">Clear Imported Files</a>
71+
<select id="editorMode" title="Editor Language Mode" style="margin-left:10px; background:#333; color:#ccc; border:1px solid #555; padding:2px 4px; font-size:12px;">
72+
<option value="cascadestudio">CascadeStudio JS</option>
73+
<option value="openscad">OpenSCAD</option>
74+
</select>
7075
</div>
7176
<div id="appbody" style="height:auto">
7277
<script type="module" src="./js/MainPage/main.js"></script>

js/CADWorker/CascadeStudioMainWorker.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,11 @@ class CascadeStudioWorker {
116116
// Route incoming messages to registered handlers
117117
onmessage = function (e) {
118118
let response = self.messageHandlers[e.data.type](e.data.payload);
119-
if (response) { postMessage({ "type": e.data.type, payload: response }); }
119+
if (response) {
120+
const msg = { "type": e.data.type, payload: response };
121+
if (e.data.requestId) { msg.requestId = e.data.requestId; }
122+
postMessage(msg);
123+
}
120124
};
121125

122126
// Signal that the worker is ready

js/MainPage/CascadeAPI.js

Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
// CascadeAPI.js - Programmatic API for agent/CLI integration
2+
3+
import { STLExporter } from '../../node_modules/three/examples/jsm/exporters/STLExporter.js';
4+
import { OBJExporter } from '../../node_modules/three/examples/jsm/exporters/OBJExporter.js';
5+
6+
/** Exposes window.CascadeAPI for programmatic control of Cascade Studio.
7+
* Designed for use by AI agents (via Playwright) and developer tooling. */
8+
class CascadeAPI {
9+
constructor(app) {
10+
this._app = app;
11+
}
12+
13+
/** Install the API on window and add discovery meta tag. */
14+
install() {
15+
window.CascadeAPI = this;
16+
// Add meta tag for agent discovery
17+
const meta = document.createElement('meta');
18+
meta.name = 'cascade-api';
19+
meta.content = 'window.CascadeAPI';
20+
document.head.appendChild(meta);
21+
}
22+
23+
// --- Code Management ---
24+
25+
/** Set the editor code. */
26+
setCode(code) {
27+
this._app.editor.setCode(code);
28+
}
29+
30+
/** Get the current editor code. */
31+
getCode() {
32+
return this._app.editor.getCode();
33+
}
34+
35+
/** Evaluate the current code and return a Promise that resolves when generation is complete. */
36+
evaluate() {
37+
return new Promise((resolve) => {
38+
// Listen for generation completion
39+
const originalHandler = this._app.messageBus.handlers["combineAndRenderShapes"];
40+
this._app.messageBus.on("combineAndRenderShapes", (payload) => {
41+
// Restore original handler and call it
42+
this._app.messageBus.on("combineAndRenderShapes", originalHandler);
43+
if (originalHandler) originalHandler(payload);
44+
resolve();
45+
});
46+
this._app.editor.evaluateCode(false);
47+
});
48+
}
49+
50+
// --- Results ---
51+
52+
/** Get console logs since last evaluation. */
53+
getConsoleLog() {
54+
return this._app.console.getLogs();
55+
}
56+
57+
/** Get errors since last evaluation. */
58+
getErrors() {
59+
return this._app.console.getErrors();
60+
}
61+
62+
/** Get the current model as STEP format string. Returns a Promise. */
63+
getSTEP() {
64+
return this._app.messageBus.request("saveShapeSTEP");
65+
}
66+
67+
/** Get the current model as STL format string. Synchronous. */
68+
getSTL() {
69+
const viewport = this._app.viewport;
70+
if (!viewport || !viewport.mainObject) return '';
71+
const exporter = new STLExporter();
72+
return exporter.parse(viewport.mainObject);
73+
}
74+
75+
/** Get the current model as OBJ format string. Synchronous. */
76+
getOBJ() {
77+
const viewport = this._app.viewport;
78+
if (!viewport || !viewport.mainObject) return '';
79+
const exporter = new OBJExporter();
80+
return exporter.parse(viewport.mainObject);
81+
}
82+
83+
/** Take a screenshot of the viewport as a base64 PNG data URL. */
84+
screenshot() {
85+
const viewport = this._app.viewport;
86+
if (!viewport) return '';
87+
// Force render
88+
viewport.environment.renderer.render(viewport.environment.scene, viewport.environment.camera);
89+
return viewport.environment.curCanvas.toDataURL('image/png');
90+
}
91+
92+
// --- State ---
93+
94+
/** Returns true if the worker has finished initialization. */
95+
isReady() {
96+
return this._app.startup !== null;
97+
}
98+
99+
/** Returns true if the worker is currently evaluating code. */
100+
isWorking() {
101+
return window.workerWorking;
102+
}
103+
104+
/** Set the editor mode: 'cascadestudio' or 'openscad'. */
105+
setMode(mode) {
106+
this._app.editor.setMode(mode);
107+
const modeSelect = document.getElementById('editorMode');
108+
if (modeSelect) modeSelect.value = mode;
109+
}
110+
111+
/** Get the current editor mode. */
112+
getMode() {
113+
return this._app.editor.mode;
114+
}
115+
116+
// --- Self-Documentation ---
117+
118+
/** Get structured capabilities description for agent consumption. */
119+
getCapabilities() {
120+
return {
121+
version: '1.0',
122+
modes: ['cascadestudio', 'openscad'],
123+
currentMode: this.getMode(),
124+
isReady: this.isReady(),
125+
isWorking: this.isWorking(),
126+
api: {
127+
code: {
128+
setCode: { params: ['code: string'], description: 'Set editor code' },
129+
getCode: { params: [], returns: 'string', description: 'Get editor code' },
130+
evaluate: { params: [], returns: 'Promise<void>', description: 'Evaluate code, resolves when rendering completes' },
131+
},
132+
results: {
133+
getConsoleLog: { params: [], returns: 'string[]', description: 'Get console logs since last eval' },
134+
getErrors: { params: [], returns: 'string[]', description: 'Get errors since last eval' },
135+
getSTEP: { params: [], returns: 'Promise<string>', description: 'Export model as STEP' },
136+
getSTL: { params: [], returns: 'string', description: 'Export model as STL' },
137+
getOBJ: { params: [], returns: 'string', description: 'Export model as OBJ' },
138+
screenshot: { params: [], returns: 'string', description: 'Viewport screenshot as base64 PNG data URL' },
139+
},
140+
state: {
141+
isReady: { params: [], returns: 'boolean', description: 'Worker initialized' },
142+
isWorking: { params: [], returns: 'boolean', description: 'Worker evaluating' },
143+
setMode: { params: ['mode: string'], description: 'Set editor mode (cascadestudio/openscad)' },
144+
getMode: { params: [], returns: 'string', description: 'Get current mode' },
145+
},
146+
},
147+
cadFunctions: {
148+
primitives: {
149+
'Box(x, y, z, centered?)': 'Create a box',
150+
'Sphere(radius)': 'Create a sphere',
151+
'Cylinder(radius, height, centered?)': 'Create a cylinder',
152+
'Cone(r1, r2, height)': 'Create a cone',
153+
'Polygon(points, wire?)': 'Create a polygon face or wire',
154+
'Circle(radius, wire?)': 'Create a circle face or wire',
155+
'BSpline(points, closed?)': 'Create a BSpline curve',
156+
'Text3D(text, size, height, fontName?)': 'Create extruded 3D text',
157+
'Sketch(startingPoint)': 'Start a 2D sketch chain (.LineTo().ArcTo().End().Face())',
158+
},
159+
transforms: {
160+
'Translate(offset, shape, keepOriginal?)': 'Translate a shape',
161+
'Rotate(axis, degrees, shape, keepOriginal?)': 'Rotate a shape around axis',
162+
'Scale(factor, shape, keepOriginal?)': 'Scale a shape',
163+
'Mirror(vector, shape, keepOriginal?)': 'Mirror a shape across plane',
164+
'Transform(translation, rotation, scale, shape)': 'Full transform (used by gizmos)',
165+
},
166+
booleans: {
167+
'Union(shapes, keepObjects?, fuzz?, keepEdges?)': 'Boolean union of shapes',
168+
'Difference(mainBody, tools, keepObjects?, fuzz?, keepEdges?)': 'Boolean subtraction',
169+
'Intersection(shapes, keepObjects?, fuzz?, keepEdges?)': 'Boolean intersection',
170+
},
171+
operations: {
172+
'Extrude(face, direction, keepFace?)': 'Extrude a face along a direction vector',
173+
'Revolve(shape, degrees, direction?, keepShape?)': 'Revolve a shape around an axis',
174+
'RotatedExtrude(wire, height, rotation, keepWire?)': 'Helical extrusion',
175+
'Loft(wires, keepWires?)': 'Loft through wire profiles',
176+
'Pipe(shape, wirePath, keepInputs?)': 'Sweep a shape along a path',
177+
'Offset(shape, distance, tolerance?, keepShape?)': 'Offset/shell a shape',
178+
'FilletEdges(shape, radius, edgeList, keepOriginal?)': 'Fillet edges',
179+
'ChamferEdges(shape, distance, edgeList, keepOriginal?)': 'Chamfer edges',
180+
'RemoveInternalEdges(shape, keepShape?)': 'Remove internal edges',
181+
},
182+
iteration: {
183+
'ForEachSolid(shape, callback)': 'Iterate solids in compound',
184+
'ForEachFace(shape, callback)': 'Iterate faces',
185+
'ForEachEdge(shape, callback)': 'Iterate edges',
186+
'ForEachWire(shape, callback)': 'Iterate wires',
187+
'ForEachVertex(shape, callback)': 'Iterate vertices',
188+
},
189+
gui: {
190+
'Slider(name, default, min, max, realTime?, step?, precision?)': 'Add a slider control',
191+
'Checkbox(name, default?)': 'Add a checkbox control',
192+
'TextInput(name, default?, realTime?)': 'Add a text input control',
193+
'Dropdown(name, default?, options?, realTime?)': 'Add a dropdown control',
194+
'Button(name)': 'Add a button',
195+
},
196+
utility: {
197+
'Remove(array, item)': 'Remove item from sceneShapes array',
198+
'GetWire(shape, index, keepOriginal?)': 'Get a wire by index from a shape',
199+
'GetSolidFromCompound(shape, index, keepOriginal?)': 'Get solid by index',
200+
'GetNumSolidsInCompound(shape)': 'Count solids in compound',
201+
'SaveFile(filename, fileURL)': 'Trigger file download',
202+
},
203+
},
204+
};
205+
}
206+
207+
/** Get example code for both modes. */
208+
getExamples() {
209+
return {
210+
cascadestudio: {
211+
description: 'CascadeStudio JavaScript mode — call CAD functions directly',
212+
basic: `// Primitives
213+
let box = Box(10, 20, 30);
214+
let sphere = Sphere(50);
215+
let cyl = Cylinder(10, 40, true);
216+
let cone = Cone(20, 5, 30);`,
217+
transforms: `// Transforms
218+
let box = Box(10, 10, 10);
219+
Translate([20, 0, 0], box);
220+
Rotate([0, 0, 1], 45, Box(10, 10, 10));
221+
Mirror([1, 0, 0], Box(10, 10, 10));
222+
Scale(2, Sphere(10));`,
223+
booleans: `// Booleans
224+
let sphere = Sphere(50);
225+
let cylX = Rotate([1, 0, 0], 90, Cylinder(30, 200, true));
226+
let cylY = Rotate([0, 1, 0], 90, Cylinder(30, 200, true));
227+
let cylZ = Cylinder(30, 200, true);
228+
Difference(sphere, [cylX, cylY, cylZ]);`,
229+
extrusions: `// Extrusions
230+
let face = Polygon([[0,0],[100,0],[100,50],[50,100],[0,100]]);
231+
Extrude(face, [0, 0, 20]);
232+
233+
let circle = Circle(30);
234+
Revolve(circle, 360, [0, 1, 0]);`,
235+
sketch: `// Sketch API
236+
let sketch = new Sketch([0, 0])
237+
.LineTo([100, 0])
238+
.Fillet(20)
239+
.LineTo([100, 100])
240+
.End(true)
241+
.Face();
242+
Extrude(sketch, [0, 0, 20]);`,
243+
gui: `// GUI Controls
244+
let radius = Slider("Radius", 20, 5, 50);
245+
let height = Slider("Height", 40, 10, 100);
246+
let centered = Checkbox("Centered", true);
247+
Cylinder(radius, height, centered);`,
248+
},
249+
openscad: {
250+
description: 'OpenSCAD mode — use standard OpenSCAD syntax',
251+
basic: `// Primitives
252+
cube(10);
253+
sphere(r=20);
254+
cylinder(h=30, r=10);`,
255+
transforms: `// Transforms
256+
translate([20, 0, 0]) cube(10);
257+
rotate([0, 0, 45]) cube(10);
258+
mirror([1, 0, 0]) sphere(15);`,
259+
booleans: `// Booleans
260+
difference() {
261+
cube(20, center=true);
262+
sphere(13);
263+
}`,
264+
modules: `// Custom modules
265+
module rounded_box(size, r) {
266+
minkowski() {
267+
cube(size - 2*r, center=true);
268+
sphere(r);
269+
}
270+
}
271+
rounded_box(30, 5);`,
272+
loops: `// For loops
273+
for (i = [0:4]) {
274+
translate([i * 15, 0, 0])
275+
sphere(5);
276+
}`,
277+
},
278+
};
279+
}
280+
}
281+
282+
export { CascadeAPI };

0 commit comments

Comments
 (0)