Skip to content

Add straight skeleton extrusion (extrude_skeleton) for roof generation - #74

Merged
petrasvestartas merged 1 commit into
mainfrom
feature/straight-skeleton-extrusion
Jul 22, 2026
Merged

petrasvestartas merged 1 commit into
mainfrom
feature/straight-skeleton-extrusion

Conversation

@petrasvestartas

@petrasvestartas petrasvestartas commented Jul 21, 2026 •

Copy link
Copy Markdown
Collaborator

Why this PR:

I received a request from a Rhino user:

Hi Petras,

Thanks for your work on compas_cgal.

I’m wondering if there are any plans to add cgal [extrude_skeleton] to compas?([CGAL 6.2 - 2D Straight Skeleton and Polygon Offsetting: Skeleton Extrusion](https://doc.cgal.org/latest/Straight_skeleton_2/group__PkgStraightSkeleton2Extrusion.html#ga0d1ccb10934bb54fb88a6174f30dfe49))

It would be super useful for roofs - I have several Australian suburbs to model!

All the best,
Paul Minifie
RMIT University

Summary

Adds a wrapper around CGAL's extrude_skeleton (CGAL 6.x 2D Straight Skeleton and Polygon Offsetting: Skeleton Extrusion), so a 2D polygon — a building footprint — can be turned into a closed 3D roof mesh with polygon faces. This directly answers the request to bring extrude_skeleton to COMPAS for roof modelling (e.g. modelling suburbs).

Based on the CGAL extrude_skeleton example and the improved straight skeleton write-up.

Screenshot

Straight Skeleton Extrusion

Usage

from compas.geometry import Polygon
from compas_cgal.straight_skeleton_2 import extrude

roof = extrude(Polygon(points), angles=45.0)                    # standard hip roof
courtyard = extrude(Polygon(points), holes=[hole])              # roof with a courtyard
capped = extrude(Polygon(points), maximum_height=1.5)           # truncated roof

Testing

python -m pytest tests/test_straight_skeleton_2_extrude.py — 7 passing; existing straight-skeleton tests still pass. ruff format / ruff check clean.

Wrap CGAL's extrude_skeleton so a 2D polygon (optionally with holes) becomes a
closed 3D roof mesh. extrude() returns a ready-to-display triangulated mesh plus
the roof outline (eaves, hips, ridges). Includes example, screenshot, and tests.
@petrasvestartas
petrasvestartas force-pushed the feature/straight-skeleton-extrusion branch from 540c99b to 0750087 Compare July 21, 2026 19:46
@petrasvestartas

Copy link
Copy Markdown
Collaborator Author

@jf--- could you review the pr?

@jf---

jf--- commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

would have expected this to be trivial but its much more involved that I'd initially expect. all for it.

@petrasvestartas
petrasvestartas merged commit 26aa204 into main Jul 22, 2026
8 checks passed

@jf--- jf--- left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the C++ binding, Python wrapper, tests, and docs. This is clean and correct — CGAL usage checks out, the build is wired, and the API fits the module. No blockers; one item worth fixing before merge, the rest are follow-ups.

Worth fixing before merge

Silent maximum_height <= 0 → unbounded roof. extrude() maps maximum_height=None → -1.0 (straight_skeleton_2.py:453) and the binding computes bounded = maximum_height > 0.0 (straight_skeleton_2.cpp:331). So maximum_height=0.0 (or any negative) silently yields an unbounded roof instead of erroring, and -1.0 doubles as both the None-sentinel and a plausible-looking user value. The C++ docstring documents "Values <= 0 mean unbounded" but the Python extrude() docstring (:393) doesn't. Suggest validating maximum_height > 0 in extrude() and raising, rather than overloading a numeric sentinel.

Follow-ups (non-blocking)

  • Coplanarity tolerance duplicated across the language boundary. C++ merges patches at cosine_of_maximum_angle(0.9998) (straight_skeleton_2.cpp:372, ~1.15°); Python filters roof edges at angle_tol=1.0 degree (straight_skeleton_2.py:322/:328, ~0.99985). Same near-coplanar threshold as two unlinked magic numbers in two files, and the Python default has no rationale comment. Worth naming it and noting the two are intentionally coupled so they don't drift.
  • Test gaps (test_straight_skeleton_2_extrude.py): the weights= path is only tested alongside angles= to assert exclusivity — nothing drives weights alone; _contour_speeds' per-edge / per-contour branches and both its ValueError paths are uncovered; the RuntimeError path (angles ≥ 90 without maximum_height) is untested; outline assertions are len(...) > 0 only.
  • Angle range not validated. CGAL requires angles strictly within (0, 180); out-of-range values fall through to the generic "Failed to extrude straight skeleton" RuntimeError (straight_skeleton_2.cpp:348/:353), whose text doesn't mention the range. A range-check in extrude() (or the range in the message) saves a debugging session.

Nits

  • example_straight_skeleton_2_extrude.md prose says taper angle (singular); the parameter is angles.
  • Footprint z-coordinates are dropped (the roof is built on the z=0 plane) — worth one docstring line.
  • The _roof_outline docstring uses an rST :: literal block; a fenced ```python block renders better under this repo's mkdocstrings.

What's good

  • CGAL usage is correct: angles passed in degrees, CCW-boundary / CW-holes orientation auto-fix, and triangulate_faces before remesh_planar_patches (remesh needs a triangle mesh).
  • Build wiring is in place: m.def("extrude_straight_skeleton", …) inside NB_MODULE(_straight_skeleton_2), compiled by CMake, with <nanobind/stl/vector.h> added for the vector<vector<int>> marshaling.
  • Ear-clipping n-gon roof faces (instead of centroid-fanning) so non-convex faces stay closed is a nice touch.
  • API is consistent with the module (polygon-in, high-level-object-out), and the ValueError model matches the sibling functions.

Happy to re-look once the maximum_height guard is in.

@jf---
jf--- deleted the feature/straight-skeleton-extrusion branch July 22, 2026 10:25
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