diff --git a/CHANGELOG.md b/CHANGELOG.md index aa45abc0ae5..34f5557a191 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +* Added `compas.geometry.Line.point_from_start` and `compas.geometry.Line.point_from_end`. +* Added `compas.geometry.Line.flip` and `compas.geometry.Line.flipped`. * Added an `compas.geometry.Frame.interpolate_frame(s)` method * Added `compas.colors.Color.contrast`. * Added `compas.geometry.Brep.from_plane`. diff --git a/src/compas/geometry/curves/line.py b/src/compas/geometry/curves/line.py index e0aa9dba02c..427d82511a3 100644 --- a/src/compas/geometry/curves/line.py +++ b/src/compas/geometry/curves/line.py @@ -297,17 +297,19 @@ def transform(self, T): # ========================================================================== def point_at(self, t): - """Construct a point at a specific location along the line. + """Construct a point along the line at a fractional position. Parameters ---------- t : float - The location along the line. + The relative position along the line as a fraction of the length of the line. + 0.0 corresponds to the start point and 1.0 corresponds to the end point. + Numbers outside of this range are also valid and correspond to points beyond the start and end point. Returns ------- :class:`compas.geometry.Point` - The point at the specified location. + The point at the specified position. See Also -------- @@ -323,6 +325,44 @@ def point_at(self, t): point = self.point + self.vector * t return point + def point_from_start(self, distance): + """Construct a point along the line at a distance from the start point. + + Parameters + ---------- + distance : float + The distance along the line from the start point towards the end point. + If the distance is negative, the point is constructed in the opposite direction of the end point. + If the distance is larger than the length of the line, the point is constructed beyond the end point. + + Returns + ------- + :class:`compas.geometry.Point` + The point at the specified distance. + + """ + point = self.point + self.direction * distance + return point + + def point_from_end(self, distance): + """Construct a point along the line at a distance from the end point. + + Parameters + ---------- + distance : float + The distance along the line from the end point towards the start point. + If the distance is negative, the point is constructed in the opposite direction of the start point. + If the distance is larger than the length of the line, the point is constructed beyond the start point. + + Returns + ------- + :class:`compas.geometry.Point` + The point at the specified distance. + + """ + point = self.end - self.direction * distance + return point + def closest_point(self, point, return_parameter=False): """Compute the closest point on the line to a given point. @@ -349,3 +389,43 @@ def closest_point(self, point, return_parameter=False): if return_parameter: return closest, t return closest + + def flip(self): + """Flip the direction of the line. + + Returns + ------- + None + + Examples + -------- + >>> line = Line([0, 0, 0], [1, 2, 3]) + >>> line + Line(Point(x=0.0, y=0.0, z=0.0), Point(x=1.0, y=2.0, z=3.0)) + >>> line.flip() + >>> line + Line(Point(x=1.0, y=2.0, z=3.0), Point(x=0.0, y=0.0, z=0.0)) + + """ + new_vector = self.vector.inverted() + self.start = self.end + self.vector = new_vector + + def flipped(self): + """Return a new line with the direction flipped. + + Returns + ------- + :class:`Line` + A new line. + + Examples + -------- + >>> line = Line([0, 0, 0], [1, 2, 3]) + >>> line + Line(Point(x=0.0, y=0.0, z=0.0), Point(x=1.0, y=2.0, z=3.0)) + >>> line.flipped() + Line(Point(x=1.0, y=2.0, z=3.0), Point(x=0.0, y=0.0, z=0.0)) + + """ + return Line(self.end, self.start) diff --git a/tests/compas/geometry/test_curves_line.py b/tests/compas/geometry/test_curves_line.py index 6fbd2209481..d049ec091a7 100644 --- a/tests/compas/geometry/test_curves_line.py +++ b/tests/compas/geometry/test_curves_line.py @@ -11,6 +11,7 @@ from compas.geometry import Vector from compas.geometry import Frame from compas.geometry import Line +from compas.tolerance import TOL @pytest.mark.parametrize( @@ -216,3 +217,68 @@ def test_line_accessors(p1, p2): # ============================================================================= # Other Methods # ============================================================================= + + +@pytest.mark.parametrize( + "p1,p2", + [ + ([0, 0, 0], [1, 0, 0]), + ([0, 0, 0], [1, 2, 3]), + ([1, 2, 3], [-1, -2, -3]), + ([-11.1, 22.2, 33.3], [1.1, -2.2, -3.3]), + ], +) +def test_line_point_from_start(p1, p2): + distances = [0, 1, 4, -9, 3.3, 0.00001, -0.00001] + for distance in distances: + line = Line(p1, p2) + point = line.point_from_start(distance) + distance_to_start = distance_point_point(point, p1) + distance_to_end = distance_point_point(point, p2) + # Check that the distance is correct + assert TOL.is_close(distance_to_start, abs(distance)) + # Check that negative distance gives a point far away from end + if distance < 0: + assert distance_to_end > line.length + + +@pytest.mark.parametrize( + "p1,p2", + [ + ([0, 0, 0], [1, 0, 0]), + ([0, 0, 0], [1, 2, 3]), + ([1, 2, 3], [-1, -2, -3]), + ([-11.1, 22.2, 33.3], [1.1, -2.2, -3.3]), + ], +) +def test_line_point_from_end(p1, p2): + distances = [0, 1, 4, -9, 3.3, 0.00001, -0.00001] + for distance in distances: + line = Line(p1, p2) + point = line.point_from_end(distance) + distance_to_start = distance_point_point(point, p1) + distance_to_end = distance_point_point(point, p2) + # Check that the distance is correct + assert TOL.is_close(distance_to_end, abs(distance)) + # Check that negative distance gives a point far away from start + if distance < 0: + assert distance_to_start > line.length + + +@pytest.mark.parametrize( + "p1,p2", + [ + ([0, 0, 0], [1, 0, 0]), + ([0, 0, 0], [1, 2, 3]), + ([1, 2, 3], [-1, -2, -3]), + ([-11.1, 22.2, 33.3], [1.1, -2.2, -3.3]), + ], +) +def test_line_flip(p1, p2): + line = Line(p1, p2) + line.flip() + assert TOL.is_zero(distance_point_point(line.start, p2)) + assert TOL.is_zero(distance_point_point(line.end, p1)) + flipped_line = Line(p1, p2).flipped() + assert TOL.is_zero(distance_point_point(flipped_line.start, p2)) + assert TOL.is_zero(distance_point_point(flipped_line.end, p1))