Skip to content

Commit 1bfab20

Browse files
committed
refactor: use compas_cgal.isolines for contour extraction
1 parent 8c1027f commit 1bfab20

File tree

11 files changed

+143
-406
lines changed

11 files changed

+143
-406
lines changed

docs/concepts/slicing-algorithms.md

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -185,29 +185,21 @@ To build connected contours:
185185

186186
## Contour Assembly
187187

188-
All slicers eventually produce contours via the `ScalarFieldContours` class:
189-
190-
### From Crossings to Paths
188+
All slicers produce contours via `ScalarFieldContours`, which uses CGAL's isoline extraction:
191189

192190
```mermaid
193191
flowchart LR
194-
A[Edge crossings] --> B[Face traversal]
195-
B --> C[Connected polylines]
192+
A[Scalar field on vertices] --> B[CGAL isolines]
193+
B --> C[Sorted polylines]
196194
C --> D[Path objects]
197195
```
198196

199-
1. **Build crossing map**: Dictionary of edge → crossing point
200-
2. **Traverse faces**: Walk around faces connecting crossings
201-
3. **Handle branches**: Multiple paths per layer for complex geometry
202-
4. **Create Paths**: Wrap polylines in Path objects with metadata
203-
204-
### Handling Complex Topology
205-
206-
The algorithm handles:
197+
The CGAL backend (`compas_cgal.isolines`) handles:
207198

208-
- **Multiple contours per layer**: Holes, disconnected regions
209-
- **Open contours**: When path hits mesh boundary
210-
- **Branching**: When contours merge or split
199+
- **Edge crossing detection**: Finding zero-crossings on mesh edges
200+
- **Polyline assembly**: Connecting crossings into coherent curves
201+
- **Multiple contours**: Holes, disconnected regions, branching
202+
- **Open/closed detection**: Identifying boundary-hitting paths
211203

212204
## Performance Considerations
213205

docs/examples/05_scalar_field.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -193,9 +193,9 @@ Creates concentric circular layers (spiral vase mode).
193193
### Geodesic Field
194194

195195
```python
196-
# Using igl for geodesic distance from boundary vertices
197-
import igl
198-
distances = igl.exact_geodesic(V, F, boundary_vertices)
196+
# Using CGAL for geodesic distance from boundary vertices
197+
from compas_cgal.geodesics import heat_geodesic_distances
198+
distances = heat_geodesic_distances((V, F), boundary_vertices)
199199
```
200200

201201
Creates layers that follow surface curvature.

examples/2_curved_slicing/ex2_curved_slicing.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import time
22
from pathlib import Path
33

4+
import numpy as np
45
from compas.datastructures import Mesh
56

67
import compas_slicer.utilities as utils
78
from compas_slicer.config import InterpolationConfig
8-
from compas_slicer.post_processing import seams_smooth, simplify_paths_rdp
9+
from compas_slicer.post_processing import seams_smooth
910
from compas_slicer.pre_processing import InterpolationSlicingPreprocessor, create_mesh_boundary_attributes
1011
from compas_slicer.print_organization import (
1112
InterpolationPrintOrganizer,
@@ -28,9 +29,12 @@ def main(visualize: bool = False):
2829
# Load initial_mesh
2930
mesh = Mesh.from_obj(DATA_PATH / 'mesh.obj')
3031

31-
# Load targets (boundaries)
32-
low_boundary_vs = utils.load_from_json(DATA_PATH, 'boundaryLOW.json')
33-
high_boundary_vs = utils.load_from_json(DATA_PATH, 'boundaryHIGH.json')
32+
# Identify boundaries from mesh topology
33+
boundaries = [list(loop) for loop in mesh.vertices_on_boundaries()]
34+
avg_zs = [np.mean([mesh.vertex_coordinates(v)[2] for v in loop]) for loop in boundaries]
35+
low_idx = int(np.argmin(avg_zs))
36+
low_boundary_vs = boundaries.pop(low_idx)
37+
high_boundary_vs = [v for loop in boundaries for v in loop] # flatten remaining
3438
create_mesh_boundary_attributes(mesh, low_boundary_vs, high_boundary_vs)
3539

3640
avg_layer_height = 2.0
@@ -48,7 +52,6 @@ def main(visualize: bool = False):
4852
slicer = InterpolationSlicer(mesh, preprocessor, config)
4953
slicer.slice_model()
5054

51-
simplify_paths_rdp(slicer, threshold=0.25)
5255
seams_smooth(slicer, smooth_distance=3)
5356
slicer.printout_info()
5457
utils.save_to_json(slicer.to_data(), OUTPUT_PATH, 'curved_slicer.json')

src/compas_slicer/config.py

Lines changed: 0 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
"GcodeConfig",
2626
"PrintConfig",
2727
"OutputConfig",
28-
"GeodesicsMethod",
2928
"UnionMethod",
3029
"load_defaults",
3130
]
@@ -50,15 +49,6 @@ def load_defaults() -> dict[str, Any]:
5049
_DEFAULTS = load_defaults()
5150

5251

53-
class GeodesicsMethod(str, Enum):
54-
"""Method for computing geodesic distances."""
55-
56-
EXACT_IGL = "exact_igl"
57-
HEAT_IGL = "heat_igl"
58-
HEAT_CGAL = "heat_cgal"
59-
HEAT = "heat"
60-
61-
6252
class UnionMethod(str, Enum):
6353
"""Method for combining target boundaries."""
6454

@@ -162,10 +152,6 @@ class InterpolationConfig(Data):
162152
Maximum layer height.
163153
vertical_layers_max_centroid_dist : float
164154
Maximum distance for grouping paths into vertical layers.
165-
target_low_geodesics_method : GeodesicsMethod
166-
Method for computing geodesics to low boundary.
167-
target_high_geodesics_method : GeodesicsMethod
168-
Method for computing geodesics to high boundary.
169155
target_high_union_method : UnionMethod
170156
Method for combining high target boundaries.
171157
target_high_union_params : list[float]
@@ -181,12 +167,6 @@ class InterpolationConfig(Data):
181167
vertical_layers_max_centroid_dist: float = field(
182168
default_factory=lambda: _interpolation_defaults().get("vertical_layers_max_centroid_dist", 25.0)
183169
)
184-
target_low_geodesics_method: GeodesicsMethod = field(
185-
default_factory=lambda: GeodesicsMethod(_interpolation_defaults().get("target_low_geodesics_method", "heat_igl"))
186-
)
187-
target_high_geodesics_method: GeodesicsMethod = field(
188-
default_factory=lambda: GeodesicsMethod(_interpolation_defaults().get("target_high_geodesics_method", "heat_igl"))
189-
)
190170
target_high_union_method: UnionMethod = field(
191171
default_factory=lambda: UnionMethod(_interpolation_defaults().get("target_high_union_method", "min"))
192172
)
@@ -199,11 +179,6 @@ class InterpolationConfig(Data):
199179

200180
def __post_init__(self) -> None:
201181
super().__init__()
202-
# Convert string enums if needed
203-
if isinstance(self.target_low_geodesics_method, str):
204-
self.target_low_geodesics_method = GeodesicsMethod(self.target_low_geodesics_method)
205-
if isinstance(self.target_high_geodesics_method, str):
206-
self.target_high_geodesics_method = GeodesicsMethod(self.target_high_geodesics_method)
207182
if isinstance(self.target_high_union_method, str):
208183
self.target_high_union_method = UnionMethod(self.target_high_union_method)
209184

@@ -214,8 +189,6 @@ def __data__(self) -> dict[str, Any]:
214189
"min_layer_height": self.min_layer_height,
215190
"max_layer_height": self.max_layer_height,
216191
"vertical_layers_max_centroid_dist": self.vertical_layers_max_centroid_dist,
217-
"target_low_geodesics_method": self.target_low_geodesics_method.value,
218-
"target_high_geodesics_method": self.target_high_geodesics_method.value,
219192
"target_high_union_method": self.target_high_union_method.value,
220193
"target_high_union_params": self.target_high_union_params,
221194
"uneven_upper_targets_offset": self.uneven_upper_targets_offset,
@@ -231,12 +204,6 @@ def __from_data__(cls, data: dict[str, Any]) -> InterpolationConfig:
231204
vertical_layers_max_centroid_dist=data.get(
232205
"vertical_layers_max_centroid_dist", d.get("vertical_layers_max_centroid_dist", 25.0)
233206
),
234-
target_low_geodesics_method=data.get(
235-
"target_low_geodesics_method", d.get("target_low_geodesics_method", "heat_igl")
236-
),
237-
target_high_geodesics_method=data.get(
238-
"target_high_geodesics_method", d.get("target_high_geodesics_method", "heat_igl")
239-
),
240207
target_high_union_method=data.get("target_high_union_method", d.get("target_high_union_method", "min")),
241208
target_high_union_params=data.get("target_high_union_params", d.get("target_high_union_params", [])),
242209
uneven_upper_targets_offset=data.get(

src/compas_slicer/data/defaults.toml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@ avg_layer_height = 5.0
1111
min_layer_height = 0.5
1212
max_layer_height = 10.0
1313
vertical_layers_max_centroid_dist = 25.0
14-
target_low_geodesics_method = "heat_cgal"
15-
target_high_geodesics_method = "heat_cgal"
1614
target_high_union_method = "min"
1715
target_high_union_params = []
1816
uneven_upper_targets_offset = 0.0

src/compas_slicer/pre_processing/interpolation_slicing_preprocessor.py

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -63,23 +63,15 @@ def create_compound_targets(self) -> None:
6363
"""Create target_LOW and target_HIGH and compute geodesic distances."""
6464

6565
# --- low target
66-
geodesics_method = self.config.target_low_geodesics_method.value
67-
method = 'min' # no other union methods currently supported for lower target
68-
params: list[float] = []
69-
self.target_LOW = CompoundTarget(self.mesh, 'boundary', 1, self.DATA_PATH,
70-
union_method=method,
71-
union_params=params,
72-
geodesics_method=geodesics_method)
66+
self.target_LOW = CompoundTarget(self.mesh, 'boundary', 1, self.DATA_PATH)
7367

7468
# --- high target
75-
geodesics_method = self.config.target_high_geodesics_method.value
7669
method = self.config.target_high_union_method.value
7770
params = self.config.target_high_union_params
7871
logger.info(f"Creating target with union type: {method} and params: {params}")
79-
self.target_HIGH = CompoundTarget(self.mesh, 'boundary', 2, self.DATA_PATH,
80-
union_method=method,
81-
union_params=params,
82-
geodesics_method=geodesics_method)
72+
self.target_HIGH = CompoundTarget(
73+
self.mesh, 'boundary', 2, self.DATA_PATH, union_method=method, union_params=params
74+
)
8375

8476
# --- uneven boundaries of high target
8577
self.target_HIGH.offset = self.config.uneven_upper_targets_offset

src/compas_slicer/pre_processing/preprocessing_utils/compound_target.py

Lines changed: 5 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,8 @@
1111
from numpy.typing import NDArray
1212

1313
import compas_slicer.utilities as utils
14-
from compas_slicer.pre_processing.preprocessing_utils.geodesics import (
15-
get_cgal_HEAT_geodesic_distances,
16-
get_custom_HEAT_geodesic_distances,
17-
get_igl_EXACT_geodesic_distances,
18-
get_igl_HEAT_geodesic_distances,
19-
)
20-
21-
GeodesicsMethod = Literal['exact_igl', 'heat_igl', 'heat_cgal', 'heat']
14+
from compas_slicer.pre_processing.preprocessing_utils.geodesics import get_heat_geodesic_distances
15+
2216
UnionMethod = Literal['min', 'smooth', 'chamfer', 'stairs']
2317

2418

@@ -57,11 +51,6 @@ class CompoundTarget:
5751
DATA_PATH: str
5852
has_blend_union: bool
5953
blend_radius : float
60-
geodesics_method: str
61-
'heat_cgal' CGAL heat geodesic distances (recommended)
62-
'heat' custom heat geodesic distances
63-
anisotropic_scaling: bool
64-
This is not yet implemented
6554
"""
6655

6756
def __init__(
@@ -72,8 +61,6 @@ def __init__(
7261
DATA_PATH: str,
7362
union_method: UnionMethod = 'min',
7463
union_params: list[Any] | None = None,
75-
geodesics_method: GeodesicsMethod = 'heat_cgal',
76-
anisotropic_scaling: bool = False,
7764
) -> None:
7865

7966
if union_params is None:
@@ -89,9 +76,6 @@ def __init__(
8976
self.union_method = union_method
9077
self.union_params = union_params
9178

92-
self.geodesics_method = geodesics_method
93-
self.anisotropic_scaling = anisotropic_scaling # Anisotropic scaling not yet implemented
94-
9579
self.offset = 0
9680
self.VN = len(list(self.mesh.vertices()))
9781

@@ -145,20 +129,9 @@ def compute_geodesic_distances(self) -> None:
145129
Computes the geodesic distances from each of the target's neighborhoods to all the mesh vertices.
146130
Fills in the distances attributes.
147131
"""
148-
if self.geodesics_method == 'exact_igl':
149-
distances_lists = [get_igl_EXACT_geodesic_distances(self.mesh, vstarts) for vstarts in
150-
self.clustered_vkeys]
151-
elif self.geodesics_method == 'heat_igl':
152-
distances_lists = [get_igl_HEAT_geodesic_distances(self.mesh, vstarts) for vstarts in
153-
self.clustered_vkeys]
154-
elif self.geodesics_method == 'heat_cgal':
155-
distances_lists = [get_cgal_HEAT_geodesic_distances(self.mesh, vstarts) for vstarts in
156-
self.clustered_vkeys]
157-
elif self.geodesics_method == 'heat':
158-
distances_lists = [get_custom_HEAT_geodesic_distances(self.mesh, vstarts, str(self.OUTPUT_PATH)) for vstarts in
159-
self.clustered_vkeys]
160-
else:
161-
raise ValueError('Unknown geodesics method : ' + self.geodesics_method)
132+
distances_lists = [
133+
get_heat_geodesic_distances(self.mesh, vstarts) for vstarts in self.clustered_vkeys
134+
]
162135

163136
distances_lists = [list(dl) for dl in distances_lists] # number_of_boundaries x #V
164137
self.update_distances_lists(distances_lists)

0 commit comments

Comments
 (0)