Skip to content

Commit ae1a931

Browse files
authored
Merge pull request #774 from mattiskoh/polyline_functions
Polyline functions
2 parents 9affb21 + d7e589d commit ae1a931

File tree

4 files changed

+178
-1
lines changed

4 files changed

+178
-1
lines changed

AUTHORS.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,5 @@
2828
- Beverly Lytle <<[email protected]>> [@beverlylytle](https://github.com/beverlylytle>)
2929
- Juney Lee <<[email protected]>> [@juney-lee](https://github.com/juney-lee)
3030
- Xingxin He <<[email protected]>> [@XingxinHE](https://github.com/XingxinHE)
31-
- Robin Godwyll <<[email protected]>> [@robin-gdwl](https://github.com/robin-gdwl)
31+
- Robin Godwyll <<[email protected]>> [@robin-gdwl](https://github.com/robin-gdwl)
32+
- Mattis Koh <<[email protected]>> [@mattiskoh](https://github.com/mattiskoh)

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99
## Unreleased
1010

1111
### Added
12+
* Added `divide_polyline`, `divide_polyline_by_length`, `Polyline.split_at_corners` and `Polyline.tangent_at_point_on_polyline`.
1213

1314
* Added the magic method `__str__` to `compas.geoemetry.Transformation`.
1415

src/compas/geometry/primitives/polyline.py

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from compas.geometry.primitives import Primitive
88
from compas.geometry.primitives import Point
99
from compas.geometry.primitives import Line
10+
from compas.geometry.predicates import is_point_on_line
1011

1112
from compas.utilities import pairwise
1213

@@ -258,11 +259,131 @@ def transform(self, T):
258259
self.points[index].y = point[1]
259260
self.points[index].z = point[2]
260261

262+
def split_at_corners(self, angle_threshold):
263+
"""Splits a polyline at corners larger than the given angle_threshold
261264
265+
Parameters:
266+
-----------
267+
angle_threshold : float
268+
In radians.
269+
270+
Returns
271+
-------
272+
list of :class:`compas.geometry.Polyline`
273+
274+
"""
275+
276+
corner_ids = []
277+
split_polylines = []
278+
points = self.points
279+
seg_ids = list(range(len(self.lines)))
280+
281+
if self.is_closed():
282+
seg_ids.append(0)
283+
284+
for seg1, seg2 in pairwise(seg_ids):
285+
angle = self.lines[seg1].vector.angle(self.lines[seg2].vector)
286+
if angle >= angle_threshold:
287+
corner_ids.append(seg1+1)
288+
289+
if self.is_closed() and len(corner_ids) > 0:
290+
if corner_ids[-1] != len(points):
291+
corner_ids = [corner_ids[-1]] + corner_ids
292+
else:
293+
corner_ids = [0] + corner_ids + [len(points)]
294+
295+
for id1, id2 in pairwise(corner_ids):
296+
if id1 < id2:
297+
split_polylines.append(Polyline(points[id1:id2+1]))
298+
else:
299+
looped_pts = [points[i] for i in range(id1, len(points))] + points[1:id2+1]
300+
split_polylines.append(Polyline(looped_pts))
301+
302+
if self.is_closed() and not corner_ids:
303+
return [Polyline(self.points)]
304+
305+
return split_polylines
306+
307+
def tangent_at_point_on_polyline(self, point):
308+
"""Calculates the tangent vector of a point on a polyline
309+
310+
Parameters:
311+
-----------
312+
point: :class:`compas.geometry.Point`
313+
314+
Returns
315+
-------
316+
:class:`compas.geometry.Vector`
317+
"""
318+
for line in self.lines:
319+
if is_point_on_line(point, line):
320+
return line.direction
321+
raise Exception('{} not found!'.format(point))
322+
323+
def divide_polyline(self, num_segments):
324+
"""Divide a polyline in equal segments.
325+
326+
Parameters:
327+
-----------
328+
num_segments : int
329+
330+
Returns
331+
-------
332+
list
333+
list of :class:`compas.geometry.Point`
334+
"""
335+
segment_length = self.length/num_segments
336+
337+
return self.divide_polyline_by_length(segment_length, False)
338+
339+
def divide_polyline_by_length(self, length, strict=True):
340+
"""Splits a polyline in segments of a given length
341+
342+
Parameters:
343+
-----------
344+
length : float
345+
346+
strict : bool
347+
If set to ``False``, the remainder segment will be added even if it is smaller than the desired length
348+
349+
Returns
350+
-------
351+
list
352+
list of :class:`compas.geometry.Point`
353+
"""
354+
num_pts = int(self.length/length)
355+
total_length = [0, 0]
356+
division_pts = [self.points[0]]
357+
new_polyline = self
358+
for i in range(num_pts):
359+
for i_ln, line in enumerate(new_polyline.lines):
360+
total_length.append(total_length[-1] + line.length)
361+
if total_length[-1] > length:
362+
amp = (length - total_length[-2]) / line.length
363+
new_pt = line.start + line.vector.scaled(amp)
364+
division_pts.append(new_pt)
365+
total_length = [0, 0]
366+
remaining_pts = new_polyline.points[i_ln+2:]
367+
new_polyline = Polyline([new_pt, line.end] + remaining_pts)
368+
break
369+
elif total_length[-1] == length:
370+
total_length = [0, 0]
371+
division_pts.append(line.end)
372+
373+
if len(division_pts) == num_pts+1:
374+
break
375+
376+
if strict is False and not self.is_closed() and len(division_pts) < num_pts+1:
377+
division_pts.append(new_polyline.points[-1])
378+
elif strict is False and division_pts[-1] != self.points[-1]:
379+
division_pts.append(self.points[-1])
380+
381+
return division_pts
262382
# ==============================================================================
263383
# Main
264384
# ==============================================================================
265385

386+
266387
if __name__ == '__main__':
267388

268389
import doctest
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import compas
2+
import pytest
3+
from compas.geometry import Polyline, Point
4+
import math
5+
6+
7+
@pytest.mark.parametrize('coords,expected', [
8+
([[0.0, 0.0, 0.0], [100.0, 0.0, 0.0]], [[0.0, 0.0, 0.0], [20.0, 0.0, 0.0], [40.0, 0.0, 0.0], [60.0, 0.0, 0.0], [80.0, 0.0, 0.0], [100.0, 0.0, 0.0]]),
9+
([[0.0, 0.0, 0.0], [100.0, 0.0, 0.0], [300.0, 0.0, 0.0]], [[0.0, 0.0, 0.0], [60.0, 0.0, 0.0], [120.0, 0.0, 0.0], [180.0, 0.0, 0.0], [240.0, 0.0, 0.0], [300.0, 0.0, 0.0]]),
10+
([[0.0, 0.0, 0.0], [200.0, 0.0, 0.0], [200.0, 200.0, 0.0], [0.0, 200.0, 0.0], [0.0, 0.0, 0.0]], [
11+
[0.0, 0.0, 0.0], [160.0, 0.0, 0.0], [200.0, 120.0, 0.0], [120.0, 200.0, 0.0], [0.0, 160.0, 0.0], [0.0, 0.0, 0.0]])
12+
])
13+
def test_polyline_divide(coords, expected):
14+
assert expected == Polyline(coords).divide_polyline(5)
15+
16+
17+
@pytest.mark.parametrize('coords,expected', [
18+
([[0.0, 0.0, 0.0], [100.0, 0.0, 0.0]], [[0.0, 0.0, 0.0], [100.0, 0.0, 0.0]]),
19+
([[0.0, 0.0, 0.0], [100.0, 0.0, 0.0], [300.0, 0.0, 0.0]], [[0, 0, 0], [100, 0, 0], [200, 0, 0], [300, 0, 0]]),
20+
([[0.0, 0.0, 0.0], [200.0, 0.0, 0.0], [200.0, 200.0, 0.0], [0.0, 200.0, 0.0], [0.0, 0.0, 0.0]], [[0, 0, 0], [
21+
100, 0, 0], [200, 0, 0], [200, 100, 0], [200, 200, 0], [100.0, 200, 0], [0, 200, 0], [0, 100.0, 0], [0, 0, 0]])
22+
])
23+
def test_polyline_divide_length(coords, expected):
24+
assert expected == Polyline(coords).divide_polyline_by_length(100)
25+
26+
27+
@pytest.mark.parametrize('coords,expected', [
28+
([[0.0, 0.0, 0.0], [100.0, 0.0, 0.0]], [[0.0, 0.0, 0.0], [80.0, 0.0, 0.0]]),
29+
])
30+
def test_polyline_divide_length_strict1(coords, expected):
31+
assert expected == Polyline(coords).divide_polyline_by_length(80)
32+
33+
34+
@pytest.mark.parametrize('coords,expected', [
35+
([[0.0, 0.0, 0.0], [100.0, 0.0, 0.0]], [[0.0, 0.0, 0.0], [80.0, 0.0, 0.0], [100.0, 0.0, 0.0]]),
36+
])
37+
def test_polyline_divide_length_strict2(coords, expected):
38+
assert expected == Polyline(coords).divide_polyline_by_length(80, False)
39+
40+
41+
@pytest.mark.parametrize('coords,input,expected', [
42+
([[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 1.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 0.0]], math.pi/2, [Polyline([[0.0, 0.0, 0.0], [1, 0.0, 0.0]]),
43+
Polyline([[1, 0.0, 0.0], [1, 1, 0.0]]), Polyline([[1, 1, 0.0], [0.0, 1, 0.0]]), Polyline([[0.0, 1, 0.0], [0.0, 0.0, 0.0]])]),
44+
])
45+
def test_polyline_split_at_corners(coords, input, expected):
46+
assert expected == Polyline(coords).split_at_corners(input)
47+
48+
49+
@pytest.mark.parametrize('coords,input,expected', [
50+
([[0.0, 0.0, 0.0], [100.0, 0.0, 0.0]], [50, 0, 0], [1.0, 0.0, 0.0]),
51+
([[0.0, 0.0, 0.0], [50.0, 0.0, 0.0], [100.0, 100.0, 0.0]], [50, 0, 0], [1.0, 0.0, 0.0]),
52+
])
53+
def test_polyline_tangent_at_point(coords, input, expected):
54+
assert expected == Polyline(coords).tangent_at_point_on_polyline(input)

0 commit comments

Comments
 (0)