Skip to content

feat(meshing): trimesh_remesh — protect_boundary + sharp-feature edge constraints - #72

Merged
petrasvestartas merged 6 commits into
compas-dev:mainfrom
jf---:jf/trimesh-remesh-protect-features
Jul 22, 2026
Merged

feat(meshing): trimesh_remesh — protect_boundary + sharp-feature edge constraints#72
petrasvestartas merged 6 commits into
compas-dev:mainfrom
jf---:jf/trimesh-remesh-protect-features

Conversation

@jf---

@jf--- jf--- commented May 19, 2026

Copy link
Copy Markdown
Collaborator

Summary

Expose CGAL's edge_is_constrained_map + protect_constraints parameters from Polygon_mesh_processing::isotropic_remeshing via two new optional kwargs on trimesh_remesh. Both default to disabled, so the change is backward-compatible.

  • protect_boundary=True — every boundary edge is constrained, preserving the input boundary curve (including sharp corners) verbatim. Without this, CGAL's default smoothing pass re-samples boundary edges to target_edge_length and rounds sharp corners.
  • protect_sharp_edges_angle_deg=>0 — additionally constrain interior edges whose adjacent faces meet at a dihedral angle ≥ threshold (via PMP::detect_sharp_edges).

Motivation

Discovered while remeshing curved 3D-printing layers (annular cross-sections of a torus). With the default trimesh_remesh, sharp boundary corners get visibly rounded because CGAL smooths them along with interior vertices — there was no way from Python to mark those edges as features.

Implementation

Follows the existing edge_is_constrained_map pattern already used in src/isolines.cpp, src/geodesics.cpp, and src/booleans.cpp: a boolean property map is built on the mesh, marked for the protected edges, then passed to PMP via parameters().edge_is_constrained_map(ecm).protect_constraints(True).

PMP::detect_sharp_edges is also imported (one extra header — CGAL/Polygon_mesh_processing/detect_features.h).

Tests

Added under tests/test_meshing.py (4 new cases, all passing alongside the existing 2):

  • test_remesh_default_subdivides_boundary — default mode (no protection) subdivides boundary verts on an annular layer, as before.
  • test_remesh_protect_boundary_keeps_corners — every original corner of an outer + inner square boundary survives verbatim under protect_boundary=True.
  • test_remesh_protect_boundary_keeps_boundary_vertex_count — boundary-vertex count stays exactly 8 (4 outer + 4 inner) with protect_boundary=True; no inserts.
  • test_remesh_protect_sharp_edges_default_disabledprotect_sharp_edges_angle_deg=0.0 (default) produces identical output to omitting the kwarg, confirming backward compatibility.

Backward compatibility

Both new params default to their old behaviour (protect_boundary=False, protect_sharp_edges_angle_deg=0.0). No existing call sites change.

@jf---

jf--- commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator Author

Pssst @petrasvestartas && || @tomvanmele

@petrasvestartas

petrasvestartas commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

I have not seen it:) I will check now.

@petrasvestartas

petrasvestartas commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

@jf--- please provide documentation example, does it make sense to modify this example to show the difference:
https://compas.dev/compas_cgal/examples/example_meshing/

@petrasvestartas

Copy link
Copy Markdown
Collaborator

@jf--- would it be possible to make these little adjustments so that we could merge this?

@jf---

jf--- commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator Author

yes I'm on it

@petrasvestartas

Copy link
Copy Markdown
Collaborator

@jf--- claude can take screenshots from the compas_viewer so that docs examples can be generated more easy :)

jf--- added 4 commits July 22, 2026 12:38
…s_angle_deg

Expose CGAL's edge_is_constrained_map + protect_constraints for
isotropic_remeshing. Two new optional kwargs (both default-disabled,
backward-compatible):

- protect_boundary=True  → all boundary edges constrained; preserves
  boundary curves verbatim including sharp corners that the default
  smoothing pass otherwise rounds.
- protect_sharp_edges_angle_deg=>0  → also constrain interior edges
  with dihedral angle >= threshold (via PMP::detect_sharp_edges).

Pattern matches existing edge_is_constrained_map usage in
src/{isolines,geodesics,booleans}.cpp.
…_sharp_edges_angle_deg

- default mode: boundary IS subdivided on annular layer
- protect_boundary=True: all 8 original corners survive verbatim
- protect_boundary=True: boundary-vertex count stays exactly 8 (no inserts)
- protect_sharp_edges_angle_deg=0.0: no behavioural change vs unset
star disk remeshed OFF vs ON, false-colored by vertex density, labelled.
@jf---
jf--- force-pushed the jf/trimesh-remesh-protect-features branch from ac5d001 to 92826a5 Compare July 22, 2026 11:38
@jf---

jf--- commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator Author

Done — rebased onto current main (resolves the conflict) and reworked example_meshing.py to demonstrate protect_boundary, as you suggested.

It remeshes a dense star disk to a coarse target two ways, side by side and false-colored by local vertex density:

  • protect_boundary=False → the rim collapses to ~37 vertices (coarse, uniformly cold).
  • protect_boundary=True → the ~330-vertex perimeter is maintained (the hot band hugging the rim).

A star rim (rather than the convex RhinoVault shell) makes the density difference legible. Ready for another look.

@jf---

jf--- commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator Author

shoot, the example now is a remeshing specific example.
that should have been an additive not a replacement example @petrasvestartas

Keep the original RhinoVault shell geometry and enhance the existing example
in place, instead of replacing it with a synthetic star disk. It now remeshes
the shell to a coarse target two ways, side by side over the faint original:

- protect_boundary=False (default): the open perimeter is coarsened, corners
  rounded (~9 boundary verts at target=3).
- protect_boundary=True: every boundary edge is constrained, so all 41
  perimeter verts and the 4 corners survive while the interior coarsens.

Restores docs/examples/example_meshing.{py,md} + image to the RhinoVault
example and augments it; no separate example file, no nav change.
@petrasvestartas

Copy link
Copy Markdown
Collaborator

I added another functionality: user can provide corners points to snap to.
In the rhinovault like meshes, anchor points are often needed to be snapped:
image

…coordinate

Add an optional keep_points (Nx3) argument to trimesh_remesh. Each point is
snapped to its coincident mesh vertex (within 1e-6) and marked in a
vertex_is_constrained_map, so isotropic_remeshing neither moves nor removes it
while the boundary edges between such points are still re-sampled. This is the
coordinate-snap behaviour already used by trimesh_remesh_dual, now exposed on
the plain remesh. The vertex-constrained map is attached only when points are
supplied, leaving the default and protect_boundary paths byte-identical.

Also rework example_meshing to show all three boundary modes side by side
(default | protect_boundary | keep_points = 4 corners) and update its image.
@petrasvestartas
petrasvestartas merged commit a202cf1 into compas-dev:main Jul 22, 2026
8 checks passed
@jf---

jf--- commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator Author

perfect, thanks for the review & really do enjoy working with you on compas_cgal!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants