From 587c1927ce4d8d209abfd588eec3e1b900fd4ba5 Mon Sep 17 00:00:00 2001 From: PozzettiAndrea Date: Tue, 16 Jun 2026 17:12:45 +0200 Subject: [PATCH] Add Preview Mesh Slicer node (interactive VTK.js clip) 3D clip view + orthographic pick view (XY/YZ/XZ toggle): click 2 points to define a cut line swept along the view axis into a clip plane; flip clip slices the other side. Python node exports the mesh .vtp; clipping is interactive in viewer_slicer.html. prestartup copies the new slicer viewer. Co-Authored-By: Claude Opus 4.8 --- nodes/visualization/__init__.py | 3 ++ nodes/visualization/preview_mesh_slicer.py | 60 +++++++++++++++++++++ prestartup_script.py | 1 + web_src/preview_mesh_slicer.js | 63 ++++++++++++++++++++++ 4 files changed, 127 insertions(+) create mode 100644 nodes/visualization/preview_mesh_slicer.py create mode 100644 web_src/preview_mesh_slicer.js diff --git a/nodes/visualization/__init__.py b/nodes/visualization/__init__.py index 0d881e8c..96b0af1c 100644 --- a/nodes/visualization/__init__.py +++ b/nodes/visualization/__init__.py @@ -16,6 +16,7 @@ from .preview_gaussian import PreviewGaussianNode, NODE_CLASS_MAPPINGS as PREVIE from .preview_mesh_analysis import PreviewMeshAnalysisNode, NODE_CLASS_MAPPINGS as PREVIEW_MESH_ANALYSIS_MAPS, NODE_DISPLAY_NAME_MAPPINGS as PREVIEW_MESH_ANALYSIS_DISP from .render_mesh_batch import NODE_CLASS_MAPPINGS as RENDER_MESH_BATCH_MAPS, NODE_DISPLAY_NAME_MAPPINGS as RENDER_MESH_BATCH_DISP from .preview_mesh_boundaries import NODE_CLASS_MAPPINGS as PREVIEW_MESH_BOUNDARIES_MAPS, NODE_DISPLAY_NAME_MAPPINGS as PREVIEW_MESH_BOUNDARIES_DISP +from .preview_mesh_slicer import NODE_CLASS_MAPPINGS as PREVIEW_MESH_SLICER_MAPS, NODE_DISPLAY_NAME_MAPPINGS as PREVIEW_MESH_SLICER_DISP NODE_CLASS_MAPPINGS = {} NODE_DISPLAY_NAME_MAPPINGS = {} @@ -42,5 +43,7 @@ NODE_CLASS_MAPPINGS.update(RENDER_MESH_BATCH_MAPS) NODE_DISPLAY_NAME_MAPPINGS.update(RENDER_MESH_BATCH_DISP) NODE_CLASS_MAPPINGS.update(PREVIEW_MESH_BOUNDARIES_MAPS) NODE_DISPLAY_NAME_MAPPINGS.update(PREVIEW_MESH_BOUNDARIES_DISP) +NODE_CLASS_MAPPINGS.update(PREVIEW_MESH_SLICER_MAPS) +NODE_DISPLAY_NAME_MAPPINGS.update(PREVIEW_MESH_SLICER_DISP) __all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS'] diff --git a/nodes/visualization/preview_mesh_slicer.py b/nodes/visualization/preview_mesh_slicer.py new file mode 100644 index 00000000..68f92a3c --- /dev/null +++ b/nodes/visualization/preview_mesh_slicer.py @@ -0,0 +1,60 @@ +# SPDX-License-Identifier: GPL-3.0-or-later +# Copyright (C) 2025 ComfyUI-GeometryPack Contributors + +""" +Preview Mesh Slicer - interactive VTK.js clip/slice preview. + +Top: 3D view of the mesh with a live clipping plane. Bottom: an orthographic +pick view (toggle XY / YZ / XZ); click two points to define a cut line, which is +swept along the view axis into a clip plane. "Flip clip" slices the other side. + +The Python node just exports the mesh to a .vtp; all the clipping is interactive +in the browser (viewer_slicer.html). +""" + +import logging +import os +import uuid + +from comfy_api.latest import io + +log = logging.getLogger("geometrypack") + + +class PreviewMeshSlicer(io.ComfyNode): + """Interactive clip/slice preview of a mesh in VTK.js.""" + + @classmethod + def define_schema(cls): + return io.Schema( + node_id="GeomPackPreviewMeshSlicer", + display_name="Preview Mesh Slicer", + category="geompack/visualization", + is_output_node=True, + inputs=[ + io.Custom("TRIMESH").Input("mesh", tooltip="Mesh to slice/clip interactively."), + ], + outputs=[], + ) + + @classmethod + def execute(cls, mesh): + import folder_paths + from ._vtp_export import export_mesh_with_scalars_vtp + + tmp = folder_paths.get_temp_directory() + os.makedirs(tmp, exist_ok=True) + filename = f"gp_slicer_{uuid.uuid4().hex[:8]}.vtp" + export_mesh_with_scalars_vtp(mesh, os.path.join(tmp, filename)) + + nv = len(getattr(mesh, "vertices", [])) + nf = len(getattr(mesh, "faces", [])) + log.info("[PreviewMeshSlicer] exported %s (%d verts, %d faces)", filename, nv, nf) + return io.NodeOutput(ui={ + "mesh_file": [filename], + "summary": [f"{nv:,} verts / {nf:,} faces — slice interactively in the viewer"], + }) + + +NODE_CLASS_MAPPINGS = {"GeomPackPreviewMeshSlicer": PreviewMeshSlicer} +NODE_DISPLAY_NAME_MAPPINGS = {"GeomPackPreviewMeshSlicer": "Preview Mesh Slicer"} diff --git a/prestartup_script.py b/prestartup_script.py index b2df16c9..f6517881 100644 --- a/prestartup_script.py +++ b/prestartup_script.py @@ -24,6 +24,7 @@ viewers = [ "bvh", "fbx_animation", "compare_smpl_bvh", "text_report", "ultimate_inspection", + "slicer", ] for viewer in viewers: try: diff --git a/web_src/preview_mesh_slicer.js b/web_src/preview_mesh_slicer.js new file mode 100644 index 00000000..0a60360f --- /dev/null +++ b/web_src/preview_mesh_slicer.js @@ -0,0 +1,63 @@ +// Preview Mesh Slicer: interactive clip/slice viewer (viewer_slicer.html) in-node. +// On execute the Python node writes the mesh .vtp to temp; we mount the slicer +// iframe and postMessage it the file URL. +import { app } from "../../../scripts/app.js"; + +const TAG = "[PreviewMeshSlicer]"; +console.log(`${TAG} script loaded`); + +const EXTENSION_FOLDER = (() => { + const m = import.meta.url.match(/\/extensions\/([^/]+)\//); + return m ? m[1] : "ComfyUI-GeometryPack"; +})(); + +function viewUrl(name) { + const norm = String(name).replace(/\\/g, "/"); + const m = norm.match(/(?:^|\/)(output|input|temp)\/(.+)$/); + if (m) { + const [, type, rel] = m; + const parts = rel.split("/"); + const fname = parts.pop(); + return `/view?filename=${encodeURIComponent(fname)}&type=${type}&subfolder=${encodeURIComponent(parts.join("/"))}`; + } + return `/view?filename=${encodeURIComponent(norm.split("/").pop())}&type=temp&subfolder=`; +} + +app.registerExtension({ + name: "geompack.previewmeshslicer", + async beforeRegisterNodeDef(nodeType, nodeData) { + if (nodeData.name !== "GeomPackPreviewMeshSlicer") return; + console.log(`${TAG} registering for GeomPackPreviewMeshSlicer`); + + const onCreated = nodeType.prototype.onNodeCreated; + nodeType.prototype.onNodeCreated = function () { + const r = onCreated?.apply(this, arguments); + const node = this; + const iframe = document.createElement("iframe"); + iframe.style.cssText = "width:100%;height:100%;border:none;background:#2a2a2a;"; + iframe.src = `/extensions/${EXTENSION_FOLDER}/viewer_slicer.html?v=` + Date.now(); + const widget = node.addDOMWidget("slicer", "MESH_PREVIEW", iframe, { + getValue() { return ""; }, setValue() { }, + }); + widget.computeSize = (w) => [w || 480, Math.round((w || 480) * 1.15)]; + node.size = [Math.max(node.size?.[0] || 0, 420), Math.max(node.size?.[1] || 0, 540)]; + node._gpSlicerIframe = iframe; + return r; + }; + + const onExecuted = nodeType.prototype.onExecuted; + nodeType.prototype.onExecuted = function (message) { + onExecuted?.apply(this, arguments); + const iframe = this._gpSlicerIframe; + const name = message?.mesh_file?.[0]; + if (!iframe || !name) return; + const url = viewUrl(name); + setTimeout(() => { + if (iframe.contentWindow) { + console.log(`${TAG} loading ${url}`); + iframe.contentWindow.postMessage({ type: "LOAD_MESH", filepath: url, timestamp: Date.now() }, "*"); + } + }, 250); + }; + }, +});