holder: explicit Render button + progress UI + longer render timeout
- Replace debounced auto-render-on-param-change with an explicit Render button. Param changes mark the button "dirty" (accent ring); user clicks Render to drive a render. A Cancel button (AbortController) appears while a render is in flight. - Add indeterminate progress bar with elapsed-time counter in the status panel. Real OpenSCAD --progress streaming can come later. - Bump OPENSCAD_TIMEOUT default 60s -> 300s and gunicorn --timeout 120s -> 300s. The 60s cap was misclassified by the frontend as "OpenSCAD not installed" because the error string contained the word "openscad" -- which the JS matched too greedily. - Frontend error classifier now distinguishes "binary not found", "timed out", and "geometry empty" cases and only shows the install-OpenSCAD hint for the real not-found case.
This commit is contained in:
+60
-18
@@ -1,8 +1,8 @@
|
||||
/* holder-app.js — controller for the Hex Holder Designer page.
|
||||
*
|
||||
* - Loads /api/holder/params for the form schema and builds inputs.
|
||||
* - On any param change, debounces 400 ms then POSTs /api/holder/render and
|
||||
* pushes the returned STL ArrayBuffer into the Three.js viewer.
|
||||
* - Changing params marks the Render button "dirty"; user must click Render
|
||||
* (or Cancel) to drive POST /api/holder/render → Three.js viewer.
|
||||
* - "Download STL" re-uses the last rendered blob (no double trip).
|
||||
* - "Design busbars →" computes cells via /api/holder/cells, creates a new
|
||||
* busbar-designer project via /api/projects, redirects to /?p=<id>.
|
||||
@@ -14,8 +14,10 @@ const state = {
|
||||
params: {}, // current values, name → value
|
||||
schema: [], // [{name, label, kind, ...}]
|
||||
lastBlob: null, // last rendered STL blob (for download)
|
||||
renderTimer: null,
|
||||
rendering: false,
|
||||
dirty: false, // params changed since last successful render
|
||||
abortCtrl: null, // for cancelling in-flight render
|
||||
elapsedTimer: null,
|
||||
rendererReady: false,
|
||||
};
|
||||
|
||||
@@ -129,31 +131,56 @@ function _onParamChange(e) {
|
||||
else v = el.value;
|
||||
state.params[k] = v;
|
||||
_updateStatus();
|
||||
_scheduleRender();
|
||||
_markDirty();
|
||||
}
|
||||
|
||||
// ----- render orchestration -------------------------------------------------
|
||||
|
||||
function _scheduleRender() {
|
||||
clearTimeout(state.renderTimer);
|
||||
state.renderTimer = setTimeout(_doRender, 400);
|
||||
function _markDirty() {
|
||||
state.dirty = true;
|
||||
$("btn-render").classList.add("dirty");
|
||||
}
|
||||
|
||||
function _clearDirty() {
|
||||
state.dirty = false;
|
||||
$("btn-render").classList.remove("dirty");
|
||||
}
|
||||
|
||||
function _showProgress(show) {
|
||||
$("render-progress").hidden = !show;
|
||||
$("btn-render").hidden = show;
|
||||
$("btn-cancel").hidden = !show;
|
||||
}
|
||||
|
||||
function _classifyError(msg) {
|
||||
// Backend RuntimeError messages all carry "openscad" in them. Distinguish:
|
||||
if (/not found on PATH/i.test(msg)) return { hint: "OpenSCAD binary not found on the server (apt install openscad)." };
|
||||
if (/timed out/i.test(msg)) return { hint: "Render timed out. Try fewer cells, smaller slot widths, or simpler geometry." };
|
||||
if (/produced no STL|geometry empty/i.test(msg)) return { hint: "OpenSCAD produced an empty model — check parameter combinations (e.g. cell_top_overlap too large)." };
|
||||
return { hint: "" };
|
||||
}
|
||||
|
||||
async function _doRender() {
|
||||
if (state.rendering) {
|
||||
// Re-schedule once current finishes.
|
||||
state.renderTimer = setTimeout(_doRender, 200);
|
||||
return;
|
||||
}
|
||||
if (state.rendering) return;
|
||||
state.rendering = true;
|
||||
state.abortCtrl = new AbortController();
|
||||
$("render-status").textContent = "rendering…";
|
||||
$("warning").textContent = "";
|
||||
$("render-time").textContent = "";
|
||||
_showProgress(true);
|
||||
|
||||
const t0 = performance.now();
|
||||
state.elapsedTimer = setInterval(() => {
|
||||
const dt = ((performance.now() - t0) / 1000).toFixed(1);
|
||||
$("render-elapsed").textContent = `${dt}s`;
|
||||
}, 100);
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/holder/render", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ params: state.params }),
|
||||
signal: state.abortCtrl.signal,
|
||||
});
|
||||
if (!res.ok) {
|
||||
let msg = res.statusText;
|
||||
@@ -165,15 +192,20 @@ async function _doRender() {
|
||||
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`;
|
||||
$("render-time").textContent = "";
|
||||
_clearDirty();
|
||||
} catch (e) {
|
||||
$("render-status").textContent = `✗ ${e.message}`;
|
||||
if (/openscad/i.test(e.message)) {
|
||||
$("warning").textContent =
|
||||
"Make sure OpenSCAD is installed on the server (apt install openscad).";
|
||||
if (e.name === "AbortError") {
|
||||
$("render-status").textContent = "✗ cancelled";
|
||||
} else {
|
||||
$("render-status").textContent = `✗ ${e.message}`;
|
||||
const { hint } = _classifyError(e.message);
|
||||
if (hint) $("warning").textContent = hint;
|
||||
}
|
||||
} finally {
|
||||
state.rendering = false;
|
||||
state.abortCtrl = null;
|
||||
clearInterval(state.elapsedTimer);
|
||||
_showProgress(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,6 +223,14 @@ function _updateStatus() {
|
||||
|
||||
// ----- buttons --------------------------------------------------------------
|
||||
|
||||
$("btn-render").addEventListener("click", () => {
|
||||
if (state.rendererReady) _doRender();
|
||||
});
|
||||
|
||||
$("btn-cancel").addEventListener("click", () => {
|
||||
if (state.abortCtrl) state.abortCtrl.abort();
|
||||
});
|
||||
|
||||
$("btn-download-stl").addEventListener("click", () => {
|
||||
if (!state.lastBlob) { alert("Nothing rendered yet."); return; }
|
||||
const url = URL.createObjectURL(state.lastBlob);
|
||||
@@ -273,7 +313,9 @@ async function init() {
|
||||
_whenViewerReady(() => {
|
||||
window.HolderViewer.init($("viewer3d"));
|
||||
state.rendererReady = true;
|
||||
_doRender(); // initial render with defaults
|
||||
// Auto-render once on load so the user sees something immediately.
|
||||
// After that, rendering is explicit (Render button).
|
||||
_doRender();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user