From ce11ca310d939785053a9e987d9c033dd5a2a6b8 Mon Sep 17 00:00:00 2001 From: PozzettiAndrea Date: Fri, 14 Aug 2026 13:47:14 +0200 Subject: [PATCH] Fix save-mesh 405 (missing route) + Multi grid dims not matching inputs Save mesh: the VTK viewer's Save button POSTed to /geometrypack/save_preview, a route that was never ported into GeometryPack (inherited caller from comfy-3d-viewers; only /gpack/getpath existed). ComfyUI's GET catch-all matched the path, hence 405 instead of 404. Added POST /gpack/save_preview in __init__.py next to getpath: basename()-sanitized lookup of the temp preview export in ComfyUI output/ (tempdir fallback), copied to output/saved_; returns {success, saved_filename}. Viewer fetch updated to the new path. Multi cols/rows: entering dims whose product was smaller than the mesh count (e.g. 1x1 for 2 meshes) overflowed viewports into IMPLICIT CSS grid rows (auto height), so the rendered layout stopped matching the inputs. Now the edited field is authoritative and the other dimension auto-raises to fit (ceil(n/edited)); both inputs are updated to show the grid actually in effect. Oversized grids (empty cells) remain allowed. --- __init__.py | 41 ++++++++++++++++++++++++++++- javascript/js/mesh_preview_multi.js | 24 +++++++++++++---- javascript/viewer_vtk.html | 2 +- 3 files changed, 60 insertions(+), 7 deletions(-) diff --git a/__init__.py b/__init__.py index b498e1d4..d6e0df3d 100644 --- a/__init__.py +++ b/__init__.py @@ -52,8 +52,47 @@ def _register_gpack_routes(): items.sort() return web.json_response(items) + # /gpack/save_preview -- the viewers' "Save mesh" button. Copies a temp + # preview export (preview_*.stl/vtp/glb, written to ComfyUI output/ or the + # OS tempdir by the preview nodes) to a stable name in output/ so it + # survives the next preview run. Body: {"temp_filename": ""}. + @inst.routes.post("/gpack/save_preview") + async def _gpack_save_preview(request): + import shutil + try: + body = await request.json() + except Exception: + return web.json_response({"success": False, "error": "invalid JSON body"}, status=400) + # basename() defuses path traversal -- we only ever serve files the + # preview nodes themselves wrote into these two directories. + name = os.path.basename(str(body.get("temp_filename") or "")) + if not name: + return web.json_response({"success": False, "error": "temp_filename required"}, status=400) + import tempfile + try: + import folder_paths + out_dir = folder_paths.get_output_directory() + except Exception: + out_dir = None + candidates = [d for d in (out_dir, tempfile.gettempdir()) if d] + src = next((os.path.join(d, name) for d in candidates + if os.path.isfile(os.path.join(d, name))), None) + if src is None: + return web.json_response( + {"success": False, "error": f"{name} not found (previews are temporary; re-run the node)"}, + status=404) + if out_dir is None: + return web.json_response({"success": False, "error": "no ComfyUI output directory"}, status=500) + saved = f"saved_{name}" + try: + shutil.copy2(src, os.path.join(out_dir, saved)) + except OSError as e: + return web.json_response({"success": False, "error": str(e)}, status=500) + log.info("[GeomPack] saved preview %s -> %s", name, saved) + return web.json_response({"success": True, "saved_filename": saved}) + inst._gpack_getpath_registered = True - log.info("[GeomPack] /gpack/getpath route registered") + log.info("[GeomPack] /gpack/getpath + /gpack/save_preview routes registered") try: diff --git a/javascript/js/mesh_preview_multi.js b/javascript/js/mesh_preview_multi.js index a3a3eb96..2ae24a46 100644 --- a/javascript/js/mesh_preview_multi.js +++ b/javascript/js/mesh_preview_multi.js @@ -181,13 +181,27 @@ app.registerExtension({ iframe.src = `/extensions/${EXTENSION_FOLDER}/${viewerUrl()}?v=` + Date.now(); }); - const onDim = () => { - viewerState.grid_cols = colsInput.value ? Math.max(1, parseInt(colsInput.value, 10)) : null; - viewerState.grid_rows = rowsInput.value ? Math.max(1, parseInt(rowsInput.value, 10)) : null; + // The edited field is authoritative; the OTHER dimension auto-raises + // so cols*rows always fits every mesh. Without this, a grid smaller + // than the mesh count overflows into implicit CSS rows (auto height) + // and the layout stops matching the inputs. Both inputs are updated + // so the UI always shows the grid actually in effect. + const onDim = (edited) => () => { + const n = lastLoad?.numMeshes || 1; + let cols = Math.max(1, parseInt(colsInput.value, 10) || 0) || (lastLoad?.autoCols ?? 1); + let rows = Math.max(1, parseInt(rowsInput.value, 10) || 0) || (lastLoad?.autoRows ?? 1); + if (cols * rows < n) { + if (edited === "cols") rows = Math.ceil(n / cols); + else cols = Math.ceil(n / rows); + } + colsInput.value = cols; + rowsInput.value = rows; + viewerState.grid_cols = cols; + viewerState.grid_rows = rows; buildAndSend(); // same iframe -> viewer re-tiles, no reload }; - colsInput.addEventListener('change', onDim); - rowsInput.addEventListener('change', onDim); + colsInput.addEventListener('change', onDim("cols")); + rowsInput.addEventListener('change', onDim("rows")); syncLayoutUI(); applyCollapsed(); diff --git a/javascript/viewer_vtk.html b/javascript/viewer_vtk.html index 99f124e6..07826049 100644 --- a/javascript/viewer_vtk.html +++ b/javascript/viewer_vtk.html @@ -1031,7 +1031,7 @@ saveButton.disabled = true; saveButton.textContent = 'Saving...'; - const response = await fetch('/geometrypack/save_preview', { + const response = await fetch('/gpack/save_preview', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ temp_filename: currentFilename })