Skip to content

Commit 2c07439

Browse files
Merge pull request #57 from jf---/jf/polylines-module
add polylines module: simplify + closest point queries
2 parents 1e9c271 + c5dcef7 commit 2c07439

File tree

10 files changed

+557
-1
lines changed

10 files changed

+557
-1
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Added
1111
* Added `compas_cgal.skeletonization.mesh_skeleton_with_mapping`.
1212

13+
- Added `simplify_polyline` and `simplify_polylines` functions for polyline simplification using Douglas-Peucker algorithm
14+
- Added `closest_points_on_polyline` function for batch closest point queries on polylines
15+
16+
1317
### Changed
1418

1519
### Removed

CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,5 +236,5 @@ add_nanobind_module(_slicer src/slicer.cpp)
236236
add_nanobind_module(_straight_skeleton_2 src/straight_skeleton_2.cpp)
237237
add_nanobind_module(_triangulation src/triangulation.cpp)
238238
add_nanobind_module(_subdivision src/subdivision.cpp)
239-
239+
add_nanobind_module(_polylines src/polylines.cpp)
240240

66.3 KB
Loading
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import numpy as np
2+
from compas.geometry import Polyline
3+
from compas_viewer import Viewer
4+
from compas_viewer.config import Config
5+
6+
from compas_cgal.polylines import simplify_polyline
7+
8+
9+
# Create a complex polyline: spiral with noise
10+
n_points = 200
11+
t = np.linspace(0, 6 * np.pi, n_points)
12+
radius = 1 + t / 10
13+
noise = np.random.uniform(-0.05, 0.05, n_points)
14+
15+
x = radius * np.cos(t) + noise
16+
y = radius * np.sin(t) + noise
17+
z = t / 5
18+
19+
original_points = np.column_stack([x, y, z])
20+
21+
# Simplify with different thresholds
22+
simplified_high = simplify_polyline(original_points, threshold=0.5)
23+
24+
# Create polylines offset in X for visualization
25+
offset = 6.0
26+
original_polyline = Polyline(original_points.tolist())
27+
simplified_high_polyline = Polyline((simplified_high + [offset, 0, 0]).tolist())
28+
29+
# ==============================================================================
30+
# Visualize
31+
# ==============================================================================
32+
33+
config = Config()
34+
config.camera.target = [5, 0, 5]
35+
config.camera.position = [5, -20, 10]
36+
37+
viewer = Viewer(config=config)
38+
39+
viewer.scene.add(original_polyline, linewidth=2, show_points=False)
40+
viewer.scene.add(simplified_high_polyline, linewidth=3, show_points=False)
41+
42+
viewer.show()
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
Polyline Simplification
2+
=======================
3+
4+
This example demonstrates how to simplify polylines using the Douglas-Peucker algorithm in COMPAS CGAL.
5+
6+
Key Features:
7+
8+
* Douglas-Peucker polyline simplification
9+
* XY-plane simplification with Z-coordinate preservation
10+
* Side-by-side visualization of original and simplified polylines
11+
12+
.. figure:: /_images/example_simplify_polylines.png
13+
:figclass: figure
14+
:class: figure-img img-fluid
15+
16+
.. literalinclude:: example_simplify_polylines.py
17+
:language: python

src/compas_cgal/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"compas_cgal.triangulation",
2525
"compas_cgal.slicer",
2626
"compas_cgal.subdivision",
27+
"compas_cgal.polylines",
2728
]
2829

2930
__all__ = ["HOME", "DATA", "DOCS", "TEMP"]

src/compas_cgal/polylines.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
"""Polyline utilities using CGAL."""
2+
3+
from typing import List
4+
from typing import Union
5+
6+
import numpy as np
7+
from numpy.typing import NDArray
8+
9+
from compas_cgal import _types_std # noqa: F401 # Load vector type bindings
10+
from compas_cgal._polylines import closest_points_on_polyline as _closest_points
11+
from compas_cgal._polylines import simplify_polylines as _simplify
12+
13+
PointsList = Union[List[List[float]], NDArray]
14+
15+
16+
__all__ = ["simplify_polylines", "simplify_polyline", "closest_points_on_polyline"]
17+
18+
19+
def simplify_polylines(polylines: List[PointsList], threshold: float) -> List[NDArray]:
20+
"""Simplify multiple polylines using Douglas-Peucker algorithm.
21+
22+
Simplification is performed in the XY plane only. For 3D polylines,
23+
Z coordinates are preserved but not considered in distance calculations.
24+
25+
Parameters
26+
----------
27+
polylines : list of array-like
28+
List of polylines. Each polyline is a sequence of 2D or 3D points.
29+
threshold : float
30+
Distance threshold for simplification. Higher values remove more points.
31+
32+
Returns
33+
-------
34+
list of ndarray
35+
Simplified polylines as numpy arrays.
36+
37+
Examples
38+
--------
39+
>>> polylines = [[[0, 0], [1, 0.01], [2, 0]], [[0, 0], [0, 1], [1, 1]]]
40+
>>> simplified = simplify_polylines(polylines, threshold=0.1)
41+
>>> len(simplified[0]) # First polyline simplified
42+
2
43+
>>> len(simplified[1]) # Second has corner, preserved
44+
3
45+
46+
"""
47+
if threshold < 0:
48+
raise ValueError("threshold must be non-negative")
49+
arrays = [np.asarray(p, dtype=np.float64) for p in polylines]
50+
return _simplify(arrays, threshold)
51+
52+
53+
def simplify_polyline(polyline: PointsList, threshold: float) -> NDArray:
54+
"""Simplify a single polyline using Douglas-Peucker algorithm.
55+
56+
Simplification is performed in the XY plane only. For 3D polylines,
57+
Z coordinates are preserved but not considered in distance calculations.
58+
59+
Parameters
60+
----------
61+
polyline : array-like
62+
Sequence of 2D or 3D points.
63+
threshold : float
64+
Distance threshold for simplification.
65+
66+
Returns
67+
-------
68+
ndarray
69+
Simplified polyline as numpy array.
70+
71+
"""
72+
result = simplify_polylines([polyline], threshold)
73+
return result[0]
74+
75+
76+
def closest_points_on_polyline(query_points: PointsList, polyline: PointsList) -> NDArray:
77+
"""Find closest points on a polyline for a batch of query points.
78+
79+
Uses CGAL's AABB tree for efficient batch queries.
80+
81+
Parameters
82+
----------
83+
query_points : array-like
84+
Query points as Mx2 or Mx3 array.
85+
polyline : array-like
86+
Polyline as Nx2 or Nx3 array.
87+
88+
Returns
89+
-------
90+
ndarray
91+
Closest points on the polyline (same shape as query_points).
92+
93+
Examples
94+
--------
95+
>>> polyline = [[0, 0], [10, 0]]
96+
>>> queries = [[5, 5], [3, -2]]
97+
>>> closest = closest_points_on_polyline(queries, polyline)
98+
>>> closest[0] # Closest to (5, 5) on horizontal line
99+
array([5., 0.])
100+
101+
"""
102+
queries = np.asarray(query_points, dtype=np.float64)
103+
poly = np.asarray(polyline, dtype=np.float64)
104+
return _closest_points(queries, poly)

0 commit comments

Comments
 (0)