Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Line class added #309

Merged
merged 7 commits into from
Jul 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
257 changes: 257 additions & 0 deletions blueprints/geometry/line.py
Original file line number Diff line number Diff line change
@@ -1 +1,258 @@
"""Line module."""

from enum import Enum
from typing import Literal

import numpy as np
from shapely import Point
from typing_extensions import Self

from blueprints.geometry.operations import CoordinateSystemOptions, calculate_rotation_angle
from blueprints.type_alias import DEG
from blueprints.unit_conversion import RAD_TO_DEG


class Reference(Enum):
"""Enum of the reference options start/end."""

START = 0
END = 1


class Line:
"""Represents a line in a 3D modelling space.

Parameters
----------
start_point : Point
Starting point
end_point : Point
End point
"""

id: int = 0

def __init__(self, start_point: Point, end_point: Point) -> None:
"""Initialize the line."""
self._start_point = start_point
self._end_point = end_point
self._validate_points()
Line.id += 1

@property
def start_point(self) -> Point:
"""Return the start point."""
return self._start_point

@start_point.setter
def start_point(self, value: Point) -> None:
"""Set the start point."""
self._start_point = value
self._validate_points()

@property
def end_point(self) -> Point:
"""Return the end point."""
return self._end_point

@end_point.setter
def end_point(self, value: Point) -> None:
"""Set the end point."""
self._end_point = value
self._validate_points()

@property
def _start(self) -> np.ndarray:
"""Return the start point as a numpy array."""
return np.array(self._start_point.coords)

@property
def _end(self) -> np.ndarray:
"""Return the end point as a numpy array."""
return np.array(self._end_point.coords)

def _validate_points(self) -> None:
"""Validate if the points are different."""
# Check if start and end point are the same
if list(self.start_point.coords) == list(self.end_point.coords):
msg = f"Start and end point can't be equal. {self.start_point=} | {self.end_point=}"
raise ValueError(msg)

# if points have no z value, then declare zero as default
if not self.start_point.has_z:
self.start_point = Point(self.start_point.x, self.start_point.y, 0.0)
if not self.end_point.has_z:
self.end_point = Point(self.end_point.x, self.end_point.y, 0.0)

@property
def midpoint(self) -> Point:
"""Midpoint of the line."""
return Point((self._start + self._end) / 2)

@property
def delta_x(self) -> float:
"""Difference in X-coordinate between starting and end point (X end - X start)."""
return self.end_point.x - self.start_point.x

@property
def delta_y(self) -> float:
"""Difference in Y-coordinate between starting and end point (Y end - Y start)."""
return self.end_point.y - self.start_point.y

@property
def delta_z(self) -> float:
"""Difference in Z-coordinate between starting and end point (Z end - Z start)."""
return self.end_point.z - self.start_point.z

@property
def length(self) -> float:
"""Return the total length of the line."""
return float(np.linalg.norm(self._end - self._start))

def angle(self, coordinate_system: CoordinateSystemOptions = CoordinateSystemOptions.XY) -> DEG:
"""
Calculates rotation of the end point in relation to the start point in a given plane/coordinate system [deg].

- The rotation is calculated in the given plane in relation to the start point.
- The rotation is calculated in the range [0, 2*pi].
- The rotation is calculated in the counter-clockwise direction.

Parameters
----------
coordinate_system: CoordinateSystemOptions
Desired plane that will be used as reference to calculate the rotation. Standard is XY-plane.

Returns
-------
DEG
rotation of end point relative to the start point in degrees in the given plane [deg].
"""
return (
calculate_rotation_angle(
start_point=self.start_point,
end_point=self.end_point,
coordinate_system=coordinate_system,
)
* RAD_TO_DEG
)

@property
def unit_vector(self) -> np.ndarray:
"""Return the unit vector of the line."""
return (self._end - self._start) / self.length

