-
Notifications
You must be signed in to change notification settings - Fork 26
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
Jack cut feature #267
Jack cut feature #267
Changes from 13 commits
ab65d45
8555a7a
24a8a88
1b84518
5c919e6
26dcda1
d55f10b
ed02f46
ede5de3
3aceb89
09c1138
b9d1212
d01155c
aec2795
525ce79
6efaf47
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
# This module is a tepmorary solution to the problem of circular imports in the compas_timber package. | ||
# It will be reoved or merged with the `fabrication` module once the migration to the new feature system is complete. | ||
from .btlx_process import BTLxProcess | ||
from .btlx_process import OrientationType | ||
from .jack_cut import JackRafterCut | ||
|
||
|
||
__all__ = ["JackRafterCut", "BTLxProcess", "OrientationType"] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
from compas.data import Data | ||
|
||
|
||
class BTLxProcess(Data): | ||
"""Base class for BTLx processes. | ||
|
||
Attributes | ||
---------- | ||
ref_side_index : int | ||
The reference side, zero-based, index of the beam to be cut. 0-5 correspond to RS1-RS6. | ||
""" | ||
|
||
@property | ||
def __data__(self): | ||
return {"ref_side_index": self.ref_side_index} | ||
|
||
def __init__(self, ref_side_index): | ||
super(BTLxProcess, self).__init__() | ||
self.ref_side_index = ref_side_index | ||
|
||
|
||
class OrientationType(object): | ||
"""Enum for the orientation of the cut. | ||
|
||
Attributes | ||
---------- | ||
START : int | ||
The start of the beam is cut away. | ||
END : int | ||
The end of the beam is cut away. | ||
""" | ||
|
||
START = 0 | ||
END = 1 |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,274 @@ | ||||||
import math | ||||||
|
||||||
from compas.geometry import BrepTrimmingError | ||||||
from compas.geometry import Frame | ||||||
from compas.geometry import Line | ||||||
from compas.geometry import Plane | ||||||
from compas.geometry import Rotation | ||||||
from compas.geometry import Vector | ||||||
from compas.geometry import angle_vectors_signed | ||||||
from compas.geometry import distance_point_point | ||||||
from compas.geometry import intersection_line_plane | ||||||
from compas.geometry import is_point_behind_plane | ||||||
|
||||||
from compas_timber.elements import FeatureApplicationError | ||||||
|
||||||
from .btlx_process import BTLxProcess | ||||||
from .btlx_process import OrientationType | ||||||
|
||||||
|
||||||
class JackRafterCut(BTLxProcess): | ||||||
"""Represents a Jack Rafter Cut feature to be made on a beam. | ||||||
|
||||||
Parameters | ||||||
---------- | ||||||
orientation : int | ||||||
The orientation of the cut. Must be either OrientationType.START or OrientationType.END. | ||||||
start_x : float | ||||||
The start x-coordinate of the cut in parametric space of the reference side. -100000.0 < start_x < 100000.0. | ||||||
start_y : float | ||||||
The start y-coordinate of the cut in parametric space of the reference side. 0.0 < start_y < 50000.0. | ||||||
start_depth : float | ||||||
The start depth of the cut. 0.0 < start_depth < 50000.0. | ||||||
angle : float | ||||||
The horizontal angle of the cut. 0.1 < angle < 179.9. | ||||||
inclination : float | ||||||
The vertical angle of the cut. 0.1 < inclination < 179.9. | ||||||
|
||||||
""" | ||||||
|
||||||
@property | ||||||
def __data__(self): | ||||||
data = super(JackRafterCut, self).__data__ | ||||||
data["orientation"] = self.orientation | ||||||
data["start_x"] = self.start_x | ||||||
data["start_y"] = self.start_y | ||||||
data["start_depth"] = self.start_depth | ||||||
data["angle"] = self.angle | ||||||
data["inclination"] = self.inclination | ||||||
return data | ||||||
|
||||||
def __init__(self, orientation, start_x=0.0, start_y=0.0, start_depth=0.0, angle=90.0, inclination=90.0, **kwargs): | ||||||
super(JackRafterCut, self).__init__(**kwargs) | ||||||
self._orientation = None | ||||||
self._start_x = None | ||||||
self._start_y = None | ||||||
self._start_depth = None | ||||||
self._angle = None | ||||||
self._inclination = None | ||||||
|
||||||
self.orientation = orientation | ||||||
self.start_x = start_x | ||||||
self.start_y = start_y | ||||||
self.start_depth = start_depth | ||||||
self.angle = angle | ||||||
self.inclination = inclination | ||||||
|
||||||
######################################################################## | ||||||
# Properties | ||||||
######################################################################## | ||||||
|
||||||
@property | ||||||
def orientation(self): | ||||||
return self._orientation | ||||||
|
||||||
@orientation.setter | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Question: what is the benefit of using setters and private properties for this class? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this an object oriented thing that's strongly related to the encapsulation principle. In statically typed languages where private access is enforced you'd typically always need setters and getters. In python, since nothing is enforced by the language, it can get blurry. There's different reasons why you'd want to use properties in python. In this particular case, I used properties and their setters for input validation. Since BTLx has a clear specification for the allowed value ranges for each parameter it seemed like a good place to validate those. |
||||||
def orientation(self, orientation): | ||||||
if orientation not in [OrientationType.START, OrientationType.END]: | ||||||
raise ValueError("Orientation must be either OrientationType.START or OrientationType.END.") | ||||||
self._orientation = orientation | ||||||
|
||||||
@property | ||||||
def start_x(self): | ||||||
return self._start_x | ||||||
|
||||||
@start_x.setter | ||||||
def start_x(self, start_x): | ||||||
if start_x > 100000.0 or start_x < -100000.0: | ||||||
raise ValueError("Start X must be between -100000.0 and 100000.") | ||||||
self._start_x = start_x | ||||||
|
||||||
@property | ||||||
def start_y(self): | ||||||
return self._start_y | ||||||
|
||||||
@start_y.setter | ||||||
def start_y(self, start_y): | ||||||
if start_y > 50000.0: | ||||||
raise ValueError("Start Y must be less than 50000.0.") | ||||||
self._start_y = start_y | ||||||
|
||||||
@property | ||||||
def start_depth(self): | ||||||
return self._start_depth | ||||||
|
||||||
@start_depth.setter | ||||||
def start_depth(self, start_depth): | ||||||
if start_depth > 50000.0: | ||||||
raise ValueError("Start Depth must be less than 50000.0.") | ||||||
self._start_depth = start_depth | ||||||
|
||||||
@property | ||||||
def angle(self): | ||||||
return self._angle | ||||||
|
||||||
@angle.setter | ||||||
def angle(self, angle): | ||||||
if angle > 179.9 or angle < 0.1: | ||||||
raise ValueError("Angle must be between 0.1 and 179.9.") | ||||||
self._angle = angle | ||||||
|
||||||
@property | ||||||
def inclination(self): | ||||||
return self._inclination | ||||||
|
||||||
@inclination.setter | ||||||
def inclination(self, inclination): | ||||||
if inclination > 179.9 or inclination < 0.1: | ||||||
raise ValueError("Inclination must be between 0.1 and 179.9.") | ||||||
self._inclination = inclination | ||||||
|
||||||
######################################################################## | ||||||
# Alternative constructors | ||||||
######################################################################## | ||||||
|
||||||
@classmethod | ||||||
def from_plane_and_beam(cls, plane, beam, ref_side_index=0): | ||||||
"""Create a JackRafterCut instance from a cutting plane and the beam it should cut. | ||||||
|
||||||
Parameters | ||||||
---------- | ||||||
plane : :class:`~compas.geometry.Plane` or :class:`~compas.geometry.Frame` | ||||||
The cutting plane. | ||||||
beam : :class:`~compas_timber.elements.Beam` | ||||||
The beam that is cut by this instance. | ||||||
ref_side_index : int, optional | ||||||
The reference side index of the beam to be cut. Default is 0 (i.e. RS1). | ||||||
|
||||||
Returns | ||||||
------- | ||||||
:class:`~compas_timber.fabrication.JackRafterCut` | ||||||
|
||||||
""" | ||||||
# type: (Plane | Frame, Beam, int) -> JackRafterCut | ||||||
if isinstance(plane, Frame): | ||||||
plane = Plane.from_frame(plane) | ||||||
start_y = 0.0 | ||||||
start_depth = 0.0 | ||||||
ref_side = beam.ref_sides[ref_side_index] # TODO: is this arbitrary? | ||||||
ref_edge = Line.from_point_and_vector(ref_side.point, ref_side.xaxis) | ||||||
orientation = cls._calculate_orientation(ref_side, plane) | ||||||
|
||||||
point_start_x = intersection_line_plane(ref_edge, plane) | ||||||
if point_start_x is None: | ||||||
raise ValueError("Plane does not intersect with beam.") | ||||||
|
||||||
start_x = distance_point_point(ref_edge.point, point_start_x) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why? :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ref_edge is a compas Line, no? in this case shouldn't it be ref_edge.start? I thought it was a typo or smth ;p |
||||||
angle = cls._calculate_angle(ref_side, plane, orientation) | ||||||
inclination = cls._calculate_inclination(ref_side, plane, orientation) | ||||||
return cls(orientation, start_x, start_y, start_depth, angle, inclination, ref_side_index=ref_side_index) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should the ref_side_index be in this class constructor, since it is not a parameter of the class? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||||
|
||||||
@staticmethod | ||||||
def _calculate_orientation(ref_side, cutting_plane): | ||||||
# orientation is START if cutting plane normal points towards the start of the beam and END otherwise | ||||||
# essentially if the start is being cut or the end | ||||||
if is_point_behind_plane(ref_side.point, cutting_plane): | ||||||
return OrientationType.END | ||||||
else: | ||||||
return OrientationType.START | ||||||
|
||||||
@staticmethod | ||||||
def _calculate_angle(ref_side, plane, orientation): | ||||||
# vector rotation direction of the plane's normal in the vertical direction | ||||||
angle_vector = Vector.cross(ref_side.zaxis, plane.normal) | ||||||
angle = angle_vectors_signed(ref_side.xaxis, angle_vector, ref_side.zaxis, deg=True) | ||||||
if orientation == OrientationType.START: | ||||||
return 180 - abs(angle) # get the other side of the angle | ||||||
else: | ||||||
return abs(angle) | ||||||
|
||||||
@staticmethod | ||||||
def _calculate_inclination(ref_side, plane, orientation): | ||||||
# vector rotation direction of the plane's normal in the horizontal direction | ||||||
inclination_vector = Vector.cross(ref_side.yaxis, plane.normal) | ||||||
inclination = angle_vectors_signed(ref_side.xaxis, inclination_vector, ref_side.yaxis, deg=True) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was a problem with the equivalent drilling module because it was returning values out of range. you could consider using the |
||||||
if orientation == OrientationType.START: | ||||||
return 180 - abs(inclination) # get the other side of the angle | ||||||
else: | ||||||
return abs(inclination) | ||||||
|
||||||
######################################################################## | ||||||
# Methods | ||||||
######################################################################## | ||||||
|
||||||
def apply(self, geometry, beam): | ||||||
"""Apply the feature to the beam geometry. | ||||||
|
||||||
Parameters | ||||||
---------- | ||||||
geometry : :class:`~compas.geometry.Brep` | ||||||
The beam geometry to be cut. | ||||||
beam : :class:`compas_timber.elements.Beam` | ||||||
The beam that is cut by this instance. | ||||||
|
||||||
Raises | ||||||
------ | ||||||
:class:`~compas_timber.elements.FeatureApplicationError` | ||||||
If the cutting plane does not intersect with beam geometry. | ||||||
|
||||||
Returns | ||||||
------- | ||||||
:class:`~compas.geometry.Brep` | ||||||
The resulting geometry after processing | ||||||
|
||||||
""" | ||||||
# type: (Brep, Beam) -> Brep | ||||||
cutting_plane = self.plane_from_params_and_beam(beam) | ||||||
try: | ||||||
return geometry.trimmed(cutting_plane) | ||||||
except BrepTrimmingError: | ||||||
raise FeatureApplicationError( | ||||||
cutting_plane, | ||||||
geometry, | ||||||
"The cutting plane does not intersect with beam geometry.", | ||||||
) | ||||||
|
||||||
def plane_from_params_and_beam(self, beam): | ||||||
"""Calculates the cutting plane from the machining parameters in this instance and the given beam | ||||||
|
||||||
Parameters | ||||||
---------- | ||||||
beam : :class:`compas_timber.elements.Beam` | ||||||
The beam that is cut by this instance. | ||||||
|
||||||
Returns | ||||||
------- | ||||||
:class:`compas.geometry.Plane` | ||||||
The cutting plane. | ||||||
|
||||||
""" | ||||||
# type: (Beam) -> Plane | ||||||
assert self.angle is not None | ||||||
assert self.inclination is not None | ||||||
|
||||||
# start with a plane aligned with the ref side but shifted to the start_x of the cut | ||||||
ref_side = beam.side_as_surface(self.ref_side_index) | ||||||
p_origin = ref_side.point_at(self.start_x, 0.0) | ||||||
cutting_plane = Frame(p_origin, ref_side.frame.xaxis, ref_side.frame.yaxis) | ||||||
|
||||||
# normal pointing towards xaxis so just need the delta | ||||||
horizontal_angle = math.radians(self.angle - 90) | ||||||
rot_a = Rotation.from_axis_and_angle(cutting_plane.zaxis, horizontal_angle, point=p_origin) | ||||||
|
||||||
# normal pointing towards xaxis so just need the delta | ||||||
vertical_angle = math.radians(self.inclination - 90) | ||||||
rot_b = Rotation.from_axis_and_angle(cutting_plane.yaxis, vertical_angle, point=p_origin) | ||||||
|
||||||
cutting_plane.transform(rot_a * rot_b) | ||||||
# for simplicity, we always start with normal pointing towards xaxis. | ||||||
# if start is cut, we need to flip the normal | ||||||
if self.orientation == OrientationType.END: | ||||||
plane_normal = cutting_plane.xaxis | ||||||
else: | ||||||
plane_normal = -cutting_plane.xaxis | ||||||
return Plane(cutting_plane.point, plane_normal) |
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. With these changes does the l_mitter factory module gets removed? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yup. that's the intention. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Where does the serialization of the btlx process parameters happen? shouldn't there be a class method for that? Also does this mean that header attributes are being removed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right, that's missing. I introduced this in one of the last commits, might have not pushed yet.
I'm still very much debating though. I'd like to extract the XML schema specific stuff out of these classes, but they are nevertheless related. In the meantime, I created a
JackRafterCutParams
class whose sole responsibility is to populate the parameter values fromJackRafterCut
in they appropriate key-value pair according to the specification.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it's in the latest commit aec2795