|
| 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