def get_internal_point(self, distance: float, reference: Literal["start", "end"] = "start") -> Point:
"""Return an internal point within the line in a given distance from the reference point.

Parameters
----------
distance : float
Distance from the given reference point following the axis of the line
reference: Literal["start", "end"]
Reference point in the line where given distance is declared. Default -> "start"

Returns
-------
Point
Internal point within the line in a given distance from the reference point.

Raises
------
ValueError
If the distance is greater than the total length of the line.
If the distance is a negative number.
"""
if distance > self.length:
msg = f"Distance must be equal or less than total length of the line. Length={self.length} | Distance={distance}"
raise ValueError(msg)
if distance < 0:
msg = "Given Distance must be a positive number."
raise ValueError(msg)

match reference.lower():
case "start":
internal_point = self._start + distance * self.unit_vector
case "end":
internal_point = self._end - distance * self.unit_vector
case _:
msg = f"'{reference}' is an invalid input for 'reference_point', use 'start' or 'end'."
raise ValueError(msg)
return Point(internal_point)

def adjust_length(self, distance: float, direction: Literal["start", "end"] = "end") -> Self:
"""Extends or shortens the line in a given direction. The end of the line is the default direction.

Parameters
----------
distance : float
Distance to extend or shorten the line. Positive number extends the line, negative number shortens the line.
direction: Literal["start", "end"]
Given direction where the line needs to be extended. Default towards the end of the line.

Returns
-------
Line
The new line with the adjusted length.
"""
if distance < 0 and abs(distance) >= self.length:
raise ValueError("When shortening the line, the absolute value of the extra length must be less than the total length of the line.")

match direction.lower():
case "end":
new_point = self._end + distance * self.unit_vector
self.end_point = Point(new_point)
case "start":
new_point = self._start - distance * self.unit_vector
self.start_point = Point(new_point)
case _:
msg = "Invalid input for 'direction', use 'start' or 'end'."
raise ValueError(msg)
return self

def get_evenly_spaced_points(self, n: int = 2) -> list[Point]:
"""Return a list of evenly spaced internal points of the line from start to end point with an n number of desired points.

Parameters
----------
n : int
Total number of internal points desired. A minimum of 2 points are required.
"""
if not isinstance(n, int):
msg = "n must be an integer"
raise TypeError(msg)
if n < 2:
msg = "n must be equal or greater than 2"
raise ValueError(msg)

# Create a list of evenly spaced points
evenly_spaced_points = np.linspace(start=0, stop=self.length, num=n, endpoint=True)

return [Point(self._start + distance * self.unit_vector) for distance in evenly_spaced_points]

def divide_into_n_lines(self, n: int = 2) -> list["Line"]:
"""Return a list of evenly divided lines.

Parameters
----------
n : int
Total number of lines desired. A minimum of 2 lines are required.
"""
if not isinstance(n, int):
msg = "n must be an integer"
raise TypeError(msg)
if n < 2:
msg = "n must be equal or greater than 2"
raise ValueError(msg)

evenly_spaced_points = self.get_evenly_spaced_points(n + 1)
return [Line(start_point=point_1, end_point=point_2) for point_1, point_2 in zip(evenly_spaced_points[:-1], evenly_spaced_points[1:])]

def __eq__(self, other: object) -> bool:
"""Return True if the lines are equal."""
if not isinstance(other, Line):
raise NotImplementedError("Line can only be compared to other Line object")
return np.allclose(self._start, other._start) and np.allclose(self._end, other._end)

def __repr__(self) -> str:
"""Return the representation of the line."""
return f"Line({self.start_point}, {self.end_point})"
2 changes: 1 addition & 1 deletion blueprints/geometry/operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def calculate_rotation_angle(
If start_point or end_point do not have z value when rotation angle in XZ or YZ plane is requested.
"""
if list(start_point.coords) == list(end_point.coords):
msg = f"Start and end point can't be equal. start={start_point} | end={end_point}"
msg = f"Start and end point can't be equal. {start_point=} | {end_point=}"
raise ValueError(msg)

if coordinate_system != CoordinateSystemOptions.XY and (not start_point.has_z or not end_point.has_z):
Expand Down
Loading