Takes the VSA patch_id field, robustly fits each patch to plane/sphere/cylinder/cone
(geometric residual, sigma-clip refit), and AGGLOMERATIVELY MERGES adjacent patches
whose COMBINED points fit one primitive -- collapsing VSA's bands of a cylinder/cone/
sphere into a single patch. Outputs face fields patch_id / patch_type (0 plane,1 sphere,
2 cyl,3 cone) / fit_residual + per-patch params in metadata.
Key correctness fixes over the first cut: merge gates on INLIER FRACTION (>= thresh of
combined points within merge_tol) not mean RMS -- a biased sample previously let a
degenerate fit swallow the whole mesh into one "plane". Plus degeneracy guards on the
sphere (reject r > 8*spread = really a plane/cylinder), cylinder (r outside [1e-4,30]*
spread), and cone (half-angle in [0.09,1.48], bounded apex). Face grouping via one
argsort instead of per-patch np.where (was ~1.8B ops). Validated on decimated 101_clean:
4039 raw patches -> 73 consolidated; barrel=cylinder, flange=plane, chamfers=cone.
Known: runtime scales with raw patch count (agglomerative); torus not yet supported.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
GPU Lloyd VSA -> per-face patch_id field. Assign each face to the proxy minimizing
area*||n-N||^2 + w_pos*||c-C||^2 (L2,1 normal metric + spatial term), refit area-
weighted proxy normal/centroid, iterate; then connected-component split (k-means
labels can be disconnected) + de-speckle. Intentionally over-segments curved surfaces
into normal-coherent strips (the upcoming Fit Primitives node merges them into single
cylinders/cones/spheres). Requires watertight + winding-consistent input (errors
otherwise -- the B-rep pipeline assumes a solid). Validated on 101_clean: 931k faces ->
~1900 patches in 3.8s on GPU, flange coherent, barrel in bands as expected.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Detects fillet / rounded-edge regions by the geometric signature DeFillet (Jiang et
al., SIGGRAPH 2025) is built on: a fillet is swept by a rolling ball, so every point
is DEVELOPABLE (|k_min|/|k_max| small -> cylindrical, not a sphere/blob) with a small,
locally-CONSTANT radius r=1/|k_max|. Per-vertex principal curvatures (libigl) ->
threshold developable + radius window + local-radius consistency -> connect into fillet
strips -> per-strip radius. Outputs vertex fields fillet / fillet_radius / fillet_id /
kappa_max.
max_radius_frac (fraction of bbox diagonal) is the key knob separating fillets from the
body: validated on 101_clean, ~0.015-0.02 cleanly isolates the hole-rim / edge rounds
while excluding the main barrel cylinder and the flat flange. Honest caveat (same one
DeFillet's released code leaves to semantics): a true small cylinder can read as a
fillet -- the radius cap mitigates but doesn't fully resolve it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New regularizer dropdown on the Decrease-Gaussian backend:
- reduce (old): L2, minimize sum K^2 -> lowers |K| everywhere, spreads it thin.
- developable (new, default): L1 via IRLS reweight w=1/(|K|+eps) -> pushes small K
to 0 and CONCENTRATES it onto sparse seams = piecewise ZERO-Gaussian-curvature
(planes + cylinders + cones kept smooth, only the seams curve). The CAD-friendly
mode: unlike piecewise-flat L0 it does NOT facet fillets/cylinders.
Exact angle-defect gradient + foldless barrier + Gauss-Bonnet conservation unchanged.
Validated across the real Tripo mesh set (each ~1M faces): developable removes
~22-71% of spurious total absolute Gaussian curvature sum|K| (e.g. 110: -71%,
114: -54%, 104: -50%), watertight, foldless. On a noisy developable target (cylinder)
sum|K| 719->107 (vs 498 for L2). Already-clean meshes (101, fandisk, block) change
little / need a higher anchor -- expected, they're already developable.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The CPU l0 path loops over every adjacency edge (~1.4M on a 931k-face mesh) in pure
Python each iteration, plus the pure-Python per-face vertex update -- tens of millions
of Python iterations, effectively a hang at real-mesh scale. The vectorized GPU path
does the same 10 iterations in ~1.6s. Flip the default to GPU (matching the other heavy
backends) and note the CPU limit in the tooltip.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Detects the tangent line where a flat region rolls into a fillet/round -- a SECOND
-ORDER feature the surface is tangent-continuous across (normals match, dihedral ~0),
so crease/dihedral detectors miss it. Signal is the CURVATURE step: max principal
curvature (libigl quadric fit) ~0 on the plane vs ~1/r on the fillet, thresholded
resolution-aware (kappa_max*local_edge < 1/flat_radius_edges). Two methods:
region_boundary (de-speckled flat<->curved boundary) and curvature_jump (direct
curvature-step edges). Outputs vertex fields tangent_edge / flat_region / kappa_max.
Validated on 101: median dihedral AT the detected seam is 1.6deg and only 1.6% of
seam edges are sharp (>20deg) -- i.e. 98.4% are smooth tangent lines a dihedral
detector cannot see; curvature steps 26->72 across them.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Region Growing (analysis): seed faces flattest-first and grow regions by a tunable
similarity metric -- criterion dropdown (normals / normals+area / curvature+normals /
curvature) with per-term weights and a threshold. Greedy flood-fill against the
region's running area-weighted representative; small regions merged into their
strongest neighbour. Outputs per-face region_id. On 101 (CAD): by normals it recovers
the planar structure (top region ~79k faces, ~70% of faces in large regions) in ~6s.
Decrease Gaussian (sharpen backend): gradient flow on the Gaussian-curvature energy
sum K_i^2 (K_i = vertex angle defect) + Tikhonov anchor, so |K| is driven toward zero
-> locally developable (planes/cylinders/cones). Exact analytic angle-defect gradient
(verified vs finite differences to 1e-10); every step passes through the foldless
signed-area barrier. Verified: monotone decrease of mean|K|, total K conserved
(Gauss-Bonnet, 2*pi*chi), watertight/foldless/volume-stable. Wired into Sharpen Mesh.
(Includes segment_patches.py, previously-untracked, so the analysis __init__ imports
resolve on a clean checkout.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The bare Jacobi projection sweep is only marginally stable: iterated at high
counts it overshoots at preserved creases, oscillates, collapses triangles, and
folds. Replace it with the EXACT per-vertex regularized point-to-plane solve plus
a signed-area barrier line search:
(sum_{f in i} n_f n_f^T + anchor*I) x_i = sum_f n_f (n_f . c_f) + anchor*x0_i
anchor*I + drag-back to the FIXED original positions x0 kill the divergence; the
barrier caps the step so no triangle inverts. Strong smoothing now stays stable:
on a 931k-face mesh the old sweep created ~10.5k collapsed faces and ROUGHENED
the surface (1.61->2.11 deg), while the new update creates ~0 and smooths cleanly
(1.61->1.40 deg), watertight, volume stable, in ~13s on GPU.
New `vertex_anchor` knob (lower = stronger smoothing). Applied identically to the
CPU helper and the GPU port (results agree to 5e-7). Method follows the mesh-
denoising / inversion-free geometric-optimization literature (regularized LSE
vertex update + signed-area barrier).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New wide_dihedral knob: averages each face's field over N rings of edge-adjacent
faces (area-weighted) before the per-edge reduction. With reduction=angle on
face_normals this turns the raw 2-face dihedral into the angle between BROADENED
multi-face normals, so a feature smeared over many faces (chamfer / fine fillet)
reads as one larger turn instead of many tiny per-edge angles. 0 = raw 2-face;
2-4 is a good range (higher also rounds genuinely-sharp single-edge creases).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two of the mathematicians' recommendations:
1) curvature_guided robustness: use the INTRINSIC-DELAUNAY cotmatrix
(igl.intrinsic_delaunay_cotmatrix -> non-negative weights, no spurious curvature
spikes from obtuse slivers; Fisher-Springborn-Schroeder-Bobenko 2007) and
BARYCENTRIC mass (always positive; Voronoi+1e-12-clamp manufactured fake
curvature on obtuse triangles). Both modes benefit; TV verified still correct.
2) New "Segment by Curvature" node (geompack/analysis): classify each vertex by
curvature TYPE from the principal curvatures (k1,k2) -- the (H,K)/shape-operator
classes the reviews said are needed (|H| alone can't express cylinders):
planar (both ~0), cylindrical/conical (one ~0, K~0 developable), spherical
(umbilic), saddle (K<0), generic. "Zero" is resolution-aware (radius >
flat_radius_edges x LOCAL edge length). Outputs segment_class + connected
segment_region vertex fields. Verified: box->planar+edges, cylinder->cyl+caps,
sphere->spherical.
This is the foundation for the per-region primitive snapping (plane/cylinder/
sphere fit) -- the next layer.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Synthesis of two mathematician reviews. Two decisive changes:
1) regularizer="tv" (new default): Total-Variation denoising of the
mean-curvature-normal field via Chambolle-Pock (min 1/2||u-delta||^2 +
alpha*sum_edges||u_j-u_i||). The L1-of-gradient prior yields PIECEWISE-CONSTANT
curvature with crisp jumps -> genuine regions of constant curvature
(planes/cylinders/spheres), which a bilateral low-pass cannot do (it only makes
ramps). Matrix-free (gradient/divergence scatter ops), GPU. tv_weight is the
single strength knob (relative to median curvature). regularizer="bilateral"
keeps the previous edge-aware diffusion.
2) Reconstruction now uses CGLS on the stacked [L; sqrt(lam) I] instead of forming
L^T L + Jacobi-CG. Forming L^T L squares the condition number (~1/h^4) and
silently under-converges in float32; CGLS works on the unsquared operator
(effective sqrt(kappa) ~ 1/h^2), reusing the same GPU sparse mat-vec.
Also fixed the stale module docstring (described the removed magnitude-only path).
Verified: TV denoises a noisy sphere to near-constant |H| (std 2.73->0.20 at
tv_weight=1.5 vs bilateral 0.37), recovers true 1/R, radius preserved.
Follow-ups from the reviews (not yet done): intrinsic-Delaunay cotmatrix +
barycentric mass for sliver robustness; (H,K)/shape-operator targets so cylinders
(K=0) are expressible; per-region CMC / primitive snapping.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The backend filtered the curvature MAGNITUDE |H|, which rectifies noise: on a flat
region true H=0 but noise gives delta random directions with positive length, so
averaging |delta| keeps a positive floor and the reconstruction INJECTS curvature
everywhere ("increasing everywhere"). Now it bilaterally filters the SIGNED
mean-curvature-normal VECTOR delta, so opposite-pointing noise cancels: a noisy
flat (or spurious bump) averages toward 0 -> flattens, and a noisy curved region
averages toward its consistent curvature -> fills. Curvature-range weight still
keeps real curvature-regions (flat<->fillet) from bleeding together. Removed the
now-moot direction_blend param.
Verified: noisy sphere |H| mean 3.94->1.02 (~1/R), std 2.8->0.18, radius preserved;
flat+bump interior |H| mean 5.24->0.27 (flattened). Open-mesh boundaries remain a
weak spot (boundary delta ill-defined) -- fine for watertight input.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
smoothing_iterations used explicit cotangent diffusion `f += 0.5 * Minv @ L @ f`,
which is unstable: the Laplace-Beltrami spectral radius scales like 1/edge^2, so a
fixed step diverges on fine meshes (and obtuse triangles add negative cotangent
weights) -> fields exploded to ~1e12. Replaced with row-normalized umbrella
neighbour averaging (a convex combination), unconditionally stable at any
resolution. Verified: on a fine noisy sphere, smoothing now monotonically reduces
the field variance with finite values instead of blowing up.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Curvature finer than the LOCAL triangles is the quadric fit blowing up at a sharp
edge / sliver, not real geometry (sampling theory: Vasa et al. CGF 2016 need
kappa*edge small; Amenta-Bern epsilon-sampling / local feature size: radius 1/kappa
must exceed local sample spacing). New clamp_to_resolution (ON by default) caps
|kappa| per vertex so the radius of curvature can't be finer than min_radius_edges
x the LOCAL edge length, applied to the principal curvatures before deriving every
field -- so the 0.37-vs-150 artifact spikes are removed automatically. clamp_percentile
remains as a secondary statistical clip. Verified: a box's sharp-edge spike caps at
1/local_edge.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The range weight was on NORMAL difference, so it only stopped diffusion at
normal jumps -- it blurred curvature across tangent flat<->fillet boundaries
instead of preserving them. Replaced it with a CURVATURE range weight (the true
second-order analog of guided_normal): a guidance |H| (2 spatial-smoothing
passes) defines regions, and curvature diffuses WITHIN a curvature-region and
stops at curvature steps -> pushes toward piecewise-constant curvature with crisp
region boundaries. Param sigma_r_degrees -> curvature_sigma (relative to the |H|
spread). Verified: noisy sphere |H|-std 2.33->0.91 (curvature uniformized),
clean sphere still a fixed point, radius preserved.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Compute Curvature now also attaches per-FACE versions of every curvature field
(mean over each face's 3 verts) to mesh.face_attributes, so face-field consumers
like Preview Mesh Boundaries can use them. Added a curvature_angle_deg field
(both vertex + face) = degrees(|kappa_max| * mean_edge_length) -- curvature
expressed as a DIHEDRAL-EQUIVALENT angle, so you can threshold it in the same
degree units as a crease (and, unlike a raw dihedral, it's defined inside a
smooth fillet, not just at sharp edges). Added 'angle_deg' to primary_output.
Preview Mesh Boundaries: added max / mean / min reductions so you can threshold
edges by the LEVEL of a face field (e.g. curvature_angle_deg) instead of only its
jump (abs_diff) or normal angle. Verified end to end: Compute Curvature ->
Preview Mesh Boundaries(face_field=curvature_angle_deg, reduction=max) highlights
the curved/fillet edges.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
curvature_abs_max was the principal curvature with the largest MAGNITUDE but
kept its SIGN -- so concave regions read negative, contradicting the name and
breaking flat-vs-curved thresholding. It's now max(|k1|,|k2|) >= 0 (the field to
threshold: ~0 flat, ~1/r on a fillet, independent of concave/convex). Added a
new curvature_dominant field that keeps the sign (negative=concave,
positive=convex) + a dominant_signed primary-output option, so the concave/convex
information isn't lost. Verified abs_max >= 0 on a torus.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New "Mesh Repair" dispatcher node (geompack/repair), same GraphBuilder backend
pattern as Fill Holes, designed to grow more backends (pymeshfix, ...).
First backend = pymeshlab cleanup: per-op toggles for remove duplicate vertices,
null/zero-area (degenerate) faces, folded faces, duplicate faces, T-vertices,
non-manifold-edge repair, small floating components (by % bbox-diagonal), and
unreferenced vertices -- applied in a safe order, each guarded. This is the real
sliver/degenerate remover: CGAL's SWIG binding does NOT expose
remove_almost_degenerate_faces (only remove_connected_components /
remove_isolated_vertices), so pymeshlab's meshing_remove_* is the right tool.
Verified: a mesh with a duplicate vertex + null face + floating component repairs
to watertight. GeomPackMeshRepair added to the text_report info-widget list.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Were only exposing target_faces/preserve_sharp/preserve_boundary. Added the
remaining pyquadriflow params: adaptive_scale (curvature-adaptive quad sizing),
minimum_cost_flow (cleaner connectivity/singularities), aggressive_sat (seamless
fully-integer, highest quality), and seed. Wired through the backend + dispatcher.
Also documented that preserve_sharp's threshold is hardcoded at 60 deg in
QuadriFlow's C++ (ComputeSharpEdges: edge sharp when adjacent face normals
deviate > 60 deg) -- not adjustable without rebuilding the binding/core.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New "faithc" Remesh backend (CUDA) wrapping faithc-aot / faithcontour (Luo et al.
CVPR 2026): builds a hierarchical octree (resolution = 2^max_level) around the
surface and extracts a feature-preserving contour mesh from per-voxel anchors +
edge flux -- a more faithful alternative to SDF + Marching Cubes / dual
contouring.
Uses the class API (atom3d MeshBVH + OctreeIndexer -> FCTEncoder -> FCTDecoder)
because the published AOT wheel's legacy function API (faithcontour.api/ops) is
broken (imports a removed `_C`; kernels now live in atom3d.kernels). Imports are
deferred to execute() so the comfy-env metadata scan isn't pulled into the CUDA
import. Normalizes to [-1,1]^3 and inverts the transform so output is in world
coords (verified bounds match).
Exposed knobs: max_level (= grid resolution, power-of-2), min_level, QEF
lambda_n/lambda_d/weight_power, clamp_anchors, and quad->tri triangulation mode.
Verified end-to-end on the regenerated py3.13/torch2.8 env (res 128 -> ~154k
faces, res 256 -> ~617k).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cross-checked against RXMesh's flip.cuh predicate and adopted:
- Valence criterion now minimizes SUM OF SQUARED deviation (valence-target)^2
(canonical Botsch-Kobbelt / RXMesh), not L1 |deviation|.
- Exclude flips touching any boundary vertex (not just boundary edges).
Also fixes a real bug the valence path exposed: the per-pass independent set was
face-disjoint, which is fine for the geometric Delaunay criterion but NOT for the
per-vertex valence criterion -- two flips sharing only a vertex both change its
valence (scored on stale data) and the result oscillated (27k flips, regularity
got worse). Switched to a VERTEX-disjoint Luby set (no two simultaneous flips
share any of the 4 vertices), which subsumes face-disjointness (topology safe)
and makes each flip's valence change independent. Verified: valence now drops
sum-sq-deviation 3624 -> 130 (28x more regular), Delaunay unchanged, watertight/
Euler preserved, ~22k flips on 327k faces in 4.7s.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A flip is now rejected if the edge's dihedral (angle between the two adjacent
face normals) exceeds feature_angle -- so creases/feature lines are never flipped
across (the standard feature-preservation rule from isotropic remeshing).
Default 30 deg; set 180 to disable. Info reports how many feature edges were
locked. Verified monotonic on a moderate-dihedral mesh (3/10/30/180 deg ->
115/369/1206/7373 flips) and that cube creases survive.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extrinsic edge-flip / Delaunay-flip node (geompack/remeshing): swaps the shared
diagonal of adjacent triangles to improve the triangulation while keeping the
vertex set fixed, so the output is a normal renderable mesh (unlike igl/
geometry-central intrinsic Delaunay, which flip geodesically and leave F).
Two criteria: delaunay (flip when opposite angles sum > 180 deg -> maximizes min
angle) and valence (Botsch-Kobbelt connectivity regularization toward ideal
valence 6/4 -- the flip step of isotropic remeshing).
Vectorized: the criterion is batched numpy over all edges; conflicting flips are
resolved per pass by a Luby maximal-independent-set step (random priority +
scatter-max over faces). igl.edge_flaps (C++) is the only library call per pass,
so no Python per-edge loop. Guards against non-manifold/duplicate edges, folds,
and degenerate triangles. ~66k flips on 327k faces in 4.4s; preserves Euler /
watertightness / manifoldness; carries per-vertex fields through.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Every Sharpen backend now attaches a per-vertex scalar field
"sharpen_displacement_magnitude" (how far each vertex moved) to the output
mesh, so you can colour/visualise where the sharpen acted (guarded by a
vertex-count check). Reuses the displacement already computed for the stats.
- Raise guided_normal normal_iterations ceiling 50 -> 200 (it's the main
strength knob; high values progressively flatten).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a "Time: Xs" line (perf_counter around the compute) to guided_normal,
l0_minimize, and curvature_guided so the node reports how long the sharpen took.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The second-order analogue of guided_normal: instead of filtering normals and
projecting onto planes, it filters the CURVATURE field and reconstructs via a
Laplacian-domain solve (differential-coordinate / Laplacian editing; Sorkine
2004, Lipman 2004).
Pipeline (torch sparse, CUDA): delta = L x (differential coords; |delta| ~ mean-
curvature normal) -> edge-aware bilateral filter of scalar |H| over the 1-ring,
range-weighted by normal difference (diffuses curvature within a region, stops at
edges) -> rebuild delta_f keeping each vertex's own direction -> reconstruct by
preconditioned-CG solving (L^T L + lambda I) x = L^T delta_f + lambda x0.
Key property (verified): magnitude-only filtering makes a sphere/cylinder a fixed
point (no flattening, no shrink). direction_blend>0 additionally smooths the delta
direction to denoise positional noise at some flattening risk (verified to move a
noisy sphere toward the clean one while staying spherical). Knobs: iterations,
sigma_s, sigma_r (deg), anchor_weight (Tikhonov), direction_blend, cg_iters, use_gpu.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Per-vertex curvature estimation with two backends, informed by DDG/GPU research:
- quadric_fit (default): libigl principal_curvature over a k-ring (robust on
irregular CAD tessellation; gives principal curvatures k1>=k2).
- ddg: cotangent-Laplacian mean curvature (Meyer et al. 2003) + angle-defect
Gaussian (Gauss-Bonnet); fast sparse mat-vec + scatter for large meshes.
Mean=(k1+k2)/2, Gaussian=k1*k2, principal recovered as H +/- sqrt(max(0,H^2-K)).
Attaches curvature_mean/gaussian/k1/k2/abs_max/shape_index as vertex_attributes
(colourable in the field viewers, usable as an adaptive-remeshing sizing field),
plus a canonical 'curvature' alias. Options: radius (k-ring), field smoothing,
percentile clamp (slivers), and pre-clean (merge/degenerate/fix-normals).
Validated on a known sphere (mean~0.5, K~0.25) and a cube (abs_max spikes on
edges); both methods agree with theory.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>