diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d932a4b..c9beb4a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +### Changed + +### Removed + +## [0.9.5] 2026-07-22 + +### Added + * Added `protect_boundary`, `protect_sharp_edges_angle_deg`, and `keep_points` parameters to `compas_cgal.meshing.trimesh_remesh`. `protect_boundary` constrains all boundary edges (no split/collapse/flip) so an open mesh's border curve — including sharp corners — is preserved verbatim instead of being re-sampled to `target_edge_length`; `protect_sharp_edges_angle_deg` marks interior edges whose dihedral angle ≥ the threshold as constrained (`0.0` disables interior-feature detection); `keep_points` (an Nx3 array) pins only the mesh vertices coincident with the given points (matched by coordinate within tolerance, via `vertex_is_constrained_map`), so specific vertices — e.g. the four corners of an open patch — survive while the rest of the boundary is still re-sampled. * Reworked `docs/examples/example_meshing.py` / `.md` to demonstrate all three boundary modes on the RhinoVault shell side by side (default, `protect_boundary=True`, and `keep_points` with the four corners), replacing the example image. * Added `compas_cgal.straight_skeleton_2.extrude` wrapping CGAL's `extrude_skeleton`, turning a 2D polygon (optionally with holes) into a closed 3D roof mesh, with control over roof pitch via taper `angles` or straight skeleton `weights` and an optional `maximum_height`. Internally coplanar extrusion faces are merged into polygons (via `remesh_planar_patches`) to recover the true roof planes, and `extrude` returns a `(mesh, lines)` tuple: a triangulated, ready-to-display mesh (so non-convex roof faces render correctly) and the roof outline (eaves, hips and ridges) as a list of `compas.geometry.Line`. diff --git a/docs/assets/images/example_skeletonization_with_mapping.png b/docs/assets/images/example_skeletonization_with_mapping.png index f4aa7830..02cbcec2 100644 Binary files a/docs/assets/images/example_skeletonization_with_mapping.png and b/docs/assets/images/example_skeletonization_with_mapping.png differ diff --git a/docs/examples/example_skeletonization_with_mapping.py b/docs/examples/example_skeletonization_with_mapping.py index f5430fa5..3f4b9533 100644 --- a/docs/examples/example_skeletonization_with_mapping.py +++ b/docs/examples/example_skeletonization_with_mapping.py @@ -8,6 +8,7 @@ from compas.geometry import Scale from compas.geometry import Translation from compas_viewer import Viewer +from compas_viewer.config import Config from compas_cgal.skeletonization import mesh_skeleton_with_mapping @@ -49,12 +50,21 @@ # Viz # ============================================================================= -viewer = Viewer() - -viewer.renderer.camera.target = [0, 0, 1.5] -viewer.renderer.camera.position = [-5, -5, 1.5] - -viewer.scene.add(mesh, opacity=0.2, show_points=False, facecolor=[0.9, 0.9, 0.9]) +config = Config() +config.renderer.show_grid = False +config.camera.target = [0.2, 0.0, 1.5] +config.camera.position = [-4.8, -4.8, 2.0] + +viewer = Viewer(config=config) + +viewer.scene.add( + mesh, + opacity=0.6, + show_points=False, + show_lines=True, + facecolor=[0.85, 0.85, 0.85], + linecolor=[0.4, 0.4, 0.4], +) for idx in start_indices: viewer.scene.add(Point(*v[idx]), pointcolor=[1.0, 0.0, 0.0], pointsize=15) # red = start diff --git a/pyproject.toml b/pyproject.toml index 0709d6d5..67e629bb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -95,7 +95,7 @@ macos.archs = ["x86_64", "arm64"] # ============================================================================ [tool.bumpversion] -current_version = "0.9.3" +current_version = "0.9.5" message = "Bump version to {new_version}" commit = true tag = true diff --git a/src/compas_cgal/__init__.py b/src/compas_cgal/__init__.py index 7d24526d..03b34a39 100644 --- a/src/compas_cgal/__init__.py +++ b/src/compas_cgal/__init__.py @@ -5,7 +5,7 @@ __copyright__ = "Block Research Group - ETH Zurich" __license__ = "MIT License" __email__ = ["van.mele@arch.ethz.ch", "vestartas@arch.ethz.ch"] -__version__ = "0.9.3" +__version__ = "0.9.5" HERE = os.path.dirname(__file__) diff --git a/tests/test_meshing.py b/tests/test_meshing.py index f1104588..eff35529 100644 --- a/tests/test_meshing.py +++ b/tests/test_meshing.py @@ -72,10 +72,14 @@ def square_with_hole(): # corners. 8 triangles total. F = np.asarray( [ - [0, 1, 5], [0, 5, 4], - [1, 2, 6], [1, 6, 5], - [2, 3, 7], [2, 7, 6], - [3, 0, 4], [3, 4, 7], + [0, 1, 5], + [0, 5, 4], + [1, 2, 6], + [1, 6, 5], + [2, 3, 7], + [2, 7, 6], + [3, 0, 4], + [3, 4, 7], ], dtype=np.int32, ) @@ -91,9 +95,7 @@ def test_remesh_default_subdivides_boundary(square_with_hole): V, F = square_with_hole V_new, F_new = trimesh_remesh((V, F), target_edge_length=0.5, number_of_iterations=5) # Boundary subdivision adds vertices on outer + inner loops. - assert V_new.shape[0] > V.shape[0], ( - f"default mode should add boundary verts; got {V_new.shape[0]} <= {V.shape[0]}" - ) + assert V_new.shape[0] > V.shape[0], f"default mode should add boundary verts; got {V_new.shape[0]} <= {V.shape[0]}" def test_remesh_protect_boundary_keeps_corners(square_with_hole): @@ -133,15 +135,13 @@ def test_remesh_protect_boundary_keeps_boundary_vertex_count(square_with_hole): i, j = int(face[k]), int(face[(k + 1) % 3]) he_set.add((i, j)) boundary_verts = set() - for (i, j) in he_set: + for i, j in he_set: if (j, i) not in he_set: boundary_verts.add(i) boundary_verts.add(j) # Original boundary had 8 verts (4 outer + 4 inner); protect_boundary # forbids splitting boundary edges so count must stay exactly 8. - assert len(boundary_verts) == 8, ( - f"protect_boundary=True must keep 8 boundary verts; got {len(boundary_verts)}" - ) + assert len(boundary_verts) == 8, f"protect_boundary=True must keep 8 boundary verts; got {len(boundary_verts)}" def test_remesh_protect_sharp_edges_default_disabled(): @@ -150,9 +150,7 @@ def test_remesh_protect_sharp_edges_default_disabled(): V = np.asarray([[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0]], dtype=np.float64) F = np.asarray([[0, 1, 2], [0, 2, 3]], dtype=np.int32) V_a, F_a = trimesh_remesh((V, F), 0.3, number_of_iterations=5) - V_b, F_b = trimesh_remesh( - (V, F), 0.3, number_of_iterations=5, protect_sharp_edges_angle_deg=0.0 - ) + V_b, F_b = trimesh_remesh((V, F), 0.3, number_of_iterations=5, protect_sharp_edges_angle_deg=0.0) np.testing.assert_array_equal(V_a.shape, V_b.shape) np.testing.assert_array_equal(F_a.shape, F_b.shape)