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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
47c98b796e
commit
587c1927ce
@@ -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']
|
||||
|
||||
@@ -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"}
|
||||
@@ -24,6 +24,7 @@ viewers = [
|
||||
"bvh", "fbx_animation", "compare_smpl_bvh",
|
||||
"text_report",
|
||||
"ultimate_inspection",
|
||||
"slicer",
|
||||
]
|
||||
for viewer in viewers:
|
||||
try:
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user