/* scad-app.js — controller for the /scad universal OpenSCAD playground. * * Reuses the same Three.js viewer module (holder-viewer.js) and the same * /api/scad/render backend endpoint that the holder editor uses. */ const $ = (id) => document.getElementById(id); const state = { editor: null, rendering: false, abortCtrl: null, elapsedTimer: null, lastBlob: null, rendererReady: false, }; const EXAMPLE_SOURCE = `// Tiny example to sanity-check the renderer. // Click Render. Then start editing. \$fn = 80; difference() { // Outer rounded box hull() { for (x = [-20, 20], y = [-12, 12]) translate([x, y, 0]) cylinder(h = 8, r = 4); } // Six holes for (i = [-2 : 2]) translate([i * 8, 0, -1]) cylinder(h = 10, d = 3); } `; function _whenViewerReady(cb) { if (window.HolderViewer) { cb(); return; } let tries = 0; const t = setInterval(() => { if (window.HolderViewer) { clearInterval(t); cb(); } else if (++tries > 50) { clearInterval(t); console.error("HolderViewer never loaded"); } }, 60); } function _initEditor(initialValue) { state.editor = window.CodeMirror($("scad-editor"), { value: initialValue, mode: "text/x-csrc", theme: "dracula", lineNumbers: true, indentUnit: 4, tabSize: 4, lineWrapping: false, viewportMargin: Infinity, }); } function _showProgress(show) { $("render-progress").hidden = !show; $("btn-render").hidden = show; $("btn-cancel").hidden = !show; } async function _doRender() { if (state.rendering || !state.editor) return; const source = state.editor.getValue().trim(); if (!source) { $("render-status").textContent = "✗ source is empty"; return; } state.rendering = true; state.abortCtrl = new AbortController(); $("render-status").textContent = "rendering…"; $("warning").textContent = ""; _showProgress(true); const t0 = performance.now(); state.elapsedTimer = setInterval(() => { $("render-elapsed").textContent = ((performance.now() - t0) / 1000).toFixed(1) + "s"; }, 100); try { const res = await fetch("/api/scad/render", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ source }), signal: state.abortCtrl.signal, }); if (!res.ok) { let msg = res.statusText; try { msg = (await res.json()).error || msg; } catch {} throw new Error(msg); } state.lastBlob = await res.blob(); const buf = await state.lastBlob.arrayBuffer(); window.HolderViewer.loadSTL(buf); const dt = ((performance.now() - t0) / 1000).toFixed(1); $("render-status").textContent = `✓ rendered in ${dt}s · ${(state.lastBlob.size/1024).toFixed(0)} kB`; } catch (e) { if (e.name === "AbortError") $("render-status").textContent = "✗ cancelled"; else $("render-status").textContent = `✗ ${e.message}`; } finally { state.rendering = false; state.abortCtrl = null; clearInterval(state.elapsedTimer); _showProgress(false); } } $("btn-render").addEventListener("click", _doRender); $("btn-cancel").addEventListener("click", () => state.abortCtrl?.abort()); $("btn-load-example").addEventListener("click", () => { if (state.editor) state.editor.setValue(EXAMPLE_SOURCE); }); $("btn-download-stl").addEventListener("click", () => { if (!state.lastBlob) { alert("Nothing rendered yet."); return; } const url = URL.createObjectURL(state.lastBlob); const a = document.createElement("a"); a.href = url; a.download = "rendered.stl"; document.body.appendChild(a); a.click(); setTimeout(() => { URL.revokeObjectURL(url); a.remove(); }, 100); }); // Ctrl+Enter from inside the editor triggers Render. document.addEventListener("keydown", (e) => { if ((e.ctrlKey || e.metaKey) && e.key === "Enter") { e.preventDefault(); _doRender(); } }); // Bootstrap: viewer + editor with an empty buffer (or example). _whenViewerReady(() => { window.HolderViewer.init($("viewer3d")); state.rendererReady = true; _initEditor(EXAMPLE_SOURCE); });