Exposes the face-neighborhood (k-ring) size used for both guidance and
filtering, previously hardcoded to the vertex 1-ring. Higher rings widen the
filter footprint so each pass hits harder / reaches farther -- a genuinely
stronger lever than just adding iterations. _build_vertex_based_face_neighbors
grows the patch by ring expansion (rings=1 reproduces the old 1-ring exactly, so
existing callers are unchanged). Wired through CPU + GPU paths and the dispatcher
(default 1, max 4; cost/memory grow ~rings^2).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- sigma_r is now entered in DEGREES (angle between normals at which they stop
blending), converted internally to the unit-normal chord 2*sin(theta/2).
Default 20 deg == the previous 0.35 default, so behavior is unchanged.
- Clarified sigma_s / sigma_r tooltips (spatial vs range scale).
- Added a node-level description to Sharpen Mesh explaining the pipeline and the
guided-normal method (renders as ComfyUI's node help on hover).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a GPU path to the guided_normal backend behind a use_gpu toggle. The new
guided_normal_gpu module is a faithful torch port of Zhang et al. 2015: same
min-range-metric guidance (incl. the O(patch^2) max-pairwise-diff, chunked) and
the same bilateral normal filter + interleaved vertex update. Mesh topology
(1-ring face patches + their inner adjacency edges) is built once on CPU as
padded index tensors; every iteration then runs as batched scatter/gather on
CUDA (falls back to vectorized CPU torch).
Verified vs the CPU reference: vertex agreement ~1e-6 (float32 vs float64),
~17x faster on 82k faces.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The cell size (% of bbox diagonal) was floored at 0.1% with step 0.1, far too
coarse for fine clustering. Lower min to 1e-6 and step to 1e-6 (display_mode=number)
so small thresholds are actually enterable; clarify the tooltip. Backend passes the
value straight through (no re-clamp). Verified: 1e-6 is a near no-op, 5 reduces hard.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Surfaces MeshLab's reprojectflag (Botsch back-projection onto the original surface
after each iteration) as a `reproject` combo, default true -- matching the PMP
backends' use_projection. true keeps output verts on the input surface; false is
pure tangential smoothing that lets them drift. Verified the toggle changes
surface fidelity ~14x on a sphere.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- remesh_band (GPU cumesh): rewrite tooltip with concrete guidance -- it's the voxel
narrow-band shell width (robustness/gap-fill knob), not the detail knob; 1.0 default,
0.5 hugs surface (sharper, risk holes), 2-3 fills gaps / fuses thin walls / smoother.
- pymeshlab_isotropic: target_edge_length floor was effectively stuck at 0.01 (min
0.001 but step 0.01); lower to min 0.0001 / step 0.0001 so fine edge lengths work.
- pymeshlab_isotropic: add target_vertices / target_faces (0 = off) that back-solve the
edge length from mesh area (faces > vertices > edge_length). Verified within ~5% of
the requested count on a sphere.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Writes a per-face field 'bad_winding' = number of shared manifold edges whose
neighbour disagrees on winding direction (0 = consistent). Marks every face on a
winding seam so they can be visualized / thresholded, mirroring the Open Edges and
Degenerate Faces nodes (face_attributes + metadata + UI summary). Batch-capable.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- igl_signed_dist: expose sd_sign_type (fast_winding_number default / winding_number /
pseudonormal). Fix a pre-existing inverted sign bug -- this igl build returns
distance<0 inside, but the flip rule was S>0, so it flipped every face on a
correctly-oriented mesh. Now flips on S<0. fast_winding_number is the only oracle
that both no-ops a correct mesh and re-flips a wholly-inverted one (verified).
- igl_raycast: replace the per-face Python ray loop + unscaled 1e-6 epsilon with a
single batched igl.embree.reorient_facets_raycast (Takayama 2014), oriented per
connected component. Expose rays_minimum / use_parity / facet_wise.
- New cumesh_raystab backend: GPU cuBVH.signed_distance(mode='raystab') as a
winding-independent sign oracle (never 'watertight', which reads sign from the
input winding). CUDA-only, >8 tris. Verified no-op on correct, full-fix on inverted.
- Dispatcher: register cumesh_raystab and mirror the new params into the unified node.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Folds the vectorized torch L0 implementation into the l0_minimize backend
behind a use_gpu toggle instead of a separate backend node. When enabled it
runs CuMesh-style index_add_ scatter ops on CUDA (falls back to vectorized CPU
torch) -- orders of magnitude faster on large meshes. Surfaced in the Sharpen
dispatcher; the standalone GPU node is no longer registered (its function is
reused by l0_minimize).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
alpha is scale-invariant (angle threshold). Document the alpha->degrees
mapping in the tooltip, note beta is a no-op at iterations=1, and raise the
unified-node alpha max from 0.1 (~18deg) to 4.0 (180deg).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New "cumesh" backend runs CuMesh.unify_face_orientations() alone -- the GPU
winding/orientation fix -- to make face windings (and thus normals) consistent.
CUDA-only; fast on large meshes. Slots in alongside the trimesh/igl backends.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
Add pmp_target_vertices (0 = off) on the backend + Remesh node pmp_uniform
option. When set, back-solves the target edge length from the input mesh
area for a triangle mesh: V ~= 2A/(sqrt(3) e^2) -> e = sqrt(2A/(sqrt(3) V)).
Overrides pmp_edge_length. Approximate.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add optional cleanup passes (run on the simplified mesh, before read):
remove_degenerate_faces, remove_duplicate_faces, repair_non_manifold_edges,
remove_non_manifold_faces, remove_small_components_min_area (0=off),
remove_unreferenced_vertices. Each guarded; applied per-face removals ->
small components -> orphan verts last. Exposed on backend + Remesh node
gpu_cumesh option (same names, no remap).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The per-process-EGL GPU pool fell back to vtkXOpenGLRenderWindow with no
DISPLAY ("bad X server connection") and ran ~12s/mesh -- worse than
serial. Gate the process pool back to the software path only; GPU renders
per-mesh on the serial path (fresh plotter -> shading). Software CPU pool
and the shading fix are kept.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
grid_resolution (DC voxel grid, was hardcoded 512) is the main detail/
speed knob; project_back re-projects DC verts onto the input surface for
fidelity (was hardcoded 0). Exposed on the backend node and the Remesh
node gpu_cumesh option (gpu_grid_resolution/gpu_project_back -> mapped).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
QuadWild only exposes scale_factor (no count/edge input). Add
qw_target_vertices and qw_target_edge_length (both 0 = off) on the
QuadWild backend + the Remesh node UI. Back-solve scale_factor from the
input mesh area: vertices -> edge via e=sqrt(area/N), edge -> scale via
scale = e / mean_input_edge. Priority edge_length > vertices > scale_factor.
Approximate (QuadWild is edge-length-driven, not count-exact).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Revert plotter reuse (p.clear() dropped lights -> flat renders; reuse
barely helped anyway). Fresh plotter per mesh restores shading.
- Parallelise BOTH gpu and software paths across max CPU cores via a
process pool: each worker renders in its own GL context (gpu children
unset DISPLAY -> per-process EGL; software children share parent Xvfb)
and returns the small image, not the mesh. This is the Lightning-style
(processes, not threads) parallelism -- parallelises the CPU-bound prep
while the shared GPU serialises the cheap rasterisation. Serial fallback.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
pv.PolyData(points) adds one VERT cell per point, so n_cells became
verts+lines+polys and cell_data (lines+polys only) mismatched. Build the
PolyData empty then assign points/faces/lines -> no verts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
edge_value = reduce(face_field[A], face_field[B]); face_normals+angle =
dihedral. Thresholded edges + surface written to an ASCII .vtp and shown
in the shared VTK.js viewer (viewer_vtk.html) in-node. Face field is a
string (face_normals or any face_attributes key; available ones logged).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Recreating the EGL render window per mesh dominated per-mesh time. Keep a
single plotter alive and clear()+redraw between meshes (recreate only
after a failure). Refactored render into _make_plotter/_prep_poly/
_draw_into/_screenshot_arr; titles use named add_text so they replace on
reuse. No decimation. Workers (software path) still one-shot.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Y/Z swap is a reflection: deep-copy the wrapped mesh (never mutate the
upstream trimesh), flip_normals to restore winding/lighting, and call
reset_camera so the view re-frames the transformed bounds.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Lazy-imported comfy.utils.ProgressBar, advanced per mesh in both the
process-pool and serial paths.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Fix: len(m.faces or []) raised "ambiguous truth value" on the numpy
faces array, crashing the whole load. Use a safe _n_faces() helper.
- Threads barely helped (trimesh OBJ parsing is GIL-bound Python); switch
to a ProcessPoolExecutor for true CPU-parallel parsing, with a serial
fallback on error.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When use_gpu is off and >1 mesh, render across CPU cores via a process
pool (software rasterisation is embarrassingly parallel). Parent starts
Xvfb without a GL context (fork-safe); workers reload from file_path when
available to avoid pickling big meshes. Shared _render_mesh_array used by
serial + worker paths. GPU path unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
use_multithreading toggle loads files via a ThreadPoolExecutor (workers =
cpu_count); mesh I/O + numpy OBJ parsing release the GIL so big batches
load much faster. Order preserved; live per-mesh logging.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- revert_yz_flip: swap Y/Z axes
- three_plane: 2x2 grid (iso(random) + XY/YZ/XZ)
- mesh_opacity + show_edges with conditional edge_opacity (hidden until edges on, via web_src JS)
- show_connected_components: color by pyvista connectivity() RegionId
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Upgrade the batch renderer (was Render Mesh Batch): optional GPU/native
EGL offscreen with software fallback, tqdm + ComfyUI progress bars, and
the filename drawn via plotter.add_title. Renamed to Preview Mesh Batch
Render (node_id GeomPackPreviewMeshBatchRender).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Native <datalist> never opens inside ComfyUI transformed DOM widgets;
replace with a body-attached floating dropdown (fixed-positioned under the
input), click-to-descend into folders, fed by /gpack/getpath.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Inspired by VHS Load Video (Path), but GeometryPack-owned: register
/gpack/getpath in __init__ (main process, after PromptServer exists) to
list directory contents, and a web_src widget that replaces folder_path
with an <input>+<datalist> showing live FOLDER suggestions.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Three.js viewer.html OBJLoader chokes on some OBJs; viewer_vtk.html
uses VTK readers (same LOAD_MESH protocol) and shows the download bar.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Just-uploaded meshes are added to the file_path combo client-side but are
not in the backend cached INPUT_TYPES options, so queueing hit a 400
"Value not in list". validate_inputs(**kwargs)->True skips that combo
check; execute() still resolves the path and raises if truly missing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Switch upload from fetch to XHR for upload progress, show a bar on the
node during upload. Top-level log to confirm the extension loads.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Load Mesh: drop/upload meshes (reuses /upload/image, subfolder=3d, like
the official Load Image) + a "view 3d" toggle that mounts the shared
vtk.js viewer inline. Frontend source in tracked web_src/, copied to the
generated web/js at prestartup.
- Render Mesh Batch: offscreen pyvista render per mesh, titled by name.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Deletes mesh faces/points by a scalar field value via pyvista's threshold filter,
reusing the existing trimesh<->pyvista bridge. Keep modes above/below/between/equal,
on vertex (point) or face (cell) data (auto-prefers face data), with all_scalars for
point fields. E.g. field='viewed', keep='above', value=0.5 -> keep only the faces
Pixal3D's view actually saw. Output mesh retains surviving fields + a summary.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>