diff --git a/CHANGELOG.md b/CHANGELOG.md index ced541b0e..03af2bd20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,7 +24,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Added new `compas_timber._fabrication.DrillingParams`. * Added new `compas_timber._fabrication.StepJoint`. * Added new `compas_timber._fabrication.StepJointNotch`. +* Added new `compas_timber._fabrication.DovetailTenon`. +* Added new `compas_timber._fabrication.DovetailMortise`. * Added new `compas_timber.connections.TStepJoint`. +* Added new `compas_timber.connections.TDovetailJoint`. * Added new `utilities` module in `connections` package. ### Changed diff --git a/src/compas_timber/_fabrication/__init__.py b/src/compas_timber/_fabrication/__init__.py index e70a600d1..9c220ca73 100644 --- a/src/compas_timber/_fabrication/__init__.py +++ b/src/compas_timber/_fabrication/__init__.py @@ -10,6 +10,10 @@ from .step_joint_notch import StepJointNotchParams from .step_joint import StepJoint from .step_joint import StepJointParams +from .dovetail_tenon import DovetailTenon +from .dovetail_tenon import DovetailTenonParams +from .dovetail_mortise import DovetailMortise +from .dovetail_mortise import DovetailMortiseParams __all__ = [ @@ -23,4 +27,8 @@ "StepJointNotchParams", "StepJoint", "StepJointParams", + "DovetailTenon", + "DovetailTenonParams", + "DovetailMortise", + "DovetailMortiseParams", ] diff --git a/src/compas_timber/_fabrication/btlx_process.py b/src/compas_timber/_fabrication/btlx_process.py index 0bb929743..f84b6d0a6 100644 --- a/src/compas_timber/_fabrication/btlx_process.py +++ b/src/compas_timber/_fabrication/btlx_process.py @@ -106,3 +106,43 @@ class StepShapeType(object): HEEL = "heel" TAPERED_HEEL = "taperedheel" DOUBLE = "double" + + +class TenonShapeType(object): + """Enum for the tenon shape of the cut. + + Attributes + ---------- + STEP : literal("step") + A step shape. + HEEL : literal("heel") + A heel shape. + TAPERED_HEEL : literal("taperedheel") + A tapered heel shape. + DOUBLE : literal("double") + A double shape. + """ + + AUTOMATIC = "automatic" + SQUARE = "square" + ROUND = "round" + ROUNDED = "rounded" + RADIUS = "radius" + + +class LimitationTopType(object): + """Enum for the top limitation of the cut. + + Attributes + ---------- + LIMITED : literal("limited") + Limitation to the cut. + UNLIMITED : literal("unlimited") + No limit to the cut. + POCKET : literal("pocket") + Pocket like limitation to the cut. + """ + + LIMITED = "limited" + UNLIMITED = "unlimited" + POCKET = "pocket" diff --git a/src/compas_timber/_fabrication/dovetail_mortise.py b/src/compas_timber/_fabrication/dovetail_mortise.py new file mode 100644 index 000000000..2af846122 --- /dev/null +++ b/src/compas_timber/_fabrication/dovetail_mortise.py @@ -0,0 +1,727 @@ +import math + +from compas.geometry import Box +from compas.geometry import Brep +from compas.geometry import Frame +from compas.geometry import Line +from compas.geometry import PlanarSurface +from compas.geometry import Plane +from compas.geometry import Rotation +from compas.geometry import distance_point_point +from compas.geometry import intersection_line_plane +from compas.geometry import is_point_behind_plane +from compas.tolerance import TOL + +from compas_timber.elements import FeatureApplicationError + +from .btlx_process import BTLxProcess +from .btlx_process import BTLxProcessParams +from .btlx_process import LimitationTopType +from .btlx_process import OrientationType +from .btlx_process import TenonShapeType + + +class DovetailMortise(BTLxProcess): + """Represents a Dovetail Mortise feature to be made on a beam. + + Parameters + ---------- + start_x : float + The start x-coordinate of the cut in parametric space of the reference side. Distance from the beam start to the reference point. -100000.0 < start_x < 100000.0. + start_y : float + The start y-coordinate of the cut in parametric space of the reference side. Distance from the reference edge to the reference point. -5000.0 < start_y < 5000.0. + start_depth : float + The start depth of the cut in parametric space of the reference side. Margin on the reference side. 0.0 < start_depth < 5000.0. + angle : float + The angle of the cut. Angle between edge and reference edge. -180.0 < angle < 180.0. + slope : float + The slope of the cut. Angle between axis along the length of the mortise and rederence side. 0.1 < slope < 179.9. + inclination : float + The inclination of the cut. Angle between axis along the width of the mortise and rederence side. 0.1 < inclination < 179.9. + limitation_top : str + The limitation type of the top length of the cut. Should be either 'limited', 'unlimited', or 'pocket'. + length_limited_bottom : bool + Whether the bottom length of the cut is limited. True or False. + length : float + The length of the cut. 0.0 < length < 5000.0. + width : float + The width of the cut. 0.0 < width < 1000.0. + depth : float + The depth of the mortise. 0.0 < depth < 1000.0. + cone_angle : float + The cone angle of the cut. 0.0 < cone_angle < 30.0. + use_flank_angle : bool + Whether the flank angle is used. True or False. + flank_angle : float + The flank angle of the cut. Angle of the tool. 5.0 < flank_angle < 35.0. + shape : str + The shape of the cut. Must be either 'automatic', 'square', 'round', 'rounded', or 'radius'. + shape_radius : float + The radius of the shape of the cut. 0.0 < shape_radius < 1000.0. + + """ + + PROCESS_NAME = "DovetailMortise" # type: ignore + + # Class-level attribute + _dovetail_tool_params = {} + + @property + def __data__(self): + data = super(DovetailMortise, self).__data__ + data["start_x"] = self.start_x + data["start_y"] = self.start_y + data["start_depth"] = self.start_depth + data["angle"] = self.angle + data["slope"] = self.slope + data["inclination"] = self.inclination + data["limitation_top"] = self.limitation_top + data["length_limited_bottom"] = self.length_limited_bottom + data["length"] = self.length + data["width"] = self.width + data["depth"] = self.depth + data["cone_angle"] = self.cone_angle + data["use_flank_angle"] = self.use_flank_angle + data["flank_angle"] = self.flank_angle + data["shape"] = self.shape + data["shape_radius"] = self.shape_radius + return data + + def __init__( + self, + start_x=0.0, + start_y=50.0, + start_depth=0.0, + angle=0.0, + slope=90.0, + inclination=90.0, + limitation_top=LimitationTopType.LIMITED, + length_limited_bottom=True, + length=80.0, + width=40.0, + depth=28.0, + cone_angle=15.0, + use_flank_angle=False, + flank_angle=15.0, + shape=TenonShapeType.AUTOMATIC, + shape_radius=20.0, + **kwargs + ): + super(DovetailMortise, self).__init__(**kwargs) + self._start_x = None + self._start_y = None + self._start_depth = None + self._angle = None + self._slope = None + self._inclination = None + self._limitation_top = None + self._length_limited_bottom = None + self._length = None + self._width = None + self._depth = None + self._cone_angle = None + self._use_flank_angle = None + self._flank_angle = None + self._shape = None + self._shape_radius = None + + self.start_x = start_x + self.start_y = start_y + self.start_depth = start_depth + self.angle = angle + self.slope = slope + self.inclination = inclination + self.limitation_top = limitation_top + self.length_limited_bottom = length_limited_bottom + self.length = length + self.width = width + self.depth = depth + self.cone_angle = cone_angle + self.use_flank_angle = use_flank_angle + self.flank_angle = flank_angle + self.shape = shape + self.shape_radius = shape_radius + + ######################################################################## + # Properties + ######################################################################## + + @property + def params_dict(self): + return DovetailMortiseParams(self).as_dict() + + @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("StartX must be between -100000.0 and 100000.0") + 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 > 5000.0 or start_y < -5000.0: + raise ValueError("StartY must be between -5000.0 and 5000.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 > 5000.0 or start_depth < 0.0: + raise ValueError("StartDepth must be between 0.0 and 5000.0") + self._start_depth = start_depth + + @property + def angle(self): + return self._angle + + @angle.setter + def angle(self, angle): + if angle > 180.0 or angle < -180.0: + raise ValueError("Angle must be between -180.0 and 180.0.") + self._angle = angle + + @property + def slope(self): + return self._slope + + @slope.setter + def slope(self, slope): + if slope > 179.9 or slope < 0.1: + raise ValueError("Slope must be between 0.1 and 179.9.") + self._slope = slope + + @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 + + @property + def limitation_top(self): + return self._limitation_top + + @limitation_top.setter + def limitation_top(self, limitation_top): + if limitation_top not in [LimitationTopType.LIMITED, LimitationTopType.UNLIMITED, LimitationTopType.POCKET]: + raise ValueError("LimitationTop must be either limited, unlimited or pocket.") + self._limitation_top = limitation_top + + @property + def length_limited_bottom(self): + return self._length_limited_bottom + + @length_limited_bottom.setter + def length_limited_bottom(self, length_limited_bottom): + if not isinstance(length_limited_bottom, bool): + raise ValueError("LengthLimitedBottom must be either True or False.") + self._length_limited_bottom = length_limited_bottom + + @property + def length(self): + return self._length + + @length.setter + def length(self, length): + if length > 5000.0 or length < 0.0: + raise ValueError("Length must be between 0.0 and 5000.0") + self._length = length + + @property + def width(self): + return self._width + + @width.setter + def width(self, width): + if width > 1000.0 or width < 0.0: + raise ValueError("Width must be between 0.0 and 1000.0") + self._width = width + + @property + def depth(self): + return self._height + + @depth.setter + def depth(self, depth): + if depth > 1000.0 or depth < 0.0: + raise ValueError("depth must be between 0.0 and 1000.0") + self._height = depth + + @property + def cone_angle(self): + return self._cone_angle + + @cone_angle.setter + def cone_angle(self, cone_angle): + if cone_angle > 30.0 or cone_angle < 0.0: + raise ValueError("ConeAngle must be between 0.0 and 30.0.") + self._cone_angle = cone_angle + + @property + def use_flank_angle(self): + return self._use_flank_angle + + @use_flank_angle.setter + def use_flank_angle(self, use_flank_angle): + if not isinstance(use_flank_angle, bool): + raise ValueError("UseFlankAngle must be either True or False.") + self._use_flank_angle = use_flank_angle + + @property + def flank_angle(self): + return self._flank_angle + + @flank_angle.setter + def flank_angle(self, flank_angle): + if flank_angle > 35.0 or flank_angle < 5.0: + raise ValueError("FlankAngle must be between 5.0 and 35.0.") + self._flank_angle = flank_angle + + @property + def shape(self): + return self._shape + + @shape.setter + def shape(self, shape): + if shape not in [ + TenonShapeType.AUTOMATIC, + TenonShapeType.SQUARE, + TenonShapeType.ROUND, + TenonShapeType.ROUNDED, + TenonShapeType.RADIUS, + ]: + raise ValueError("Shape must be either 'automatic', 'square', 'round', 'rounded', or 'radius'.") + self._shape = shape + + @property + def shape_radius(self): + return self._shape_radius + + @shape_radius.setter + def shape_radius(self, shape_radius): + if shape_radius > 1000.0 or shape_radius < 0.0: + raise ValueError("ShapeRadius must be between 0.0 and 1000.0") + self._shape_radius = shape_radius + + ######################################################################## + # Alternative constructors + ######################################################################## + + @classmethod + def from_frame_and_beam( + cls, + frame, + beam, + start_depth=0.0, + angle=0.0, + length=80.0, + width=40.0, + depth=28.0, + cone_angle=10.0, + flank_angle=15.0, + shape=TenonShapeType.AUTOMATIC, + shape_radius=20.0, + ref_side_index=0, + ): + """Create a DovetailMortise instance from a cutting surface and the beam it should cut. This could be the ref_side of the cross beam of a Joint and the cross beam. + + Parameters + ---------- + frame : :class:`~compas.geometry.Frame` or :class:`~compas.geometry.Plane` + The cutting frame. + beam : :class:`~compas_timber.elements.Beam` + The beam that is cut by this instance. + start_depth : float, optional + The start depth of the cut along the y-axis of the beam. This offset is to be used in case of housing. Default is 0.0. + angle : float, optional + The angle of the cut. + length : float, optional + The length of the mortise. + width : float, optional + The width of the mortise. + depth : float, optional + The depth of the mortise. The equivalent value of the DovetailTenon BTLxProcess is the height. + cone_angle : float, optional + The cone angle of the dovetail mortise. + flank_angle : float, optional + The flank angle of the dovetail mortise. + shape : str, optional + The shape of the dovetail mortise in regards to it's edges. Default is 'automatic'. + shape_radius : float, optional + The radius of the shape of the dovetail mortise. Default is 20.0. + 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.DovetailMortise` + + """ + # type: (Frame|Plane, Beam, float, float, float, float, float, float, float, float, float, float, int) -> DovetailMortise + + if isinstance(frame, Plane): + frame = Frame.from_plane(frame) + + # define ref_side & ref_edge + ref_side = beam.ref_sides[ref_side_index] + ref_edge = Line.from_point_and_vector(ref_side.point, ref_side.xaxis) + + # calculate orientation + orientation = cls._calculate_orientation(ref_side, frame) + + # calclulate start_x and start_y + start_x = cls._calculate_start_x(ref_side, ref_edge, frame) + start_y = abs(frame.point[2] - ref_side.point[2]) + + # define angle + if orientation == OrientationType.START: + angle -= 90.0 + else: + angle += 90.0 + + # define slope and inclination + # TODO: In which cases do you want indiferent slope and inclination? + slope = 90.0 + inclination = 90.0 + + # determine if the top and bottom length of the cut is limited + limitation_top = LimitationTopType.UNLIMITED + length_limited_bottom = True + + use_flank_angle = True if flank_angle != 15.0 else False # TODO: does this change anything? + + return cls( + start_x, + start_y, + start_depth, + angle, + slope, + inclination, + limitation_top, + length_limited_bottom, + length, + width, + depth, + cone_angle, + use_flank_angle, + flank_angle, + shape, + shape_radius, + ref_side_index=ref_side_index, + ) + + @staticmethod + def _calculate_orientation(ref_side, cutting_frame): + # calculate the orientation of the beam by comparing the xaxis's direction of the ref_side and the plane. + # Orientation is not set as a param for the BTLxDovetailMortise processing but its essential for the definition of the rest of the params. + perp_plane = Plane(cutting_frame.point, cutting_frame.xaxis) + if is_point_behind_plane(ref_side.point, perp_plane): + return OrientationType.END + else: + return OrientationType.START + + @staticmethod + def _calculate_start_x(ref_side, ref_edge, cutting_frame): + # calculate the start_x of the cut based on the ref_side, ref_edge and cutting_frame + perp_plane = Plane(cutting_frame.point, ref_side.xaxis) + point_start_x = intersection_line_plane(ref_edge, perp_plane) + if point_start_x is None: + raise ValueError("Plane does not intersect with beam.") + + start_x = distance_point_point(ref_side.point, point_start_x) + return start_x + + ######################################################################## + # Class Methods + ######################################################################## + + @classmethod + def define_dovetail_tool(self, tool_angle, tool_diameter, tool_height): + """Define the parameters for the dovetail feature based on a defined dovetail cutting tool. + + Parameters + ---------- + tool_angle : float + The angle of the dovetail cutter tool. + tool_diameter : float + The diameter of the dovetail cutter tool. + tool_height : float + The height of the dovetail cutter tool. + + """ + # type: (float, float, float) -> None + self._dovetail_tool_params = { + "tool_angle": tool_angle, + "tool_diameter": tool_diameter, + "tool_height": tool_height, + } + + ######################################################################## + # Methods + ######################################################################## + + def apply(self, geometry, beam): + """Apply the feature to the beam geometry. + + Parameters + ---------- + geometry : :class:`compas.geometry.Brep` + The geometry to be processed. + + beam : :class:`compas_timber.elements.Beam` + The beam that is milled by this instance. + + Raises + ------ + :class:`~compas_timber.elements.FeatureApplicationError` + If the cutting planes do not create a volume that itersects with beam geometry or any step fails. + + Returns + ------- + :class:`~compas.geometry.Brep` + The resulting geometry after processing + + """ + # type: (Brep, Beam) -> Brep + + # get dovetail volume from params and beam + try: + dovetail_volume = self.dovetail_volume_from_params_and_beam(beam) + except ValueError as e: + raise FeatureApplicationError( + None, geometry, "Failed to generate dovetail mortise volume from parameters and beam: {}".format(str(e)) + ) + + # fillet the edges of the dovetail volume based on the shape + if ( + self.shape != TenonShapeType.SQUARE and not self.length_limited_bottom + ): # TODO: Change negation to affirmation once Brep.fillet is implemented + edge_ideces = [4, 7] if self.length_limited_bottom else [5, 8] + try: + dovetail_volume.fillet( + self.shape_radius, [dovetail_volume.edges[edge_ideces[0]], dovetail_volume.edges[edge_ideces[1]]] + ) # TODO: NotImplementedError + except Exception as e: + raise FeatureApplicationError( + dovetail_volume, + geometry, + "Failed to fillet the edges of the dovetail volume based on the shape: {}".format(str(e)), + ) + + # remove tenon volume to geometry + try: + geometry -= dovetail_volume + except Exception as e: + raise FeatureApplicationError( + dovetail_volume, geometry, "Failed to add tenon volume to geometry: {}".format(str(e)) + ) + + return geometry + + def frame_from_params_and_beam(self, beam): + """Calculates the cutting frame 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.Frame` + The cutting frame. + + """ + # type: (Beam) -> Frame + 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, self.start_y) + cutting_frame = Frame(p_origin, ref_side.frame.xaxis, ref_side.frame.yaxis) + + # rotate the cutting frame based on the angle + rotation = Rotation.from_axis_and_angle( + cutting_frame.normal, math.radians(self.angle + 90), cutting_frame.point + ) + cutting_frame.transform(rotation) + + return cutting_frame + + def dovetail_cutting_planes_from_params_and_beam(self, beam): + """Calculates the cutting planes for the dovetail mortise 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 + ------- + list of :class:`compas.geometry.Frame` + The cutting planes for the dovetail mortise. + """ + assert self.angle is not None + assert self.depth is not None + assert self.start_depth is not None + + # start with a plane aligned with the ref side but shifted to the start_x of the cut + cutting_frame = self.frame_from_params_and_beam(beam) + + offseted_cutting_frame = Frame(cutting_frame.point, -cutting_frame.xaxis, cutting_frame.yaxis) + offseted_cutting_frame.point += cutting_frame.normal * self.depth + + cutting_surface = PlanarSurface( + xsize=beam.height, + ysize=beam.width, + frame=cutting_frame, + ) + + # offset the cutting surface in case of housing + cutting_surface.translate(-cutting_frame.zaxis * self.start_depth) + + # calculate the displacement of the edge points of the dovetail from the top-center + dx_top = self.width / 2 + self.length * abs(math.tan(math.radians(self.cone_angle))) + dx_bottom = self.width / 2 + dy = self.length + + dovetail_profile_points = [ + cutting_surface.point_at(-dx_top, 0), + cutting_surface.point_at(dx_top, 0), + cutting_surface.point_at(dx_bottom, -dy), + cutting_surface.point_at(-dx_bottom, -dy), + ] + + dovetail_edges = [ + Line(dovetail_profile_points[0], dovetail_profile_points[1]), # Top line + Line(dovetail_profile_points[1], dovetail_profile_points[2]), # Right line + Line(dovetail_profile_points[2], dovetail_profile_points[3]), # Bottom line + Line(dovetail_profile_points[3], dovetail_profile_points[0]), # Left line + ] + + trimming_frames = [] + for i, edge in enumerate(dovetail_edges): + # create the initial frame using the line's direction and the cutting frame's normal + frame = Frame(edge.midpoint, edge.direction, cutting_frame.normal) + + if i != 0: + # determine the rotation direction: right and bottom are positive, top and left are negative + # apply the rotation based on the flank angle + rotation = Rotation.from_axis_and_angle(edge.direction, math.radians(self.flank_angle), frame.point) + frame.transform(rotation) + + trimming_frames.append(frame) + + # translate the top trimming frame to the top of the beam if the top is unlimited + if self.limitation_top == LimitationTopType.UNLIMITED: + trimming_frames[0].translate(cutting_frame.yaxis * (beam.height - TOL.relative)) + + cutting_frame.xaxis = -cutting_frame.xaxis + trimming_frames.append(cutting_frame) + return trimming_frames + + def dovetail_volume_from_params_and_beam(self, beam): + """Calculates the dovetail mortise volume 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.Brep` + The mortise volume. + + """ + # type: (Beam) -> Brep + + assert self.inclination is not None + assert self.slope is not None + assert self.depth is not None + + cutting_frame = self.frame_from_params_and_beam(beam) + + # create the dovetail volume by trimming a box # TODO: PluginNotInstalledError for Brep.from_loft + # get the box as a brep + dovetail_volume = Brep.from_box( + Box( + (beam.width + (beam.width * math.sin(math.radians(self.inclination)))) * 2, + (beam.height / math.sin(math.radians(self.slope))) * 2, + self.depth * 2, + cutting_frame, + ) + ) + + # get the cutting planes for the dovetail tenon + trimming_frames = self.dovetail_cutting_planes_from_params_and_beam(beam) + + # trim the box to create the dovetail volume + for frame in trimming_frames: + try: + frame.xaxis = -frame.xaxis + dovetail_volume.trim(frame) + except Exception as e: + raise FeatureApplicationError( + frame, dovetail_volume, "Failed to trim tenon volume with cutting plane: {}".format(str(e)) + ) + + return dovetail_volume + + +class DovetailMortiseParams(BTLxProcessParams): + """A class to store the parameters of a Dovetail Mortise feature. + + Parameters + ---------- + instance : :class:`~compas_timber._fabrication.DovetailMortise` + The instance of the Dovetail Mortise feature. + """ + + def __init__(self, instance): + # type: (DovetailMortise) -> None + super(DovetailMortiseParams, self).__init__(instance) + + def as_dict(self): + """Returns the parameters of the Dovetail Mortise feature as a dictionary. + + Returns + ------- + dict + The parameters of the Dovetail Mortise as a dictionary. + """ + # type: () -> OrderedDict + + result = super(DovetailMortiseParams, self).as_dict() + result["StartX"] = "{:.{prec}f}".format(self._instance.start_x, prec=TOL.precision) + result["StartY"] = "{:.{prec}f}".format(self._instance.start_y, prec=TOL.precision) + result["StartDepth"] = "{:.{prec}f}".format(self._instance.start_depth, prec=TOL.precision) + result["Angle"] = "{:.{prec}f}".format(self._instance.angle, prec=TOL.precision) + result["Slope"] = "{:.{prec}f}".format(self._instance.slope, prec=TOL.precision) + # result["Inclination"] = "{:.{prec}f}".format(self._instance.inclination, prec=TOL.precision) + #! Inclination is a parameter according to the documentation but gives an error in BTL Viewer. + result["LimitationTop"] = self._instance.limitation_top + result["LengthLimitedBottom"] = "yes" if self._instance.length_limited_bottom else "no" + result["Length"] = "{:.{prec}f}".format(self._instance.length, prec=TOL.precision) + result["Width"] = "{:.{prec}f}".format(self._instance.width, prec=TOL.precision) + result["Depth"] = "{:.{prec}f}".format(self._instance.depth, prec=TOL.precision) + result["ConeAngle"] = "{:.{prec}f}".format(self._instance.cone_angle, prec=TOL.precision) + result["UseFlankAngle"] = "yes" if self._instance.use_flank_angle else "no" + result["FlankAngle"] = "{:.{prec}f}".format(self._instance.flank_angle, prec=TOL.precision) + result["Shape"] = self._instance.shape + result["ShapeRadius"] = "{:.{prec}f}".format(self._instance.shape_radius, prec=TOL.precision) + return result diff --git a/src/compas_timber/_fabrication/dovetail_tenon.py b/src/compas_timber/_fabrication/dovetail_tenon.py new file mode 100644 index 000000000..eaafd7a65 --- /dev/null +++ b/src/compas_timber/_fabrication/dovetail_tenon.py @@ -0,0 +1,872 @@ +import math + +from compas.geometry import Box +from compas.geometry import Brep +from compas.geometry import Frame +from compas.geometry import Line +from compas.geometry import PlanarSurface +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.tolerance import TOL + +from compas_timber.elements import FeatureApplicationError + +from .btlx_process import BTLxProcess +from .btlx_process import BTLxProcessParams +from .btlx_process import OrientationType +from .btlx_process import TenonShapeType + + +class DovetailTenon(BTLxProcess): + """Represents a Dovetail Tenon 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. Distance from the beam start to the reference point. -100000.0 < start_x < 100000.0. + start_y : float + The start y-coordinate of the cut in parametric space of the reference side. Distance from the reference edge to the reference point. -5000.0 < start_y < 5000.0. + start_depth : float + The start depth of the cut in parametric space of the reference side. Margin on the reference side. -5000.0 < start_depth < 5000.0. + angle : float + The angle of the cut. Angle between edge and reference edge. 0.1 < angle < 179.9. + inclination : float + The inclination of the cut. Inclination between face and reference side. 0.1 < inclination < 179.9. + rotation : float + The rotation of the cut. Angle between axis of the tenon and rederence side. 0.1 < rotation < 179.9. + length_limited_top : bool + Whether the top length of the cut is limited. True or False. + length_limited_bottom : bool + Whether the bottom length of the cut is limited. True or False. + length : float + The length of the cut. 0.0 < length < 5000.0. + width : float + The width of the cut. 0.0 < width < 1000.0. + height : float + The height of the tenon. 0.0 < height < 1000.0. + cone_angle : float + The cone angle of the cut. 0.0 < cone_angle < 30.0. + use_flank_angle : bool + Whether the flank angle is used. True or False. + flank_angle : float + The flank angle of the cut. Angle of the tool. 5.0 < flank_angle < 35.0. + shape : str + The shape of the cut. Must be either 'automatic', 'square', 'round', 'rounded', or 'radius'. + shape_radius : float + The radius of the shape of the cut. 0.0 < shape_radius < 1000.0. + + """ + + PROCESS_NAME = "DovetailTenon" # type: ignore + + # Class-level attribute + _dovetail_tool_params = {} + + @property + def __data__(self): + data = super(DovetailTenon, 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 + data["rotation"] = self.rotation + data["length_limited_top"] = self.length_limited_top + data["length_limited_bottom"] = self.length_limited_bottom + data["length"] = self.length + data["width"] = self.width + data["height"] = self.height + data["cone_angle"] = self.cone_angle + data["use_flank_angle"] = self.use_flank_angle + data["flank_angle"] = self.flank_angle + data["shape"] = self.shape + data["shape_radius"] = self.shape_radius + return data + + def __init__( + self, + orientation, + start_x=0.0, + start_y=50.0, + start_depth=50.0, + angle=90.0, + inclination=90.0, + rotation=90.0, + length_limited_top=True, + length_limited_bottom=True, + length=80.0, + width=40.0, + height=28.0, + cone_angle=15.0, + use_flank_angle=False, + flank_angle=15.0, + shape=TenonShapeType.AUTOMATIC, + shape_radius=20.0, + **kwargs + ): + super(DovetailTenon, 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._rotation = None + self._length_limited_top = None + self._length_limited_bottom = None + self._length = None + self._width = None + self._height = None + self._cone_angle = None + self._use_flank_angle = None + self._flank_angle = None + self._shape = None + self._shape_radius = 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 + self.rotation = rotation + self.length_limited_top = length_limited_top + self.length_limited_bottom = length_limited_bottom + self.length = length + self.width = width + self.height = height + self.cone_angle = cone_angle + self.use_flank_angle = use_flank_angle + self.flank_angle = flank_angle + self.shape = shape + self.shape_radius = shape_radius + + ######################################################################## + # Properties + ######################################################################## + + @property + def params_dict(self): + return DovetailTenonParams(self).as_dict() + + @property + def orientation(self): + return self._orientation + + @orientation.setter + 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("StartX must be between -100000.0 and 100000.0.") + 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 > 5000.0 or start_y < -5000.0: + raise ValueError("StartY must be between -5000.0 and 5000.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 > 5000.0 or start_depth < -5000.0: + raise ValueError("StartDepth must be between -5000.0 and 5000.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 + + @property + def rotation(self): + return self._rotation + + @rotation.setter + def rotation(self, rotation): + if rotation > 179.9 or rotation < 0.1: + raise ValueError("Rotation must be between 0.1 and 179.9.") + self._rotation = rotation + + @property + def length_limited_top(self): + return self._length_limited_top + + @length_limited_top.setter + def length_limited_top(self, length_limited_top): + if not isinstance(length_limited_top, bool): + raise ValueError("LengthLimitedTop must be either True or False.") + self._length_limited_top = length_limited_top + + @property + def length_limited_bottom(self): + return self._length_limited_bottom + + @length_limited_bottom.setter + def length_limited_bottom(self, length_limited_bottom): + if not isinstance(length_limited_bottom, bool): + raise ValueError("LengthLimitedBottom must be either True or False.") + self._length_limited_bottom = length_limited_bottom + + @property + def length(self): + return self._length + + @length.setter + def length(self, length): + if length > 5000.0 or length < 0.0: + raise ValueError("Length must be between 0.0 and 5000.0.") + self._length = length + + @property + def width(self): + return self._width + + @width.setter + def width(self, width): + if width > 1000.0 or width < 0.0: + raise ValueError("Width must be between 0.0 and 1000.0.") + self._width = width + + @property + def height(self): + return self._height + + @height.setter + def height(self, height): + if height > 1000.0 or height < 0.0: + raise ValueError("Height must be between 0.0 and 1000.0.") + self._height = height + + @property + def cone_angle(self): + return self._cone_angle + + @cone_angle.setter + def cone_angle(self, cone_angle): + if cone_angle > 30.0 or cone_angle < 0.0: + raise ValueError("ConeAngle must be between 0.0 and 30.0.") + self._cone_angle = cone_angle + + @property + def use_flank_angle(self): + return self._use_flank_angle + + @use_flank_angle.setter + def use_flank_angle(self, use_flank_angle): + if not isinstance(use_flank_angle, bool): + raise ValueError("UseFlankAngle must be either True or False.") + self._use_flank_angle = use_flank_angle + + @property + def flank_angle(self): + return self._flank_angle + + @flank_angle.setter + def flank_angle(self, flank_angle): + if flank_angle > 35.0 or flank_angle < 5.0: + raise ValueError("FlankAngle must be between 5.0 and 35.0.") + self._flank_angle = flank_angle + + @property + def shape(self): + return self._shape + + @shape.setter + def shape(self, shape): + if shape not in [ + TenonShapeType.AUTOMATIC, + TenonShapeType.SQUARE, + TenonShapeType.ROUND, + TenonShapeType.ROUNDED, + TenonShapeType.RADIUS, + ]: + raise ValueError("Shape must be either 'automatic', 'square', 'round', 'rounded', or 'radius'.") + self._shape = shape + + @property + def shape_radius(self): + return self._shape_radius + + @shape_radius.setter + def shape_radius(self, shape_radius): + if shape_radius > 1000.0 or shape_radius < 0.0: + raise ValueError("ShapeRadius must be between 0.0 and 1000.") + self._shape_radius = shape_radius + + ######################################################################## + # Alternative constructors + ######################################################################## + + @classmethod + def from_plane_and_beam( + cls, + plane, + beam, + start_y=0.0, + start_depth=50.0, + rotation=0.0, + length=80.0, + width=40.0, + height=28.0, + cone_angle=10.0, + flank_angle=15.0, + shape=TenonShapeType.AUTOMATIC, + shape_radius=20.0, + ref_side_index=0, + ): + """Create a DovetailTenon instance from a cutting surface and the beam it should cut. This could be the ref_side of the cross beam of a Joint and the main beam. + + 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. + start_y : float, optional + The start y-coordinate of the cut in parametric space of the reference side. Default is 0.0. + start_depth : float, optional + The start depth of the tenon, which is an offset along the normal of the reference side. Default is 50.0. + rotation : float, optional + The angle of rotation of the tenon. Default is 0.0. + length : float, optional + The length of the tenon. Default is 80.0. + width : float, optional + The width of the bottom edge of the tenon. Default is 40.0. + height : float, optional + The height of the tenon. Related to the dovetail tool and can be defined using the `DovetailTenon.define_dovetail_tool()` method. Default is 28.0. + cone_angle : float, optional + The angle of the cone of the tenon. Default is 10.0. + flank_angle : float, optional + The angle of the flank of the tenon. Related to the dovetail tool and can be defined using the `DovetailTenon.define_dovetail_tool()` method. Default is 15.0. + shape : str, optional + The shape of the tenon. Default is 'automatic'. + shape_radius : float, optional + The radius of the shape of the tenon. Related to the dovetail tool and can be defined using the `DovetailTenon.define_dovetail_tool()` method. Default is 20.0. + 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.DovetailTenon` + + """ + # type: (Plane|Frame, Beam, float, float, bool, int) -> DovetailTenon + + if cls._dovetail_tool_params: + # get the tool parameters + tool_angle = cls._dovetail_tool_params["tool_angle"] + tool_diameter = cls._dovetail_tool_params["tool_diameter"] + tool_height = cls._dovetail_tool_params["tool_height"] + tool_top_radius = tool_diameter / 2 - tool_height * (math.tan(math.radians(tool_angle))) + # update parameters related to the tool if a tool is defined + height = min(height, tool_height) + flank_angle = tool_angle + shape_radius = tool_top_radius + + # find the difference of the bottom and top radius of the frustum cone + frustum_difference = height * math.tan(math.radians(flank_angle)) + + if isinstance(plane, Frame): + plane = Plane.from_frame(plane) + # define ref_side & ref_edge + ref_side = beam.ref_sides[ref_side_index] + ref_edge = Line.from_point_and_vector(ref_side.point, ref_side.xaxis) + + # calculate orientation + orientation = cls._calculate_orientation(ref_side, plane) + + # calculate angle + angle = cls._calculate_angle(ref_side, plane) + + # calculate inclination + inclination = cls._calculate_inclination(ref_side, plane, orientation, angle) + + # calculate start_y & rotation + if orientation == OrientationType.END: + rotation = -rotation + start_y = -start_y + start_y += beam.width / 2 # TODO: Should this be bound as well? + rotation += 90 + + # bound start_depth, length and width + start_depth = cls._bound_start_depth(start_depth, inclination, height) + length = cls._bound_length( + ref_side, plane, beam.height, start_depth, inclination, length, height, frustum_difference + ) + width = cls._bound_width(beam.width, angle, length, width, cone_angle, shape_radius, frustum_difference) + + # calculate start_x + start_x = cls._calculate_start_x( + ref_side, + ref_edge, + plane, + orientation, + start_y, + start_depth, + angle, + ) + + # determine if the top and bottom length of the cut is limited + length_limited_top, length_limited_bottom = cls._calculate_length_limits( + beam, start_depth, length, inclination + ) # TODO: Should this instead come first and override the start_depth and length? + + use_flank_angle = True if flank_angle != 15.0 else False # TODO: does this change anything? + + return cls( + orientation, + start_x, + start_y, + start_depth, + angle, + inclination, + rotation, + length_limited_top, + length_limited_bottom, + length, + width, + height, + cone_angle, + use_flank_angle, + flank_angle, + shape, + shape_radius, + ref_side_index=ref_side_index, + ) + + @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.START + else: + return OrientationType.END + + @staticmethod + def _calculate_start_x(ref_side, ref_edge, plane, orientation, start_y, start_depth, angle): + # calculate the start_x of the cut based on the ref_side, ref_edge, plane, start_y and angle + plane.translate(ref_side.normal * start_depth) + 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_side.point, point_start_x) + # count for start_depth and start_y in the start_x + if orientation == OrientationType.END: + start_x -= start_y / math.tan(math.radians(angle)) + else: + start_x += start_y / math.tan(math.radians(angle)) + return start_x + + @staticmethod + def _calculate_angle(ref_side, plane): + # 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) + return abs(angle) + + @staticmethod + def _calculate_inclination(ref_side, plane, orientation, angle): + # calculate the inclination between the ref_side and the plane + if orientation == OrientationType.END: + angle = 180 - angle + rotation = Rotation.from_axis_and_angle(ref_side.normal, math.radians(angle)) + rotated_axis = ref_side.xaxis.copy() + rotated_axis.transform(rotation) + + cross_plane = Vector.cross(rotated_axis, plane.normal) + cross_ref_side = Vector.cross(rotated_axis, ref_side.normal) + + inclination = angle_vectors_signed(cross_ref_side, cross_plane, rotated_axis, deg=True) + return abs(inclination) + + @staticmethod + def _calculate_length_limits(beam, start_depth, length, inclination): + # determine if the top and bottom length of the cut is limited + length_limited_top = start_depth > 0.0 + length_limited_bottom = length < ((beam.height) / math.sin(math.radians(inclination)) - start_depth) + + # necessary override, otherwise tenon would go out of the blank + if inclination > 90.0: + length_limited_bottom = True + elif inclination < 90.0: + length_limited_top = True + return length_limited_top, length_limited_bottom + + @staticmethod + def _bound_length(ref_side, plane, beam_height, start_depth, inclination, length, height, frustum_difference): + # bound the inserted length value to the maximum possible length for the beam based on the inclination so that the tenon does not go out of the blank + max_length = (beam_height) / (math.sin(math.radians(inclination))) - start_depth + + # define the inclination angle regardless of the orientation start or end + inclination_vector = Vector.cross(ref_side.yaxis, plane.normal) + origin_inclination = math.degrees(ref_side.xaxis.angle(inclination_vector)) + if origin_inclination < 90.0: + max_length = max_length - (frustum_difference + height / abs(math.tan(math.radians(inclination)))) + return min(max_length, length) + + @staticmethod + def _bound_width(beam_width, angle, length, width, cone_angle, shape_radius, frustum_difference): + # bound the inserted width value to the minumum(based on the dovetail tool radius) and maximum(so that the tenon does not go out of the blank) possible width for the beam + max_width = beam_width / math.sin(math.radians(angle)) - 2 * ( + frustum_difference + length * math.tan(math.radians(cone_angle)) + ) + min_width = 2 * shape_radius + if width < min_width: + width = min_width + elif width > max_width: + width = max_width + return width + + @staticmethod + def _bound_start_depth(start_depth, inclination, height): + # bound the start_depth value to the minimum possible start_depth if the incliantion is larger than 90 so that the tenon does not go out of the blank + min_start_depth = height / (math.tan(math.radians(180 - inclination))) + return max(start_depth, min_start_depth) + + ######################################################################## + # Class Methods + ######################################################################## + + @classmethod + def define_dovetail_tool(self, tool_angle, tool_diameter, tool_height): + """Define the parameters for the dovetail feature based on a defined dovetail cutting tool. + + Parameters + ---------- + tool_angle : float + The angle of the dovetail cutter tool. + tool_diameter : float + The diameter of the dovetail cutter tool. + tool_height : float + The height of the dovetail cutter tool. + + """ + # type: (float, float, float) -> None + self._dovetail_tool_params = { + "tool_angle": tool_angle, + "tool_diameter": tool_diameter, + "tool_height": tool_height, + } + + ######################################################################## + # Methods + ######################################################################## + + def apply(self, geometry, beam): + """Apply the feature to the beam geometry. + + Parameters + ---------- + geometry : :class:`compas.geometry.Brep` + The geometry to be processed. + + beam : :class:`compas_timber.elements.Beam` + The beam that is milled by this instance. + + Raises + ------ + :class:`~compas_timber.elements.FeatureApplicationError` + If the cutting planes do not create a volume that itersects with beam geometry or any step fails. + + Returns + ------- + :class:`~compas.geometry.Brep` + The resulting geometry after processing + + """ + # type: (Brep, Beam) -> Brep + + # get cutting plane from params and beam + try: + cutting_plane = Plane.from_frame(self.frame_from_params_and_beam(beam)) + except ValueError as e: + raise FeatureApplicationError( + None, geometry, "Failed to generate cutting plane from parameters and beam: {}".format(str(e)) + ) + + # get dovetail volume from params and beam + try: + dovetail_volume = self.dovetail_volume_from_params_and_beam(beam) + except ValueError as e: + raise FeatureApplicationError( + None, geometry, "Failed to generate dovetail tenon volume from parameters and beam: {}".format(str(e)) + ) + + # fillet the edges of the dovetail volume based on the shape + if ( + self.shape != TenonShapeType.SQUARE and not self.length_limited_bottom + ): # TODO: Change negation to affirmation once Brep.fillet is implemented + edge_ideces = [4, 7] if self.length_limited_top else [5, 8] + try: + dovetail_volume.fillet( + self.shape_radius, [dovetail_volume.edges[edge_ideces[0]], dovetail_volume.edges[edge_ideces[1]]] + ) # TODO: NotImplementedError + except Exception as e: + raise FeatureApplicationError( + dovetail_volume, + geometry, + "Failed to fillet the edges of the dovetail volume based on the shape: {}".format(str(e)), + ) + + # trim geometry with cutting planes + try: + geometry.trim(cutting_plane) + except Exception as e: + raise FeatureApplicationError( + cutting_plane, geometry, "Failed to trim geometry with cutting plane: {}".format(str(e)) + ) + + # add tenon volume to geometry + try: + geometry += dovetail_volume + except Exception as e: + raise FeatureApplicationError( + dovetail_volume, geometry, "Failed to add tenon volume to geometry: {}".format(str(e)) + ) + + return geometry + + def frame_from_params_and_beam(self, beam): + """ + Calculates the cutting frame 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.Frame` + The cutting frame. + """ + assert self.angle is not None + assert self.inclination is not None + assert self.start_depth is not None + + # get the reference side surface of the beam + ref_side = beam.side_as_surface(self.ref_side_index) + + # move the reference side surface to the start depth + ref_side.translate(-ref_side.frame.normal * self.start_depth) + + # convert angles to radians + inclination_radians = math.radians(self.inclination) + angle_radians = math.radians(self.angle + 90) + + # calculate the point of origin based on orientation + p_origin = ref_side.point_at(self.start_x, self.start_y) + if self.orientation == OrientationType.END: + yaxis = ref_side.frame.yaxis + else: + yaxis = -ref_side.frame.yaxis + inclination_radians += math.pi + + # create the initial cutting plane + cutting_frame = Frame(p_origin, -ref_side.frame.xaxis, yaxis) + + # apply rotations to the cutting plane based on angle and inclination parameters + rot_a = Rotation.from_axis_and_angle(cutting_frame.zaxis, angle_radians, point=p_origin) + rot_b = Rotation.from_axis_and_angle(cutting_frame.yaxis, inclination_radians, point=p_origin) + cutting_frame.transform(rot_a * rot_b) + + # for simplicity align normal towards x-axis + cutting_frame = Frame(cutting_frame.point, -cutting_frame.yaxis, cutting_frame.xaxis) + + # apply rotation based on the rotation parameter + rot_angle = math.radians(self.rotation - 90) + if self.orientation == OrientationType.START: + rot_angle = -rot_angle + rotation = Rotation.from_axis_and_angle(cutting_frame.normal, rot_angle, cutting_frame.point) + cutting_frame.transform(rotation) + + return cutting_frame + + def dovetail_cutting_planes_from_params_and_beam(self, beam): + """Calculates the cutting planes for the dovetail tenon from the machining parameters in this instance and the given beam.""" + + # get the cutting frame + cutting_frame = self.frame_from_params_and_beam(beam) + + # offset the cutting frame to create the cutting surface based on the height of the tenon. It needs to face in the opposite direction. + offseted_cutting_frame = Frame(cutting_frame.point, cutting_frame.xaxis, cutting_frame.yaxis) + offseted_cutting_frame.point += cutting_frame.normal * self.height + + cutting_surface = PlanarSurface( + xsize=beam.height / math.sin(math.radians(self.inclination)), + ysize=beam.width / math.sin(math.radians(self.angle)), + frame=cutting_frame, + ) + # move the cutting surface to the center + cutting_surface.translate(-cutting_frame.xaxis * self.start_y) + + dx_top = self.width / 2 + self.length * abs(math.tan(math.radians(self.cone_angle))) + dx_bottom = self.width / 2 + dy = -self.length + + bottom_dovetail_points = [ + cutting_surface.point_at(self.start_y - dx_top, 0), + cutting_surface.point_at(self.start_y + dx_top, 0), + cutting_surface.point_at(self.start_y + dx_bottom, dy), + cutting_surface.point_at(self.start_y - dx_bottom, dy), + ] + + dovetail_edges = [ + Line(bottom_dovetail_points[0], bottom_dovetail_points[1]), # Top line + Line(bottom_dovetail_points[1], bottom_dovetail_points[2]), # Right line + Line(bottom_dovetail_points[2], bottom_dovetail_points[3]), # Bottom line + Line(bottom_dovetail_points[3], bottom_dovetail_points[0]), # Left line + ] + + trimming_frames = [] + for i, edge in enumerate(dovetail_edges): + # create the initial frame using the line's direction and the cutting frame's normal + frame = Frame(edge.midpoint, -edge.direction, cutting_frame.normal) + + if i != 0: + # determine the rotation direction: right and bottom are positive, top and left are negative + # apply the rotation based on the flank angle + rotation = Rotation.from_axis_and_angle(-edge.direction, math.radians(self.flank_angle), frame.point) + frame.transform(rotation) + + trimming_frames.append(frame) + + cutting_frame.xaxis = -cutting_frame.xaxis + trimming_frames.extend([cutting_frame, offseted_cutting_frame]) + return trimming_frames + + def dovetail_volume_from_params_and_beam(self, beam): + """Calculates the dovetail tenon volume 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.Brep` + The tenon volume. + + """ + # type: (Beam) -> Brep + + assert self.inclination is not None + assert self.rotation is not None + assert self.height is not None + assert self.flank_angle is not None + assert self.shape is not None + assert self.shape_radius is not None + assert self.length_limited_top is not None + assert self.length_limited_bottom is not None + + cutting_frame = self.frame_from_params_and_beam(beam) + + # create the dovetail volume by trimming a box # TODO: PluginNotInstalledError for Brep.from_loft + # get the box as a brep + dovetail_volume = Brep.from_box( + Box( + (beam.width + (beam.width * math.sin(math.radians(self.rotation)))) * 2, + (beam.height / math.sin(math.radians(self.inclination))) * 2, + self.height * 2, + cutting_frame, + ) + ) + + # get the cutting planes for the dovetail tenon + trimming_frames = self.dovetail_cutting_planes_from_params_and_beam(beam) + + # trim the box to create the dovetail volume + for frame in trimming_frames: + try: + dovetail_volume.trim(frame) + except Exception as e: + raise FeatureApplicationError( + frame, dovetail_volume, "Failed to trim tenon volume with cutting plane: {}".format(str(e)) + ) + + return dovetail_volume + + +class DovetailTenonParams(BTLxProcessParams): + """A class to store the parameters of a Dovetail Tenon feature. + + Parameters + ---------- + instance : :class:`~compas_timber._fabrication.DovetailTenon` + The instance of the Dovetail Tenon feature. + """ + + def __init__(self, instance): + # type: (DovetailTenon) -> None + super(DovetailTenonParams, self).__init__(instance) + + def as_dict(self): + """Returns the parameters of the Dovetail Tenon feature as a dictionary. + + Returns + ------- + dict + The parameters of the Dovetail Tenon as a dictionary. + """ + # type: () -> OrderedDict + result = super(DovetailTenonParams, self).as_dict() + result["Orientation"] = self._instance.orientation + result["StartX"] = "{:.{prec}f}".format(self._instance.start_x, prec=TOL.precision) + result["StartY"] = "{:.{prec}f}".format(self._instance.start_y, prec=TOL.precision) + result["StartDepth"] = "{:.{prec}f}".format(self._instance.start_depth, prec=TOL.precision) + result["Angle"] = "{:.{prec}f}".format(self._instance.angle, prec=TOL.precision) + result["Inclination"] = "{:.{prec}f}".format(self._instance.inclination, prec=TOL.precision) + result["Rotation"] = "{:.{prec}f}".format(self._instance.rotation, prec=TOL.precision) + result["LengthLimitedTop"] = "yes" if self._instance.length_limited_top else "no" + result["LengthLimitedBottom"] = "yes" if self._instance.length_limited_bottom else "no" + result["Length"] = "{:.{prec}f}".format(self._instance.length, prec=TOL.precision) + result["Width"] = "{:.{prec}f}".format(self._instance.width, prec=TOL.precision) + result["Height"] = "{:.{prec}f}".format(self._instance.height, prec=TOL.precision) + result["ConeAngle"] = "{:.{prec}f}".format(self._instance.cone_angle, prec=TOL.precision) + result["UseFlankAngle"] = "yes" if self._instance.use_flank_angle else "no" + result["FlankAngle"] = "{:.{prec}f}".format(self._instance.flank_angle, prec=TOL.precision) + result["Shape"] = self._instance.shape + result["ShapeRadius"] = "{:.{prec}f}".format(self._instance.shape_radius, prec=TOL.precision) + return result diff --git a/src/compas_timber/connections/__init__.py b/src/compas_timber/connections/__init__.py index 8fd4b43bb..ec5e674ed 100644 --- a/src/compas_timber/connections/__init__.py +++ b/src/compas_timber/connections/__init__.py @@ -14,6 +14,7 @@ from .t_step_joint import TStepJoint from .t_halflap import THalfLapJoint from .x_halflap import XHalfLapJoint +from .t_dovetail import TDovetailJoint __all__ = [ "Joint", @@ -33,4 +34,5 @@ "JointTopology", "ConnectionSolver", "find_neighboring_beams", + "TDovetailJoint", ] diff --git a/src/compas_timber/connections/t_dovetail.py b/src/compas_timber/connections/t_dovetail.py new file mode 100644 index 000000000..48eaa14b7 --- /dev/null +++ b/src/compas_timber/connections/t_dovetail.py @@ -0,0 +1,283 @@ +import math + +from compas_timber._fabrication import DovetailMortise +from compas_timber._fabrication import DovetailTenon +from compas_timber._fabrication.btlx_process import TenonShapeType +from compas_timber.connections.utilities import beam_ref_side_incidence +from compas_timber.connections.utilities import beam_ref_side_incidence_with_vector + +from .joint import BeamJoinningError +from .joint import Joint +from .solver import JointTopology + + +class TDovetailJoint(Joint): + """ + Represents a T-Dovetail type joint which joins two beams, one of them at its end (main) and the other one along its centerline (cross). + A dovetail cut is made on the main beam, and a corresponding notch is made on the cross beam to fit the main beam. + + This joint type is compatible with beams in T topology. + + Please use `TDovetailJoint.create()` to properly create an instance of this class and associate it with a model. + + Parameters + ---------- + main_beam : :class:`~compas_timber.parts.Beam` + First beam to be joined. + cross_beam : :class:`~compas_timber.parts.Beam` + Second beam to be joined. + start_y : float + Start position of the dovetail cut along the y-axis of the main beam. + start_depth : float + Depth of the dovetail cut from the surface of the main beam. + rotation : float + Rotation of the dovetail cut around the main beam's axis. + length : float + Length of the dovetail cut along the main beam. + width : float + Width of the dovetail cut. + cone_angle : float + The angle of the dovetail cut, determining the taper of the joint. + dovetail_shape : int + The shape of the dovetail cut, represented by an integer index: 0: AUTOMATIC, 1: SQUARE, 2: ROUND, 3: ROUNDED, 4: RADIUS. + tool_angle : float + The angle of the tool used to create the dovetail cut. + tool_diameter : float + The diameter of the tool used to create the dovetail cut. + tool_height : float + The height of the tool used to create the dovetail cut. + + Attributes + ---------- + main_beam : :class:`~compas_timber.parts.Beam` + First beam to be joined. + cross_beam : :class:`~compas_timber.parts.Beam` + Second beam to be joined. + start_y : float + Start position of the dovetail cut along the y-axis of the main beam. + start_depth : float + Depth of the dovetail cut from the surface of the main beam. + rotation : float + Rotation of the dovetail cut around the main beam's axis. + length : float + Length of the dovetail cut along the main beam. + width : float + Width of the dovetail cut. + cone_angle : float + The angle of the dovetail cut, determining the taper of the joint. + dovetail_shape : int + The shape of the dovetail cut, represented by an integer index. + tool_angle : float + The angle of the tool used to create the dovetail cut. + tool_diameter : float + The diameter of the tool used to create the dovetail cut. + tool_height : float + The height of the tool used to create the dovetail cut. + height : float, optional + The height of the joint. This is not set during initialization but can be defined later. + flank_angle : float, optional + The angle of the flanks of the dovetail joint, if applicable. + shape_radius : float, optional + The radius used to define the shape of the joint, if applicable. + features : list + List of features or machining processes applied to the joint. + """ + + SUPPORTED_TOPOLOGY = JointTopology.TOPO_T + + @property + def __data__(self): + data = super(TDovetailJoint, self).__data__ + data["main_beam"] = self.main_beam_guid + data["cross_beam"] = self.cross_beam_guid + data["start_y"] = self.start_y + data["start_depth"] = self.start_depth + data["rotation"] = self.rotation + data["length"] = self.length + data["width"] = self.width + data["cone_angle"] = self.cone_angle + data["dovetail_shape"] = self.dovetail_shape + data["tool_angle"] = self.tool_angle + data["tool_diameter"] = self.tool_diameter + data["tool_height"] = self.tool_height + return data + + def __init__( + self, + main_beam, + cross_beam, + start_y=None, + start_depth=None, + rotation=None, + length=None, + width=None, + cone_angle=None, + dovetail_shape=None, + tool_angle=None, + tool_diameter=None, + tool_height=None, + ): + super(TDovetailJoint, self).__init__() + self.main_beam = main_beam + self.cross_beam = cross_beam + self.main_beam_guid = str(main_beam.guid) if main_beam else None + self.cross_beam_guid = str(cross_beam.guid) if cross_beam else None + + # Default values if not provided + self.start_y = start_y if start_y is not None else 0.0 + self.start_depth = start_depth if start_depth is not None else 0.0 + self.rotation = rotation if rotation is not None else 0.0 + self.length = length if length is not None else 60.0 + self.width = width if width is not None else 25.0 + self.cone_angle = cone_angle if cone_angle is not None else 10.0 + self.dovetail_shape = dovetail_shape if dovetail_shape is not None else 4 # shape: RADIUS + + self.tool_angle = tool_angle if tool_angle is not None else 15.0 + self.tool_diameter = tool_diameter if tool_diameter is not None else 60.0 + self.tool_height = tool_height if tool_height is not None else 28.0 + + self.height = None + self.flank_angle = None + self.shape_radius = None + + self.features = [] + + @property + def beams(self): + return [self.main_beam, self.cross_beam] + + @property + def cross_beam_ref_side_index(self): + ref_side_dict = beam_ref_side_incidence(self.main_beam, self.cross_beam, ignore_ends=True) + ref_side_index = min(ref_side_dict, key=ref_side_dict.get) + return ref_side_index + + @property + def main_beam_ref_side_index(self): + cross_ref_side_normal = self.cross_beam.ref_sides[self.cross_beam_ref_side_index].normal + ref_side_dict = beam_ref_side_incidence_with_vector(self.main_beam, cross_ref_side_normal, ignore_ends=True) + ref_side_index = min(ref_side_dict, key=ref_side_dict.get) + return ref_side_index + + @property + def shape(self): + if self.dovetail_shape == 0: + shape_type = TenonShapeType.AUTOMATIC + elif self.dovetail_shape == 1: + shape_type = TenonShapeType.SQUARE + elif self.dovetail_shape == 2: + shape_type = TenonShapeType.ROUND + elif self.dovetail_shape == 3: + shape_type = TenonShapeType.ROUNDED + elif self.dovetail_shape == 4: + shape_type = TenonShapeType.RADIUS + else: + raise ValueError("Invalid tenon shape index. Please provide a valid index between 0 and 4.") + return shape_type + + def add_extensions(self): + """Calculates and adds the necessary extensions to the beams. + + This method is automatically called when joint is created by the call to `Joint.create()`. + + Raises + ------ + BeamJoinningError + If the extension could not be calculated. + + """ + assert self.main_beam and self.cross_beam + try: + cutting_plane = self.cross_beam.ref_sides[self.cross_beam_ref_side_index] + cutting_plane.translate(-cutting_plane.normal * self.tool_height) + start_main, end_main = self.main_beam.extension_to_plane(cutting_plane) + except AttributeError as ae: + raise BeamJoinningError(beams=self.beams, joint=self, debug_info=str(ae), debug_geometries=[cutting_plane]) + except Exception as ex: + raise BeamJoinningError(beams=self.beams, joint=self, debug_info=str(ex)) + extension_tolerance = 0.01 # TODO: this should be proportional to the unit used + self.main_beam.add_blank_extension( + start_main + extension_tolerance, + end_main + extension_tolerance, + self.guid, + ) + + def add_features(self): + """Adds the required trimming features to both beams. + + This method is automatically called when joint is created by the call to `Joint.create()`. + + """ + assert self.main_beam and self.cross_beam # should never happen + + if self.features: + self.main_beam.remove_features(self.features) + self.cross_beam.remove_features(self.features) + + # define the tool parameters + self.define_dovetail_tool(self.tool_angle, self.tool_diameter, self.tool_height) + + # generate dovetail tenon features + main_feature = DovetailTenon.from_plane_and_beam( + plane=self.cross_beam.ref_sides[self.cross_beam_ref_side_index], + beam=self.main_beam, + start_y=self.start_y, + start_depth=self.start_depth, + rotation=self.rotation, + length=self.length, + width=self.width, + height=self.height, + cone_angle=self.cone_angle, + flank_angle=self.flank_angle, + shape=self.shape, + shape_radius=self.shape_radius, + ref_side_index=self.main_beam_ref_side_index, + ) + + # generate dovetail mortise features + cross_feature = DovetailMortise.from_frame_and_beam( + frame=main_feature.frame_from_params_and_beam(self.main_beam), + beam=self.cross_beam, + start_depth=0.0, # TODO: to be updated once housing is implemented + angle=self.rotation, + length=main_feature.length, + width=main_feature.width, + depth=main_feature.height, + cone_angle=main_feature.cone_angle, + flank_angle=main_feature.flank_angle, + shape=main_feature.shape, + shape_radius=main_feature.shape_radius, + ref_side_index=self.cross_beam_ref_side_index, + ) + + # add features to beams + self.main_beam.add_features(main_feature) + self.cross_beam.add_features(cross_feature) + # add features to joint + self.features = [cross_feature, main_feature] + + def define_dovetail_tool(self, tool_angle, tool_diameter, tool_height): + """Define the parameters for the dovetail feature based on a defined dovetail cutting tool. + + Parameters + ---------- + tool_angle : float + The angle of the dovetail cutter tool. + tool_diameter : float + The diameter of the dovetail cutter tool. + tool_height : float + The height of the dovetail cutter tool. + + """ + # type: (float, float, float) -> None + # get the tool parameters + tool_top_radius = tool_diameter / 2 - tool_height * (math.tan(math.radians(tool_angle))) + # define parameters related to the tool if a tool is defined + self.height = tool_height + self.flank_angle = tool_angle + self.shape_radius = tool_top_radius + + def restore_beams_from_keys(self, model): + """After de-serialization, restores references to the main and cross beams saved in the model.""" + self.main_beam = model.elementdict[self.main_beam_guid] + self.cross_beam = model.elementdict[self.cross_beam_guid] diff --git a/tests/compas_timber/gh/test_dovetail.ghx b/tests/compas_timber/gh/test_dovetail.ghx new file mode 100644 index 000000000..d7f946809 --- /dev/null +++ b/tests/compas_timber/gh/test_dovetail.ghx @@ -0,0 +1,15023 @@ + + + + + + + + 0 + 2 + 2 + + + + + + + 1 + 0 + 7 + + + + + + fa5d732c-b31a-4064-bc30-fdd75169872b + Shaded + Selection + 1 + + 100;150;0;0 + + + 100;0;150;0 + + + + + + 638584536051543966 + + test_dovetail.ghx + + + + + 0 + + + + + + -1967 + 229 + + 1.47058821 + + + + + 0 + + + + + + + 0 + + + + + 2 + + + + + GhPython, Version=7.34.23267.11001, Culture=neutral, PublicKeyToken=null + 7.34.23267.11001 + + 00000000-0000-0000-0000-000000000000 + + + + + + + Human, Version=1.7.3.0, Culture=neutral, PublicKeyToken=null + 1.7.3.0 + + 5f86fa9f-c62b-50e8-157b-b454ef3e00fa + Human + 1.2.0 + + + + + + + 150 + + + + + c552a431-af5b-46a9-a8a4-0fcbc27ef596 + Group + + + + + 1 + + 150;255;255;255 + + A group of Grasshopper objects + 1631fe30-c80f-49ec-9128-96481121ad52 + a8a91d2c-6d94-4451-b47e-a81520b31d82 + ae229730-bb17-41d8-826e-9f750d58d128 + 54426880-0968-40ad-8b1e-e50de0a67289 + 46feaa45-4a40-458f-a4fd-9de5989af911 + 1dc4a321-0021-4a13-90df-e852c7e4e773 + f05fce76-e87f-48e1-a86d-07f6d4084049 + 3606e2e7-dfd8-4977-a4d6-75b937b9b9f1 + 2a5a867a-a76c-4f02-bce5-209e79f61aac + f29282aa-9bfd-49b7-a1a0-1f7fc87af0e9 + 31a8ef0b-5079-4e66-9612-dff6a751c5a8 + 11 + 0b9fb065-5070-4c3f-96bb-831d0aa1aedf + Group + + + + + + + + + + + c552a431-af5b-46a9-a8a4-0fcbc27ef596 + Group + + + + + 1 + + 150;255;255;255 + + A group of Grasshopper objects + 37c9fe3d-b5bb-4362-ba0f-d5053fcb2141 + 7e4bcb53-01cc-446e-8792-764d02b1451e + c3c1dc6d-7ceb-4d7f-add6-fa924b4cc936 + ab977350-6a8d-472b-9276-52a400da755b + dec5089d-c452-417b-a948-4034d6df42ce + 1f382d1c-8a89-4e8a-8102-597801cd81a3 + ba1c263a-665b-4a0d-a1a6-e771de90f422 + 62e63fca-99eb-4ff3-8b10-df2b88807b8d + d6206efa-67b1-4c9e-b762-42a2cf302219 + 244dd1d8-93d4-4afc-851d-064a07e08680 + 6b6d7ea8-7a08-4879-939c-5f96604c4678 + 89a39aa2-89d0-4ad6-9043-6ef61b51023c + 12 + c67c3391-fbfa-4fd4-bd01-edf5270cbdc6 + Group + + + + + + + + + + + 57da07bd-ecab-415d-9d86-af36d7073abc + Number Slider + + + + + Numeric slider for single values + c3c1dc6d-7ceb-4d7f-add6-fa924b4cc936 + Number Slider + + false + 0 + + + + + + 1405 + 137 + 188 + 20 + + + 1405.227 + 137.5545 + + + + + + 2 + 1 + 0 + 200 + 0 + 0 + 10 + + + + + + + + + 57da07bd-ecab-415d-9d86-af36d7073abc + Number Slider + + + + + Numeric slider for single values + ab977350-6a8d-472b-9276-52a400da755b + Number Slider + width + false + 0 + + + + + + 1405 + 158 + 188 + 20 + + + 1405.158 + 158.6988 + + + + + + 3 + 1 + 1 + 200 + 0 + 0 + 25 + + + + + + + + + 57da07bd-ecab-415d-9d86-af36d7073abc + Number Slider + + + + + Numeric slider for single values + 1f382d1c-8a89-4e8a-8102-597801cd81a3 + Number Slider + cone_angle + false + 0 + + + + + + 1405 + 202 + 188 + 20 + + + 1405.305 + 202.4095 + + + + + + 3 + 1 + 1 + 100 + 0 + 0 + 10 + + + + + + + + + c552a431-af5b-46a9-a8a4-0fcbc27ef596 + Group + + + + + 1 + + 150;255;255;255 + + A group of Grasshopper objects + 37c9fe3d-b5bb-4362-ba0f-d5053fcb2141 + 7e4bcb53-01cc-446e-8792-764d02b1451e + 2 + ba1c263a-665b-4a0d-a1a6-e771de90f422 + Group + Ref_Side + + + + + + + + + + d5967b9f-e8ee-436b-a8ad-29fdcecf32d5 + Curve + + + + + Contains a collection of generic curves + true + 1631fe30-c80f-49ec-9128-96481121ad52 + Curve + Crv + false + 0 + + + + + + -222 + 535 + 50 + 24 + + + -196.0559 + 547.3639 + + + + + + 1 + + + + + 1 + {0} + + + + + -1 + + Y2BkYGD4DwQgGgR4mIBEeFBGZl6+c35ubn6ejkJYalFxZn6erbmesYmekbGRmbmeoaGBgaGOgnNpTklpUaptXmppSVFijo5CQGlSTmayd2plSH52ap6tqamRkYVhqqV5srmpqamxASvIFhmw4Xruqfm5qSVFlXoB+TmVOZl5qc6lRWWpLEAF7GUQC7kSi5IzMstSjVNyOfMLUvPySouSillSEksSQYo4ODiYQG4VUGdgMALS9f/4eDiZgQx+EDEViJl+1TMxdEL99fs/E4MIlP3synU/95eXBfc/ZRBoZlT64Ddn9+0/QPlAqLwAyNy4bxpcrDPvOXz4/18+rHjlAagcQwpU3KHb70cS13q4OBMDAuxtjlpjZzvZAeSS6oIuv//1TA0wOW6GwQQA + + 00000000-0000-0000-0000-000000000000 + + + + + + + + + + + + + 410755b1-224a-4c1e-a407-bf32fb45ea7e + 00000000-0000-0000-0000-000000000000 + CT: Beam + + + + + """Creates a Beam from a LineCurve.""" + +import rhinoscriptsyntax as rs +from compas.scene import Scene +from compas_rhino.conversions import line_to_compas +from compas_rhino.conversions import vector_to_compas +from ghpythonlib.componentbase import executingcomponent as component +from Grasshopper.Kernel.GH_RuntimeMessageLevel import Error +from Grasshopper.Kernel.GH_RuntimeMessageLevel import Warning +from Rhino.RhinoDoc import ActiveDoc + +from compas_timber.elements import Beam as CTBeam +from compas_timber.ghpython.rhino_object_name_attributes import update_rhobj_attributes_name + + +class Beam_fromCurve(component): + def RunScript(self, centerline, z_vector, width, height, category, updateRefObj): + # minimum inputs required + if not centerline: + self.AddRuntimeMessage(Warning, "Input parameter 'Centerline' failed to collect data") + if not width: + self.AddRuntimeMessage(Warning, "Input parameter 'Width' failed to collect data") + if not height: + self.AddRuntimeMessage(Warning, "Input parameter 'Height' failed to collect data") + + # reformat unset parameters for consistency + if not z_vector: + z_vector = [None] + if not category: + category = [None] + + beams = [] + blanks = [] + scene = Scene() + + if centerline and height and width: + # check list lengths for consistency + N = len(centerline) + if len(z_vector) not in (0, 1, N): + self.AddRuntimeMessage( + Error, " In 'ZVector' I need either none, one or the same number of inputs as the Crv parameter." + ) + if len(width) not in (1, N): + self.AddRuntimeMessage( + Error, " In 'W' I need either one or the same number of inputs as the Crv parameter." + ) + if len(height) not in (1, N): + self.AddRuntimeMessage( + Error, " In 'H' I need either one or the same number of inputs as the Crv parameter." + ) + if len(category) not in (0, 1, N): + self.AddRuntimeMessage( + Error, " In 'Category' I need either none, one or the same number of inputs as the Crv parameter." + ) + + # duplicate data if None or single value + if len(z_vector) != N: + z_vector = [z_vector[0] for _ in range(N)] + if len(width) != N: + width = [width[0] for _ in range(N)] + if len(height) != N: + height = [height[0] for _ in range(N)] + if len(category) != N: + category = [category[0] for _ in range(N)] + + for line, z, w, h, c in zip(centerline, z_vector, width, height, category): + guid, geometry = self._get_guid_and_geometry(line) + rhino_line = rs.coerceline(geometry) + line = line_to_compas(rhino_line) + + z = vector_to_compas(z) if z else None + beam = CTBeam.from_centerline(centerline=line, width=w, height=h, z_vector=z) + beam.attributes["rhino_guid"] = str(guid) if guid else None + beam.attributes["category"] = c + print(guid) + if updateRefObj and guid: + update_rhobj_attributes_name(guid, "width", str(w)) + update_rhobj_attributes_name(guid, "height", str(h)) + update_rhobj_attributes_name(guid, "zvector", str(list(beam.frame.zaxis))) + update_rhobj_attributes_name(guid, "category", c) + + beams.append(beam) + scene.add(beam.blank) + + blanks = scene.draw() + + return beams, blanks + + def _get_guid_and_geometry(self, line): + # internalized curves and GH geometry will not have persistent GUIDs, referenced Rhino objects will + # type hint on the input has to be 'ghdoc' for this to work + guid = None + geometry = line + rhino_obj = ActiveDoc.Objects.FindId(line) + if rhino_obj: + guid = line + geometry = rhino_obj.Geometry + return guid, geometry + + Creates a Beam from a LineCurve. + + 192 + 192 + + + 974 + 699 + + true + true + true + + iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAALDAAACwwBP0AiyAAAAhRJREFUSEulVLtKA0EUzSfkD3zlrUKMIcHGB6ghIj5QxEoDiiA2gtqGYBEfRRQiSRc7iXU6IWKZwh8Q/AI/wEcRr/cMmWUZ7ya7Whw2uXvvOTNnzqyPiP6Nob7+Q0bBQJ0xLQ64BRP4R6OxZ37Swf4+nRwdKyxmFyg4MEhcvxYH3YCH4+GhwHsqMd6+r9ep1WrRY7OpyGcmp2hvZxcCBXG4F3gQligykIIcIulkknJb26qGnXgW4AF/NBi6AzkIQAxcnl+AjAr5vFXzLMDN8ZFI9JXxfVMuW0RYMZ8D3dZqVs2zADeuRALBD3jbaDQUAZ74D2ib/iQwHI5UudHyFsNYLVaN5JjEGj0F+KUVQXisB+EzyO01CV0F+IUYwY21dZUUXXMCemGdKMBFMYIYgIDktx2wD0FAILBQO7FjBGGJveYE2Id5Jn8AH3g1uRhBHCLI7TUJ2BVCAHLuP9eLVgJc3GS/vyZSaboqldQWq5UKZeczyhYdSyfouCLGzLViJ9cCT1A2sbq03NNv7Ay7joXCLzzTb5JbAqa/mdm5XzfTRCeG1Dk35bcEUQAJchLArpAmkDMOJVI7PAkgrrgbuCM8F5cITbgWQFyHI9F2x29HS0y4EkBcuU/5LZF0Q1cB+I3fHMFP7stJBL3gKHBWLOoIvnGPK78lKAEQQkQjOZZQlnS+pq79lgABfNxw2UycSgPeQL4fsKzVEEQlKEcAAAAASUVORK5CYII= + + false + e28e26d5-b791-48bc-902c-a1929ace9685 + true + true + CT: Beam + Beam + + + + + + 1016 + 377 + 145 + 124 + + + 1107 + 439 + + + + + + 6 + 84fa917c-1ed8-4db3-8be1-7bdc4a6495a2 + 84fa917c-1ed8-4db3-8be1-7bdc4a6495a2 + 84fa917c-1ed8-4db3-8be1-7bdc4a6495a2 + 84fa917c-1ed8-4db3-8be1-7bdc4a6495a2 + 84fa917c-1ed8-4db3-8be1-7bdc4a6495a2 + 84fa917c-1ed8-4db3-8be1-7bdc4a6495a2 + 2 + 8ec86459-bf01-4409-baee-174d0d2b13d0 + 8ec86459-bf01-4409-baee-174d0d2b13d0 + + + + + 1 + true + Referenced curve or line, Guid of curve or line in the active Rhino document. + 1bee6a78-7b3a-4d65-9fa6-ee03a6a467d7 + Centerline + Centerline + true + 1 + true + 36c4deda-ceed-4920-a3f3-2d18ca613b35 + 1 + 87f87f55-5b71-41f4-8aea-21d494016f81 + + + + + + 1018 + 379 + 74 + 20 + + + 1056.5 + 389 + + + + + + + + 1 + true + Vector defining the orientation of the cross-section (corresponds to the height dimension). If None, a default will be taken. + 9cde7a1d-c80f-42e0-9a2a-232ea2038830 + ZVector + ZVector + true + 1 + true + 0 + 15a50725-e3d3-4075-9f7c-142ba5f40747 + + + + + + 1018 + 399 + 74 + 20 + + + 1056.5 + 409 + + + + + + + + 1 + true + Width of the cross-section (usually the smaller dimension). + 640d61a5-30a2-4d6d-9d06-693c7998daa3 + Width + Width + true + 1 + true + b0579e2b-7024-4c03-8e13-7ddcca5e08a6 + 1 + 39fbc626-7a01-46ab-a18e-ec1c0c41685b + + + + + + 1018 + 419 + 74 + 20 + + + 1056.5 + 429 + + + + + + + + 1 + true + Height of the cross-section (usually the larger dimension). + c55fd807-c67f-4bc9-8acb-00f1fb7639c8 + Height + Height + true + 1 + true + 13835904-23ad-43b2-a586-0ae141339040 + 1 + 39fbc626-7a01-46ab-a18e-ec1c0c41685b + + + + + + 1018 + 439 + 74 + 20 + + + 1056.5 + 449 + + + + + + + + 1 + true + Category of a beam. + 3cabcb03-288e-45d6-a8c0-e9ddc8b859ce + Category + Category + true + 1 + true + 0 + 37261734-eec7-4f50-b6a8-b8d1f3c4396b + + + + + + 1018 + 459 + 74 + 20 + + + 1056.5 + 469 + + + + + + + + true + (optional) If True, the attributes in the referenced object will be updated/overwritten with the new values given here. + bb55e50a-3bee-410d-8466-5dfca9d565d1 + updateRefObj + updateRefObj + true + 0 + true + 0 + d60527f5-b5af-4ef6-8970-5f96fe412559 + + + + + + 1018 + 479 + 74 + 20 + + + 1056.5 + 489 + + + + + + + + Beam object(s). + 3984d0df-294e-462f-b103-b87bb94e3021 + Beam + Beam + false + 0 + + + + + + 1122 + 379 + 37 + 60 + + + 1140.5 + 409 + + + + + + + + Shape of the beam's blank. + b0a6568e-06d8-4c68-b393-d8274f661640 + Blank + Blank + false + 0 + + + + + + 1122 + 439 + 37 + 60 + + + 1140.5 + 469 + + + + + + + + + + + + + + 57da07bd-ecab-415d-9d86-af36d7073abc + Number Slider + + + + + Numeric slider for single values + d9d51b18-d5af-4f8f-822f-9fbd0f30d642 + Number Slider + + false + 0 + + + + + + 827 + 422 + 166 + 20 + + + 827.5709 + 422.7206 + + + + + + 3 + 1 + 1 + 100 + 0 + 0 + 100 + + + + + + + + + d5967b9f-e8ee-436b-a8ad-29fdcecf32d5 + Curve + + + + + Contains a collection of generic curves + true + 18d2fa4a-de1b-4cfd-bf80-4b8d5fa0182f + Curve + Crv + false + 0 + + + + + + 317 + 369 + 50 + 24 + + + 342.1181 + 381.6854 + + + + + + 1 + + + + + 1 + {0} + + + + + -1 + + Y2BkYGD4DwQgGgR4mIBEeFBGZl6+c35ubn6ejkJYalFxZn6erbmesYmekbGRmbmeoaGBgaGOgnNpTklpUaptXmppSVFijo5CQGlSTmayd2plSH52ap6tqamRkYVhqqV5srmpqamxASvIFhmw4Xruqfm5qSVFlXoB+TmVOZl5qc6lRWWpLEAF7GUQC7kSi5IzMstSjVNyOfMLUvPySouSillSEksSQYo4ODiYQG4VUGdgMALS9f/4eDiZgQx+EDEViJl+1TMxdEL99fs/E4MIlP3synU/95eXBfc/ZRBoZlT64Ddn9+0/QPlAqLwAyFyF5Ln1s/nvOXz8/18+rHjlAagcw4SN5/afrXvg8BlNnIkBDRQsdwC55PeJbev/1zM1wIS50dUNKAAA + + 00000000-0000-0000-0000-000000000000 + + + + + + + + + + + + + 410755b1-224a-4c1e-a407-bf32fb45ea7e + 00000000-0000-0000-0000-000000000000 + CT: Beam + + + + + """Creates a Beam from a LineCurve.""" + +import rhinoscriptsyntax as rs +from compas.scene import Scene +from compas_rhino.conversions import line_to_compas +from compas_rhino.conversions import vector_to_compas +from ghpythonlib.componentbase import executingcomponent as component +from Grasshopper.Kernel.GH_RuntimeMessageLevel import Error +from Grasshopper.Kernel.GH_RuntimeMessageLevel import Warning +from Rhino.RhinoDoc import ActiveDoc + +from compas_timber.elements import Beam as CTBeam +from compas_timber.ghpython.rhino_object_name_attributes import update_rhobj_attributes_name + + +class Beam_fromCurve(component): + def RunScript(self, centerline, z_vector, width, height, category, updateRefObj): + # minimum inputs required + if not centerline: + self.AddRuntimeMessage(Warning, "Input parameter 'Centerline' failed to collect data") + if not width: + self.AddRuntimeMessage(Warning, "Input parameter 'Width' failed to collect data") + if not height: + self.AddRuntimeMessage(Warning, "Input parameter 'Height' failed to collect data") + + # reformat unset parameters for consistency + if not z_vector: + z_vector = [None] + if not category: + category = [None] + + beams = [] + blanks = [] + scene = Scene() + + if centerline and height and width: + # check list lengths for consistency + N = len(centerline) + if len(z_vector) not in (0, 1, N): + self.AddRuntimeMessage( + Error, " In 'ZVector' I need either none, one or the same number of inputs as the Crv parameter." + ) + if len(width) not in (1, N): + self.AddRuntimeMessage( + Error, " In 'W' I need either one or the same number of inputs as the Crv parameter." + ) + if len(height) not in (1, N): + self.AddRuntimeMessage( + Error, " In 'H' I need either one or the same number of inputs as the Crv parameter." + ) + if len(category) not in (0, 1, N): + self.AddRuntimeMessage( + Error, " In 'Category' I need either none, one or the same number of inputs as the Crv parameter." + ) + + # duplicate data if None or single value + if len(z_vector) != N: + z_vector = [z_vector[0] for _ in range(N)] + if len(width) != N: + width = [width[0] for _ in range(N)] + if len(height) != N: + height = [height[0] for _ in range(N)] + if len(category) != N: + category = [category[0] for _ in range(N)] + + for line, z, w, h, c in zip(centerline, z_vector, width, height, category): + guid, geometry = self._get_guid_and_geometry(line) + rhino_line = rs.coerceline(geometry) + line = line_to_compas(rhino_line) + + z = vector_to_compas(z) if z else None + beam = CTBeam.from_centerline(centerline=line, width=w, height=h, z_vector=z) + beam.attributes["rhino_guid"] = str(guid) if guid else None + beam.attributes["category"] = c + print(guid) + if updateRefObj and guid: + update_rhobj_attributes_name(guid, "width", str(w)) + update_rhobj_attributes_name(guid, "height", str(h)) + update_rhobj_attributes_name(guid, "zvector", str(list(beam.frame.zaxis))) + update_rhobj_attributes_name(guid, "category", c) + + beams.append(beam) + scene.add(beam.blank) + + blanks = scene.draw() + + return beams, blanks + + def _get_guid_and_geometry(self, line): + # internalized curves and GH geometry will not have persistent GUIDs, referenced Rhino objects will + # type hint on the input has to be 'ghdoc' for this to work + guid = None + geometry = line + rhino_obj = ActiveDoc.Objects.FindId(line) + if rhino_obj: + guid = line + geometry = rhino_obj.Geometry + return guid, geometry + + Creates a Beam from a LineCurve. + + 96 + 96 + + + 741 + 702 + + true + true + true + + iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAALDAAACwwBP0AiyAAAAhRJREFUSEulVLtKA0EUzSfkD3zlrUKMIcHGB6ghIj5QxEoDiiA2gtqGYBEfRRQiSRc7iXU6IWKZwh8Q/AI/wEcRr/cMmWUZ7ya7Whw2uXvvOTNnzqyPiP6Nob7+Q0bBQJ0xLQ64BRP4R6OxZ37Swf4+nRwdKyxmFyg4MEhcvxYH3YCH4+GhwHsqMd6+r9ep1WrRY7OpyGcmp2hvZxcCBXG4F3gQligykIIcIulkknJb26qGnXgW4AF/NBi6AzkIQAxcnl+AjAr5vFXzLMDN8ZFI9JXxfVMuW0RYMZ8D3dZqVs2zADeuRALBD3jbaDQUAZ74D2ib/iQwHI5UudHyFsNYLVaN5JjEGj0F+KUVQXisB+EzyO01CV0F+IUYwY21dZUUXXMCemGdKMBFMYIYgIDktx2wD0FAILBQO7FjBGGJveYE2Id5Jn8AH3g1uRhBHCLI7TUJ2BVCAHLuP9eLVgJc3GS/vyZSaboqldQWq5UKZeczyhYdSyfouCLGzLViJ9cCT1A2sbq03NNv7Ay7joXCLzzTb5JbAqa/mdm5XzfTRCeG1Dk35bcEUQAJchLArpAmkDMOJVI7PAkgrrgbuCM8F5cITbgWQFyHI9F2x29HS0y4EkBcuU/5LZF0Q1cB+I3fHMFP7stJBL3gKHBWLOoIvnGPK78lKAEQQkQjOZZQlnS+pq79lgABfNxw2UycSgPeQL4fsKzVEEQlKEcAAAAASUVORK5CYII= + + false + ea292354-f0cd-4b58-ba78-57c730de54ad + true + true + CT: Beam + Beam + + + + + + 1032 + 531 + 145 + 124 + + + 1123 + 593 + + + + + + 6 + 84fa917c-1ed8-4db3-8be1-7bdc4a6495a2 + 84fa917c-1ed8-4db3-8be1-7bdc4a6495a2 + 84fa917c-1ed8-4db3-8be1-7bdc4a6495a2 + 84fa917c-1ed8-4db3-8be1-7bdc4a6495a2 + 84fa917c-1ed8-4db3-8be1-7bdc4a6495a2 + 84fa917c-1ed8-4db3-8be1-7bdc4a6495a2 + 2 + 8ec86459-bf01-4409-baee-174d0d2b13d0 + 8ec86459-bf01-4409-baee-174d0d2b13d0 + + + + + 1 + true + Referenced curve or line, Guid of curve or line in the active Rhino document. + 53757008-8b2d-49b8-ada7-e0c81d24586f + Centerline + Centerline + true + 1 + true + d2d51328-18f6-4ef3-8dd6-1df6b6af922e + 1 + 87f87f55-5b71-41f4-8aea-21d494016f81 + + + + + + 1034 + 533 + 74 + 20 + + + 1072.5 + 543 + + + + + + + + 1 + true + Vector defining the orientation of the cross-section (corresponds to the height dimension). If None, a default will be taken. + db7b0ca0-f838-405d-ae51-0e61596dd253 + ZVector + ZVector + true + 1 + true + 4c90d06e-9810-41a6-baa8-91b753ccec55 + 1 + 15a50725-e3d3-4075-9f7c-142ba5f40747 + + + + + + 1034 + 553 + 74 + 20 + + + 1072.5 + 563 + + + + + + + + 1 + true + Width of the cross-section (usually the smaller dimension). + 4b9bf139-421a-4a01-b027-ec23d68ace87 + Width + Width + true + 1 + true + 81d19eff-02b1-4406-a0c3-47dd897a152f + 1 + 39fbc626-7a01-46ab-a18e-ec1c0c41685b + + + + + + 1034 + 573 + 74 + 20 + + + 1072.5 + 583 + + + + + + + + 1 + true + Height of the cross-section (usually the larger dimension). + 4ae02a01-6e0a-4998-bca5-654916c862fc + Height + Height + true + 1 + true + 478fc3b1-c4f8-4087-8fe6-c2905e21be9a + 1 + 39fbc626-7a01-46ab-a18e-ec1c0c41685b + + + + + + 1034 + 593 + 74 + 20 + + + 1072.5 + 603 + + + + + + + + 1 + true + Category of a beam. + 98e31428-399a-4dde-a0e3-585981226918 + Category + Category + true + 1 + true + 0 + 37261734-eec7-4f50-b6a8-b8d1f3c4396b + + + + + + 1034 + 613 + 74 + 20 + + + 1072.5 + 623 + + + + + + + + true + (optional) If True, the attributes in the referenced object will be updated/overwritten with the new values given here. + 32189ead-4252-4352-bf45-514c147d7b57 + updateRefObj + updateRefObj + true + 0 + true + 0 + d60527f5-b5af-4ef6-8970-5f96fe412559 + + + + + + 1034 + 633 + 74 + 20 + + + 1072.5 + 643 + + + + + + + + Beam object(s). + 90fae02f-7750-462d-828f-cf5f2a0d39ae + Beam + Beam + false + 0 + + + + + + 1138 + 533 + 37 + 60 + + + 1156.5 + 563 + + + + + + + + Shape of the beam's blank. + 55c66290-a177-4518-824a-6a531a1e8086 + Blank + Blank + false + 0 + + + + + + 1138 + 593 + 37 + 60 + + + 1156.5 + 623 + + + + + + + + + + + + + + 57da07bd-ecab-415d-9d86-af36d7073abc + Number Slider + + + + + Numeric slider for single values + 478fc3b1-c4f8-4087-8fe6-c2905e21be9a + Number Slider + + false + 0 + + + + + + 825 + 596 + 166 + 20 + + + 825.0048 + 596.0939 + + + + + + 3 + 1 + 1 + 100 + 0 + 0 + 100 + + + + + + + + + 22990b1f-9be6-477c-ad89-f775cd347105 + Flip Curve + + + + + Flip a curve using an optional guide curve. + true + 2098ce01-e0b3-4e52-b0ed-3c62bb419a62 + Flip Curve + Flip + + + + + + 391 + 615 + 66 + 44 + + + 423 + 637 + + + + + + Curve to flip + 417fea5c-f458-4cb9-81ea-3c456faf67b1 + Curve + C + false + 2ace07a8-3cff-4c52-b37f-cd3e591aeddd + 1 + + + + + + 393 + 617 + 15 + 20 + + + 402 + 627 + + + + + + + + Optional guide curve + 0cee7869-6730-4b7d-832a-4aa6b5d4749a + Guide + G + true + 0 + + + + + + 393 + 637 + 15 + 20 + + + 402 + 647 + + + + + + + + Flipped curve + 0b2eef87-f147-4a36-b443-85031493ef71 + Curve + C + false + 0 + + + + + + 438 + 617 + 17 + 20 + + + 446.5 + 627 + + + + + + + + Flip action + d77f6082-7255-4694-8fae-7a31bce87565 + Flag + F + false + 0 + + + + + + 438 + 637 + 17 + 20 + + + 446.5 + 647 + + + + + + + + + + + + 11bbd48b-bb0a-4f1b-8167-fa297590390d + End Points + + + + + Extract the end points of a curve. + true + 3606e2e7-dfd8-4977-a4d6-75b937b9b9f1 + End Points + End + + + + + + -119 + 562 + 64 + 44 + + + -88 + 584 + + + + + + Curve to evaluate + c35e0949-31c0-4148-833b-aeed3fc97aa3 + Curve + C + false + 1631fe30-c80f-49ec-9128-96481121ad52 + 1 + + + + + + -117 + 564 + 14 + 40 + + + -108.5 + 584 + + + + + + + + Curve start point + 349dd0c5-02cc-4111-abf4-8728c5a86c2c + Start + S + false + 0 + + + + + + -73 + 564 + 16 + 20 + + + -65 + 574 + + + + + + + + Curve end point + 08d2450a-ba8f-45b3-a29d-274a84997a85 + End + E + false + 0 + + + + + + -73 + 584 + 16 + 20 + + + -65 + 594 + + + + + + + + + + + + e9eb1dcf-92f6-4d4d-84ae-96222d60f56b + Move + + + + + Translate (move) an object along a vector. + true + 2a5a867a-a76c-4f02-bce5-209e79f61aac + Move + Move + + + + + + -33 + 609 + 83 + 44 + + + 15 + 631 + + + + + + Base geometry + a0edbf9a-3853-4ab0-b53f-aef5027fa137 + Geometry + G + true + 08d2450a-ba8f-45b3-a29d-274a84997a85 + 1 + + + + + + -31 + 611 + 31 + 20 + + + -6 + 621 + + + + + + + + Translation vector + 8acddbb8-4abe-4d9b-81aa-f36b38b9dda4 + -x + Motion + T + false + 3441a608-3853-45f3-92c7-1d1941125460 + 1 + + + + + + -31 + 631 + 31 + 20 + + + -6 + 641 + + + + + + 1 + + + + + 1 + {0} + + + + + + 0 + 0 + 10 + + + + + + + + + + + + Translated geometry + 3b2b25a6-3d8c-4a41-b0a2-3ea3be957972 + Geometry + G + false + 0 + + + + + + 30 + 611 + 18 + 20 + + + 39 + 621 + + + + + + + + Transformation data + d8dfb3a4-eb51-4258-aa5c-b6584682d0ef + Transform + X + false + 0 + + + + + + 30 + 631 + 18 + 20 + + + 39 + 641 + + + + + + + + + + + + 79f9fbb3-8f1d-4d9a-88a9-f7961b1012cd + Unit X + + + + + Unit vector parallel to the world {x} axis. + true + a8a91d2c-6d94-4451-b47e-a81520b31d82 + Unit X + X + + + + + + -221 + 607 + 79 + 28 + + + -176 + 621 + + + + + + Unit multiplication + 0640aeed-9882-4ebf-a1b1-5080f3b20bb9 + -x + Factor + F + false + ae229730-bb17-41d8-826e-9f750d58d128 + 1 + + + + + + -219 + 609 + 28 + 24 + + + -195.5 + 621 + + + + + + 1 + + + + + 1 + {0} + + + + + 1 + + + + + + + + + + + World {x} vector + 20ce5606-82c9-43d0-b182-e8db425945b5 + Unit vector + V + false + 0 + + + + + + -161 + 609 + 17 + 24 + + + -152.5 + 621 + + + + + + + + + + + + 57da07bd-ecab-415d-9d86-af36d7073abc + Number Slider + + + + + Numeric slider for single values + ae229730-bb17-41d8-826e-9f750d58d128 + Number Slider + + false + 0 + + + + + + -395 + 611 + 163 + 20 + + + -394.2762 + 611.8264 + + + + + + 3 + 1 + 1 + 4000 + 0 + 0 + 500 + + + + + + + + + 4c4e56eb-2f04-43f9-95a3-cc46a14f495a + Line + + + + + Create a line between two points. + true + f29282aa-9bfd-49b7-a1a0-1f7fc87af0e9 + Line + Ln + + + + + + 75 + 589 + 63 + 44 + + + 106 + 611 + + + + + + Line start point + cb99cc28-9c59-4191-95b9-3cae3e6f1e9c + Start Point + A + false + 349dd0c5-02cc-4111-abf4-8728c5a86c2c + 1 + + + + + + 77 + 591 + 14 + 20 + + + 85.5 + 601 + + + + + + + + Line end point + fb12ca80-6fc6-404b-8d93-4fdb6c2c6520 + End Point + B + false + 3b2b25a6-3d8c-4a41-b0a2-3ea3be957972 + 1 + + + + + + 77 + 611 + 14 + 20 + + + 85.5 + 621 + + + + + + + + Line segment + b37fb167-edb3-4a28-b8fd-a498a7811210 + Line + L + false + 0 + + + + + + 121 + 591 + 15 + 40 + + + 128.5 + 611 + + + + + + + + + + + + 9103c240-a6a9-4223-9b42-dbd19bf38e2b + Unit Z + + + + + Unit vector parallel to the world {z} axis. + true + 54426880-0968-40ad-8b1e-e50de0a67289 + Unit Z + Z + + + + + + -203 + 641 + 63 + 28 + + + -174 + 655 + + + + + + Unit multiplication + 45ecdc35-f4be-4e29-97b3-ae200336ed76 + Factor + F + false + 46feaa45-4a40-458f-a4fd-9de5989af911 + 1 + + + + + + -201 + 643 + 12 + 24 + + + -193.5 + 655 + + + + + + 1 + + + + + 1 + {0} + + + + + 1 + + + + + + + + + + + World {z} vector + 31dfa7fb-7b92-4e78-8a6c-51149c8b2402 + Unit vector + V + false + 0 + + + + + + -159 + 643 + 17 + 24 + + + -150.5 + 655 + + + + + + + + + + + + 57da07bd-ecab-415d-9d86-af36d7073abc + Number Slider + + + + + Numeric slider for single values + 46feaa45-4a40-458f-a4fd-9de5989af911 + Number Slider + + false + 0 + + + + + + -395 + 645 + 163 + 20 + + + -394.5755 + 645.9137 + + + + + + 3 + 1 + 1 + 2000 + 0 + 0 + 500 + + + + + + + + + a0d62394-a118-422d-abb3-6af115c75b25 + Addition + + + + + Mathematical addition + true + 31a8ef0b-5079-4e66-9612-dff6a751c5a8 + Addition + A+B + + + + + + -117 + 609 + 65 + 64 + + + -86 + 641 + + + + + + 3 + 8ec86459-bf01-4409-baee-174d0d2b13d0 + 8ec86459-bf01-4409-baee-174d0d2b13d0 + 8ec86459-bf01-4409-baee-174d0d2b13d0 + 1 + 8ec86459-bf01-4409-baee-174d0d2b13d0 + + + + + First item for addition + d9d3d34d-5080-4bb5-aa7b-9e02635fbae5 + A + A + true + 20ce5606-82c9-43d0-b182-e8db425945b5 + 1 + + + + + + -115 + 611 + 14 + 20 + + + -106.5 + 621 + + + + + + + + Second item for addition + 3898220f-37d7-48fc-b80c-31e2b32274f5 + B + B + true + 31dfa7fb-7b92-4e78-8a6c-51149c8b2402 + 1 + + + + + + -115 + 631 + 14 + 20 + + + -106.5 + 641 + + + + + + + + Third item for addition + 13ca1130-de29-4026-94fc-c1ca22604c07 + C + C + true + 7a214201-5ed9-46c9-afe6-bf48e768f870 + 1 + + + + + + -115 + 651 + 14 + 20 + + + -106.5 + 661 + + + + + + + + Result of addition + 3441a608-3853-45f3-92c7-1d1941125460 + Result + R + false + 0 + + + + + + -71 + 611 + 17 + 60 + + + -62.5 + 641 + + + + + + + + + + + + + + 2e78987b-9dfb-42a2-8b76-3923ac8bd91a + Boolean Toggle + + + + + Boolean (true/false) toggle + ca0ea429-34fb-4ce1-a30e-2c14ddc1552e + Boolean Toggle + FlipCurve + false + 0 + true + + + + + + 258 + 569 + 115 + 22 + + + + + + + + + + eeafc956-268e-461d-8e73-ee05c6f72c01 + Stream Filter + + + + + Filters a collection of input streams + true + 9c98408b-5774-4f45-bb43-c16e5d4c92b4 + Stream Filter + Filter + + + + + + 469 + 575 + 77 + 64 + + + 501 + 607 + + + + + + 3 + 2e3ab970-8545-46bb-836c-1c11e5610bce + 8ec86459-bf01-4409-baee-174d0d2b13d0 + 8ec86459-bf01-4409-baee-174d0d2b13d0 + 1 + 8ec86459-bf01-4409-baee-174d0d2b13d0 + + + + + Index of Gate stream + be384cd0-c014-4922-86ee-f8be45625fad + Gate + G + false + ca0ea429-34fb-4ce1-a30e-2c14ddc1552e + 1 + + + + + + 471 + 577 + 15 + 20 + + + 480 + 587 + + + + + + 1 + + + + + 1 + {0} + + + + + 0 + + + + + + + + + + + 2 + Input stream at index 0 + b069b652-f7a4-47dc-a04c-9b3003b0e168 + false + Stream 0 + 0 + true + 2ace07a8-3cff-4c52-b37f-cd3e591aeddd + 1 + + + + + + 471 + 597 + 15 + 20 + + + 480 + 607 + + + + + + + + 2 + Input stream at index 1 + 4bbd6850-b99a-47c4-a371-8adee5564955 + false + Stream 1 + 1 + true + 0b2eef87-f147-4a36-b443-85031493ef71 + 1 + + + + + + 471 + 617 + 15 + 20 + + + 480 + 627 + + + + + + + + 2 + Filtered stream + 09fe95fb-9551-4690-abf6-4a0665002914 + false + Stream + S(1) + false + 0 + + + + + + 516 + 577 + 28 + 60 + + + 530 + 607 + + + + + + + + + + + + + + c552a431-af5b-46a9-a8a4-0fcbc27ef596 + Group + + + + + 1 + + 150;255;255;255 + + A group of Grasshopper objects + a8a91d2c-6d94-4451-b47e-a81520b31d82 + ae229730-bb17-41d8-826e-9f750d58d128 + 54426880-0968-40ad-8b1e-e50de0a67289 + 46feaa45-4a40-458f-a4fd-9de5989af911 + 780dd7ff-1451-4d20-b77b-f1c386f8a9ed + 27b7a426-ec72-4334-8c70-78c50fb834e2 + 6 + f05fce76-e87f-48e1-a86d-07f6d4084049 + Group + + + + + + + + + + + c552a431-af5b-46a9-a8a4-0fcbc27ef596 + Group + + + + + 1 + + 150;255;255;255 + + A group of Grasshopper objects + 18d2fa4a-de1b-4cfd-bf80-4b8d5fa0182f + 1 + e22e52d8-5a24-494f-8bd6-2f5c368cced0 + Group + CrossBeam + + + + + + + + + + 410755b1-224a-4c1e-a407-bf32fb45ea7e + 00000000-0000-0000-0000-000000000000 + GhPython Script + + + + + from compas_timber.connections import TDovetailJoint + +from compas_timber.model import TimberModel +from compas_timber.fabrication import BTLx + +from compas_rhino.conversions import brep_to_rhino + +from compas_rhino import unload_modules + +unload_modules("compas_timber") + +# Instantiate TimberModel & Add Beams +timber_model = TimberModel() +beams = [cross_beam, main_beam] +for beam in beams: + if beam.features: + beam.remove_features(beam.features) + timber_model.add_element(beam) + + + +# Create TStepJoint joint +dovetail_joint = TDovetailJoint.create( + timber_model, + main_beam, + cross_beam, + start_y=start_y, + start_depth=start_depth, + rotation=rotation, + length=length, + width=width, + cone_angle=cone_angle, + dovetail_shape=4, + tool_angle=15.0, + tool_diameter=60.034, + tool_height=28.0 + ) + +timber_model.process_joinery() +# Generate Geometry +geometry = [brep_to_rhino(beam.compute_geometry()) for beam in timber_model.beams] + +# Write BTLx +btlx = BTLx(timber_model) +BTLx = btlx.btlx_string() + GhPython provides a Python script component + + 816 + 115 + + + 901 + 818 + + true + true + false + false + 20be8b4b-0eba-41a5-8f13-25defa3e633d + false + true + GhPython Script + Python + + + + + + 2284 + 683 + 155 + 184 + + + 2365 + 775 + + + + + + 9 + 84fa917c-1ed8-4db3-8be1-7bdc4a6495a2 + 84fa917c-1ed8-4db3-8be1-7bdc4a6495a2 + 84fa917c-1ed8-4db3-8be1-7bdc4a6495a2 + 84fa917c-1ed8-4db3-8be1-7bdc4a6495a2 + 84fa917c-1ed8-4db3-8be1-7bdc4a6495a2 + 84fa917c-1ed8-4db3-8be1-7bdc4a6495a2 + 84fa917c-1ed8-4db3-8be1-7bdc4a6495a2 + 84fa917c-1ed8-4db3-8be1-7bdc4a6495a2 + 84fa917c-1ed8-4db3-8be1-7bdc4a6495a2 + 3 + 3ede854e-c753-40eb-84cb-b48008f14fd4 + 8ec86459-bf01-4409-baee-174d0d2b13d0 + 8ec86459-bf01-4409-baee-174d0d2b13d0 + + + + + true + Script input cross_beam. + 36ebe6e6-4cdb-4b9d-8c4a-65feaf8cee5a + cross_beam + cross_beam + true + 0 + true + ed4dd89a-026f-43eb-81d4-433cea857301 + 1 + 87f87f55-5b71-41f4-8aea-21d494016f81 + + + + + + 2286 + 685 + 64 + 20 + + + 2319.5 + 695 + + + + + + + + true + Script input main_beam. + 407e65c9-4ebe-4581-9401-a364b25f96c2 + main_beam + main_beam + true + 0 + true + 90fae02f-7750-462d-828f-cf5f2a0d39ae + 1 + 87f87f55-5b71-41f4-8aea-21d494016f81 + + + + + + 2286 + 705 + 64 + 20 + + + 2319.5 + 715 + + + + + + + + true + Script input start_depth. + fbd107db-8f8a-4baa-876f-623012dd3bb0 + start_depth + start_depth + true + 0 + true + 4d710107-34a1-4586-be44-d6f80294f0e6 + 1 + 87f87f55-5b71-41f4-8aea-21d494016f81 + + + + + + 2286 + 725 + 64 + 20 + + + 2319.5 + 735 + + + + + + + + true + Script input length. + dab8fd89-b668-48ad-91eb-c695241b0e94 + length + length + true + 0 + true + b29d9bba-e05a-4fa4-91a3-bf1ac143a41d + 1 + 87f87f55-5b71-41f4-8aea-21d494016f81 + + + + + + 2286 + 745 + 64 + 20 + + + 2319.5 + 755 + + + + + + + + true + Script input width. + f4ca3577-e0dc-42fa-8923-d574fb19c330 + width + width + true + 0 + true + bbcbfdee-5b0e-4c92-bf29-b5811037ea03 + 1 + 87f87f55-5b71-41f4-8aea-21d494016f81 + + + + + + 2286 + 765 + 64 + 20 + + + 2319.5 + 775 + + + + + + + + true + Script input cone_angle. + b96b8906-afc8-48f4-bd18-9fb45011a64d + cone_angle + cone_angle + true + 0 + true + 3ee8dc00-1eea-48ba-bc11-c2d1911c58a5 + 1 + 87f87f55-5b71-41f4-8aea-21d494016f81 + + + + + + 2286 + 785 + 64 + 20 + + + 2319.5 + 795 + + + + + + + + true + Script input start_y. + 60f8fabd-f617-45e4-b530-2b80ab86ccd5 + start_y + start_y + true + 0 + true + e1131cec-2fb2-499e-93e5-55dce618ee47 + 1 + 87f87f55-5b71-41f4-8aea-21d494016f81 + + + + + + 2286 + 805 + 64 + 20 + + + 2319.5 + 815 + + + + + + + + true + Script input rotation. + 0061a1c5-fb5e-4201-8fb0-ba12e32ff71e + rotation + rotation + true + 0 + true + 78334c0a-357d-4e7c-b3e4-fc940bcf9f1c + 1 + 87f87f55-5b71-41f4-8aea-21d494016f81 + + + + + + 2286 + 825 + 64 + 20 + + + 2319.5 + 835 + + + + + + + + true + Script input farddoodle. + 9ba1246e-d991-44da-b36d-c30352fd34b8 + farddoodle + farddoodle + true + 0 + true + 0 + 87f87f55-5b71-41f4-8aea-21d494016f81 + + + + + + 2286 + 845 + 64 + 20 + + + 2319.5 + 855 + + + + + + + + The execution information, as output and error streams + fb496e59-6b43-4266-9c61-c466f3a89f8e + out + out + false + 0 + + + + + + 2380 + 685 + 57 + 60 + + + 2408.5 + 715 + + + + + + + + Script output geometry. + 188326e4-b130-4230-9b7f-f89e9a5dca84 + geometry + geometry + false + 0 + + + + + + 2380 + 745 + 57 + 60 + + + 2408.5 + 775 + + + + + + + + Script output BTLx. + 8674ed0a-6d18-41d1-9209-ae7aed0e558d + BTLx + BTLx + false + 0 + + + + + + 2380 + 805 + 57 + 60 + + + 2408.5 + 835 + + + + + + + + + + + + + + 59e0b89a-e487-49f8-bab8-b5bab16be14c + Panel + + + + + A panel for custom notes and text values + a31a3c97-3e91-47a8-add8-793f530b94e1 + Panel + + false + 0.88918887078762054 + ec68082b-6816-42e0-b242-ac0746b13092 + 1 + G:\Shared drives\2024_MAS\T2\03_finalization\Fabrication - Joints\BTLx\step_joint_joint_test.btlx + + + + + + 2346 + 919 + 455 + 420 + + 0 + 0 + 0 + + 2346.569 + 919.1592 + + + + + + + 255;255;250;90 + + true + true + true + false + true + D:\Papachap\Desktop\BTLx\dovetail_joint.btlx + true + + + + + + + + + 59e0b89a-e487-49f8-bab8-b5bab16be14c + Panel + + + + + A panel for custom notes and text values + dfdc288d-17d2-4daf-93bc-82b8f3f1816b + true + Panel + DovetailTenon BTLx Params + false + 0 + 6d4fe408-c656-48f6-88c9-67f2fbcdb1df + 1 + Double click to edit panel content… + + + + + + 2116 + 914 + 212 + 414 + + 0 + 0 + 0 + + 2116.989 + 914.9553 + + + + + + + 255;255;255;255 + + true + true + true + false + false + true + + + + + + + + + 59e0b89a-e487-49f8-bab8-b5bab16be14c + Panel + + + + + A panel for custom notes and text values + 1efbf060-c389-495d-aacf-3ce7dc0fcafc + true + Panel + StepJointNotch BTLx Params + false + 0 + 83bdcfa8-afbb-420d-9faa-98a158aee68d + 1 + Double click to edit panel content… + + + + + + 2813 + 920 + 212 + 418 + + 0 + 0 + 0 + + 2813.919 + 920.4133 + + + + + + + 255;255;255;255 + + true + true + true + false + false + true + + + + + + + + + 59e0b89a-e487-49f8-bab8-b5bab16be14c + Panel + + + + + A panel for custom notes and text values + d0efa1ab-d893-4335-8af7-dd6d94484ea7 + Panel + + false + 0.02266642265021801 + fb496e59-6b43-4266-9c61-c466f3a89f8e + 1 + Double click to edit panel content… + + + + + + 2285 + 584 + 370 + 107 + + 0 + 0 + 0 + + 2285.144 + 584.7254 + + + + + + + 255;255;250;90 + + true + true + true + false + false + true + + + + + + + + + c552a431-af5b-46a9-a8a4-0fcbc27ef596 + Group + + + + + 1 + + 150;255;255;255 + + A group of Grasshopper objects + 20be8b4b-0eba-41a5-8f13-25defa3e633d + d0efa1ab-d893-4335-8af7-dd6d94484ea7 + 9f3e0877-7fa6-4696-83ae-f6281f1d75f8 + 3c27b464-0da3-49d8-922b-780384eb4127 + ba663821-ec0e-4ff9-9124-fe98533cab66 + 97e9efc3-9035-45ee-bac9-e1c0876defe3 + 47c9ac3d-5335-4e4b-8cf3-b22cd1284a8e + e9fbb74f-6b87-4a1e-a684-8202331f1480 + f11ad7e4-429f-4eac-92db-ff79b89a4fa3 + f02972d5-c58c-4216-b53b-7f078d1f7fdc + 5b8e4ee4-cb19-450a-ba61-866a3686f2b2 + c6f9d24c-2b1e-4276-8e76-0fd2049e3d3a + 12 + c6445b86-caf6-4180-b0be-5dcfc6eaea9a + Group + + + + + + + + + + + 7f5c6c55-f846-4a08-9c9a-cfdc285cc6fe + Scribble + + + + + true + + 2383.265 + 573.3698 + + + 2722.865 + 573.3698 + + + 2722.865 + 610.7477 + + + 2383.265 + 610.7477 + + A quick note + Microsoft Sans Serif + 9f3e0877-7fa6-4696-83ae-f6281f1d75f8 + false + Scribble + Scribble + 50 + TDovetailJoint + + + + + + 2378.265 + 568.3698 + 349.5996 + 47.37793 + + + 2383.265 + 573.3698 + + + + + + + + + + c552a431-af5b-46a9-a8a4-0fcbc27ef596 + Group + + + + + 1 + + 150;255;255;255 + + A group of Grasshopper objects + 9f3e0877-7fa6-4696-83ae-f6281f1d75f8 + 1 + 3c27b464-0da3-49d8-922b-780384eb4127 + Group + + + + + + + + + + + 537b0419-bbc2-4ff4-bf08-afe526367b2c + Custom Preview + + + + + Allows for customized geometry previews + true + false + ba663821-ec0e-4ff9-9124-fe98533cab66 + Custom Preview + Preview + + + + + + + 2726 + 811 + 64 + 44 + + + 2776 + 833 + + + + + + Geometry to preview + true + c34e1843-bfd6-4a82-b983-7194c9f9c18c + Geometry + G + false + e9fbb74f-6b87-4a1e-a684-8202331f1480 + 1 + + + + + + 2728 + 813 + 33 + 20 + + + 2754 + 823 + + + + + + + + The material override + 710c97a8-7564-4390-af09-089f9958717d + 1 + Material + M + false + d5779e9d-d95c-4279-bd0c-9036111f46eb + 1 + + + + + + 2728 + 833 + 33 + 20 + + + 2754 + 843 + + + + + + 1 + + + + + 1 + {0} + + + + + + 255;221;160;221 + + + 255;66;48;66 + + 0.5 + + 255;255;255;255 + + 0 + + + + + + + + + + + + + + + 0148a65d-6f42-414a-9db7-9a9b2eb78437 + Brep Edges + + + + + Extract the edge curves of a brep. + true + 97e9efc3-9035-45ee-bac9-e1c0876defe3 + Brep Edges + Edges + + + + + + 2620 + 741 + 72 + 64 + + + 2650 + 773 + + + + + + Base Brep + d76deb57-3846-4215-a6d3-b4cc65f5f623 + Brep + B + false + e9fbb74f-6b87-4a1e-a684-8202331f1480 + 1 + + + + + + 2622 + 743 + 13 + 60 + + + 2630 + 773 + + + + + + + + 1 + Naked edge curves + d6302059-08b9-4456-a7f4-fc4b73a03458 + Naked + En + false + 0 + + + + + + 2665 + 743 + 25 + 20 + + + 2677.5 + 753 + + + + + + + + 1 + Interior edge curves + 8db6122b-9378-4026-a86e-58c5dd430f2a + Interior + Ei + false + 0 + + + + + + 2665 + 763 + 25 + 20 + + + 2677.5 + 773 + + + + + + + + 1 + Non-Manifold edge curves + 0c2574f5-76b7-475f-80aa-c87143496df3 + Non-Manifold + Em + false + 0 + + + + + + 2665 + 783 + 25 + 20 + + + 2677.5 + 793 + + + + + + + + + + + + a77d0879-94c2-4101-be44-e4a616ffeb0c + 5f86fa9f-c62b-50e8-157b-b454ef3e00fa + Custom Preview Lineweights + + + + + Custom Preview with Lineweights + true + 47c9ac3d-5335-4e4b-8cf3-b22cd1284a8e + Custom Preview Lineweights + PreviewLW + + + + + + + 2726 + 721 + 62 + 84 + + + 2774 + 763 + + + + + + Geometry to preview + true + b386acb8-8321-49bf-ac12-f062d85c969d + Geometry + G + false + 8db6122b-9378-4026-a86e-58c5dd430f2a + 1 + + + + + + 2728 + 723 + 31 + 20 + + + 2753 + 733 + + + + + + + + The preview shader override + 11af4056-9698-4b92-b067-fae857f62252 + 2 + Shader + S + false + d5779e9d-d95c-4279-bd0c-9036111f46eb + 1 + + + + + + 2728 + 743 + 31 + 20 + + + 2753 + 753 + + + + + + 1 + + + + + 1 + {0} + + + + + + 255;255;105;180 + + + 255;76;32;54 + + 0.5 + + 255;255;255;255 + + 0 + + + + + + + + + + + The thickness of the wire display + f4113c88-a8ac-49cd-a656-a1e84f635519 + Thickness + T + true + 0 + + + + + + 2728 + 763 + 31 + 20 + + + 2753 + 773 + + + + + + + + Set to true to try to render curves with an absolute dimension. + 56fb3908-c9cb-478b-be37-49785e21754d + Absolute + A + false + 0 + + + + + + 2728 + 783 + 31 + 20 + + + 2753 + 793 + + + + + + 1 + + + + + 1 + {0} + + + + + false + + + + + + + + + + + + + + + 919e146f-30ae-4aae-be34-4d72f555e7da + Brep + + + + + Contains a collection of Breps (Boundary REPresentations) + true + e9fbb74f-6b87-4a1e-a684-8202331f1480 + Brep + Brep + false + 188326e4-b130-4230-9b7f-f89e9a5dca84 + 1 + + + + + + 2534 + 763 + 50 + 24 + + + 2559.388 + 775.3219 + + + + + + + + + + 9c53bac0-ba66-40bd-8154-ce9829b9db1a + Colour Swatch + + + + + Colour (palette) swatch + f11ad7e4-429f-4eac-92db-ff79b89a4fa3 + Colour Swatch + Swatch + false + 0 + + 29;0;102;255 + + + + + + + 2517 + 811 + 88 + 20 + + + 2517.752 + 811.7673 + + + + + + + + + + c552a431-af5b-46a9-a8a4-0fcbc27ef596 + Group + + + + + 1 + + 150;255;255;255 + + A group of Grasshopper objects + ba663821-ec0e-4ff9-9124-fe98533cab66 + 97e9efc3-9035-45ee-bac9-e1c0876defe3 + 47c9ac3d-5335-4e4b-8cf3-b22cd1284a8e + e9fbb74f-6b87-4a1e-a684-8202331f1480 + f11ad7e4-429f-4eac-92db-ff79b89a4fa3 + 5b8e4ee4-cb19-450a-ba61-866a3686f2b2 + c6f9d24c-2b1e-4276-8e76-0fd2049e3d3a + 7 + f02972d5-c58c-4216-b53b-7f078d1f7fdc + Group + GEOMETRY + + + + + + + + + + 9c53bac0-ba66-40bd-8154-ce9829b9db1a + Colour Swatch + + + + + Colour (palette) swatch + 5b8e4ee4-cb19-450a-ba61-866a3686f2b2 + Colour Swatch + Swatch + false + 0 + + 34;240;180;137 + + + + + + + 2517 + 835 + 88 + 20 + + + 2517.514 + 835.4163 + + + + + + + + + + c9785b8e-2f30-4f90-8ee3-cca710f82402 + Entwine + + + + + Flatten and combine a collection of data streams + true + true + c6f9d24c-2b1e-4276-8e76-0fd2049e3d3a + Entwine + Entwine + + + + + + 2619 + 808 + 79 + 44 + + + 2664 + 830 + + + + + + 2 + 8ec86459-bf01-4409-baee-174d0d2b13d0 + 8ec86459-bf01-4409-baee-174d0d2b13d0 + 1 + 8ec86459-bf01-4409-baee-174d0d2b13d0 + + + + + 2 + Data to entwine + f1b60494-3bf8-4181-99bd-69acd3c3059a + false + Branch {0;0} + {0;0} + true + f11ad7e4-429f-4eac-92db-ff79b89a4fa3 + 1 + + + + + + 2621 + 810 + 28 + 20 + + + 2636.5 + 820 + + + + + + + + 2 + Data to entwine + 56b1c6dd-1a2e-4764-8c90-316095f2007a + false + Branch {0;1} + {0;1} + true + 5b8e4ee4-cb19-450a-ba61-866a3686f2b2 + 1 + + + + + + 2621 + 830 + 28 + 20 + + + 2636.5 + 840 + + + + + + + + Entwined result + d5779e9d-d95c-4279-bd0c-9036111f46eb + Result + R + false + 0 + + + + + + 2679 + 810 + 17 + 40 + + + 2687.5 + 830 + + + + + + + + + + + + + + 410755b1-224a-4c1e-a407-bf32fb45ea7e + 00000000-0000-0000-0000-000000000000 + GhPython Script + + + + + from compas_rhino.conversions import surface_to_rhino +from compas_timber._fabrication import DovetailTenon +from compas_timber._fabrication import DovetailTenonParams +from compas_timber._fabrication import DovetailMortise +from compas_timber._fabrication import DovetailMortiseParams + +from compas_timber.connections import Joint +from compas_timber.connections.utilities import beam_ref_side_incidence, beam_ref_side_incidence_with_vector + +from compas_timber.model import TimberModel +from compas_timber.fabrication import BTLx + +from compas.geometry import Point, intersection_line_line, angle_vectors, angle_vectors_signed + +from compas_rhino import unload_modules +from compas.scene import SceneObject + +import math + +from compas_rhino.conversions import frame_to_rhino, plane_to_rhino, vector_to_rhino, surface_to_rhino, polyline_to_rhino, brep_to_rhino, point_to_rhino, surface_to_rhino + +import Rhino.Geometry as rg +unload_modules("compas_timber") + +geo_tenon = main_beam.compute_geometry(False) +geo_mortise = cross_beam.compute_geometry(False) + +#define ref_sides +cross_ref_side_dict = beam_ref_side_incidence(main_beam, cross_beam, ignore_ends=True) +cross_ref_side_index = min(cross_ref_side_dict, key=cross_ref_side_dict.get) +print(cross_ref_side_index, "cross") +cross_ref_side_normal = cross_beam.ref_sides[cross_ref_side_index].normal +main_ref_side_dict = beam_ref_side_incidence_with_vector(main_beam, cross_ref_side_normal, ignore_ends=True) +main_ref_side_index = min(main_ref_side_dict, key=main_ref_side_dict.get) +print(main_ref_side_index, "main") + +ref_side_main = frame_to_rhino(main_beam.ref_sides[main_ref_side_index]) +ref_side_cross = frame_to_rhino(cross_beam.ref_sides[cross_ref_side_index]) + +#create dovetail_tenon +DovetailTenon.define_dovetail_tool(15.0, 60.034, 28.0) +dovetail_tenon = DovetailTenon.from_plane_and_beam( + cross_beam.ref_sides[cross_ref_side_index], + main_beam, + start_y=start_y, + rotation=rotation, + start_depth=start_depth, + width=width, + length=length, + cone_angle=cone_angle, + ref_side_index=main_ref_side_index, + ) + +#cutting_frame +cutting_plane_tenon= dovetail_tenon.frame_from_params_and_beam(main_beam) +#tenon cutting planes +dovetail_tenon_planes = dovetail_tenon.dovetail_cutting_planes_from_params_and_beam(main_beam) +#tenon volume +tenon_volume = dovetail_tenon.dovetail_volume_from_params_and_beam(main_beam) + + +dovetail_mortise = DovetailMortise.from_frame_and_beam( + frame=cutting_plane_tenon, + beam=cross_beam, + start_depth=0.0, + angle=rotation, + length=dovetail_tenon.length, + width=dovetail_tenon.width, + depth=dovetail_tenon.height, + cone_angle=dovetail_tenon.cone_angle, + flank_angle=dovetail_tenon.flank_angle, + shape=dovetail_tenon.shape, + shape_radius=dovetail_tenon.shape_radius, + ref_side_index=cross_ref_side_index, + ) +#cutting frame +cutting_plane_mortise = dovetail_mortise.frame_from_params_and_beam(cross_beam) +#mortise cutting planes +dovetail_mortise_planes = dovetail_mortise.dovetail_cutting_planes_from_params_and_beam(cross_beam) +#mortise volue +mortise_volume = dovetail_mortise.dovetail_volume_from_params_and_beam(cross_beam) + +#get btlx params +dovetail_mortise_params = DovetailMortiseParams(dovetail_mortise).as_dict() +mortise_btlx_params = [] +for key, value in dovetail_mortise_params.items(): + mortise_btlx_params.append("{0}: {1}".format(key, value)) + +#get btlx params +dovetail_tenon_params = DovetailTenonParams(dovetail_tenon).as_dict() +tenon_btlx_params = [] +for key, value in dovetail_tenon_params.items(): + tenon_btlx_params.append("{0}: {1}".format(key, value)) + + +#vizualize in rhino + +cutting_plane_tenon = frame_to_rhino(cutting_plane_tenon) +cutting_plane_mortise = frame_to_rhino(cutting_plane_mortise) + +tenon_planes = [frame_to_rhino(plane) for plane in dovetail_tenon_planes] +mortise_planes = [frame_to_rhino(plane) for plane in dovetail_mortise_planes] + +geo_tenon = brep_to_rhino(dovetail_tenon.apply(geo_tenon, main_beam)) +geo_mortise = brep_to_rhino(dovetail_mortise.apply(geo_mortise, cross_beam)) + +#mortise = brep_to_rhino(mortise_volume) + + GhPython provides a Python script component + + 178 + 222 + + + 1586 + 781 + + true + false + false + 071ab375-9118-4d58-aba2-a9de6de2f088 + false + true + GhPython Script + Python + + + + + + 1636 + 76 + 214 + 204 + + + 1717 + 178 + + + + + + 8 + 84fa917c-1ed8-4db3-8be1-7bdc4a6495a2 + 84fa917c-1ed8-4db3-8be1-7bdc4a6495a2 + 84fa917c-1ed8-4db3-8be1-7bdc4a6495a2 + 84fa917c-1ed8-4db3-8be1-7bdc4a6495a2 + 84fa917c-1ed8-4db3-8be1-7bdc4a6495a2 + 84fa917c-1ed8-4db3-8be1-7bdc4a6495a2 + 84fa917c-1ed8-4db3-8be1-7bdc4a6495a2 + 84fa917c-1ed8-4db3-8be1-7bdc4a6495a2 + 10 + 3ede854e-c753-40eb-84cb-b48008f14fd4 + 8ec86459-bf01-4409-baee-174d0d2b13d0 + 8ec86459-bf01-4409-baee-174d0d2b13d0 + 8ec86459-bf01-4409-baee-174d0d2b13d0 + 8ec86459-bf01-4409-baee-174d0d2b13d0 + 8ec86459-bf01-4409-baee-174d0d2b13d0 + 8ec86459-bf01-4409-baee-174d0d2b13d0 + 8ec86459-bf01-4409-baee-174d0d2b13d0 + 8ec86459-bf01-4409-baee-174d0d2b13d0 + 8ec86459-bf01-4409-baee-174d0d2b13d0 + + + + + true + Script variable Python + e17658fc-3064-4e83-9c8d-2ca7d71257bd + cross_beam + cross_beam + true + 0 + true + 11e2a181-d4ed-41f3-a20a-fdcafcd837a0 + 1 + 87f87f55-5b71-41f4-8aea-21d494016f81 + + + + + + 1638 + 78 + 64 + 25 + + + 1671.5 + 90.5 + + + + + + + + true + Script input main_beam. + c5907a16-b1b8-4c71-bdc0-ccca293ad70f + main_beam + main_beam + true + 0 + true + 90fae02f-7750-462d-828f-cf5f2a0d39ae + 1 + 87f87f55-5b71-41f4-8aea-21d494016f81 + + + + + + 1638 + 103 + 64 + 25 + + + 1671.5 + 115.5 + + + + + + + + true + Script input start_depth. + 004aac7d-efce-4814-98f9-6d6581b11a4a + start_depth + start_depth + true + 0 + true + c3c1dc6d-7ceb-4d7f-add6-fa924b4cc936 + 1 + 87f87f55-5b71-41f4-8aea-21d494016f81 + + + + + + 1638 + 128 + 64 + 25 + + + 1671.5 + 140.5 + + + + + + + + true + Script input width. + b76082c7-47ee-49e6-90a5-7fed960a32bb + width + width + true + 0 + true + ab977350-6a8d-472b-9276-52a400da755b + 1 + 87f87f55-5b71-41f4-8aea-21d494016f81 + + + + + + 1638 + 153 + 64 + 25 + + + 1671.5 + 165.5 + + + + + + + + true + Script input length. + 3e9be09b-b9b1-46ae-88bd-2a68cff46e13 + length + length + true + 0 + true + 244dd1d8-93d4-4afc-851d-064a07e08680 + 1 + 87f87f55-5b71-41f4-8aea-21d494016f81 + + + + + + 1638 + 178 + 64 + 25 + + + 1671.5 + 190.5 + + + + + + + + true + Script input cone_angle. + 77eebdf9-6c87-4e51-b360-dd5f773e8cd2 + cone_angle + cone_angle + true + 0 + true + 1f382d1c-8a89-4e8a-8102-597801cd81a3 + 1 + 87f87f55-5b71-41f4-8aea-21d494016f81 + + + + + + 1638 + 203 + 64 + 25 + + + 1671.5 + 215.5 + + + + + + + + true + Script input start_y. + 05be9a7a-6224-49b3-bcf0-791c623f7f77 + start_y + start_y + true + 0 + true + 89a39aa2-89d0-4ad6-9043-6ef61b51023c + 1 + 87f87f55-5b71-41f4-8aea-21d494016f81 + + + + + + 1638 + 228 + 64 + 25 + + + 1671.5 + 240.5 + + + + + + + + true + Script input rotation. + 09a29b2e-a2b5-4147-81d7-21fa818dc6cc + rotation + rotation + true + 0 + true + 5a7f3b31-fffa-478e-b8ef-7eb827144b57 + 1 + 87f87f55-5b71-41f4-8aea-21d494016f81 + + + + + + 1638 + 253 + 64 + 25 + + + 1671.5 + 265.5 + + + + + + + + The execution information, as output and error streams + fb966008-8d47-4262-8c09-ce71985833b0 + out + out + false + 0 + + + + + + 1732 + 78 + 116 + 20 + + + 1790 + 88 + + + + + + + + Script output cutting_plane_tenon. + 213adbf5-0592-48ab-b06a-06543b55b8da + cutting_plane_tenon + cutting_plane_tenon + false + 0 + + + + + + 1732 + 98 + 116 + 20 + + + 1790 + 108 + + + + + + + + Script output cutting_plane_mortise. + cc5eda18-08de-4b0d-91bd-9eefbfcc5dc7 + cutting_plane_mortise + cutting_plane_mortise + false + 0 + + + + + + 1732 + 118 + 116 + 20 + + + 1790 + 128 + + + + + + + + Script output ref_side_main. + d7c2e387-1413-47f3-96a4-1fd6a08f1e62 + ref_side_main + ref_side_main + false + 0 + + + + + + 1732 + 138 + 116 + 20 + + + 1790 + 148 + + + + + + + + Script output ref_side_cross. + a8110476-8bb3-4892-9423-09704614c7a3 + ref_side_cross + ref_side_cross + false + 0 + + + + + + 1732 + 158 + 116 + 20 + + + 1790 + 168 + + + + + + + + Script output mortise_btlx_params. + 83bdcfa8-afbb-420d-9faa-98a158aee68d + mortise_btlx_params + mortise_btlx_params + false + 0 + + + + + + 1732 + 178 + 116 + 20 + + + 1790 + 188 + + + + + + + + Script output tenon_btlx_params. + 6d4fe408-c656-48f6-88c9-67f2fbcdb1df + tenon_btlx_params + tenon_btlx_params + false + 0 + + + + + + 1732 + 198 + 116 + 20 + + + 1790 + 208 + + + + + + + + Script output geo_tenon. + be1a6a99-5e4d-460c-a8f0-e8312a661132 + geo_tenon + geo_tenon + false + 0 + + + + + + 1732 + 218 + 116 + 20 + + + 1790 + 228 + + + + + + + + Script output geo_mortise. + 389410e6-fbd0-4b13-ad00-528c4c2075fd + geo_mortise + geo_mortise + false + 0 + + + + + + 1732 + 238 + 116 + 20 + + + 1790 + 248 + + + + + + + + Script output mortise. + c5450653-c419-4dd1-ba43-0725a7758699 + mortise + mortise + false + 0 + + + + + + 1732 + 258 + 116 + 20 + + + 1790 + 268 + + + + + + + + + + + + + + 4f8984c4-7c7a-4d69-b0a2-183cbb330d20 + Plane + + + + + Contains a collection of three-dimensional axis-systems + f1db2a34-33d7-489c-af3c-6723cd9cafed + Plane + Pln + false + d7c2e387-1413-47f3-96a4-1fd6a08f1e62 + 1 + + + + + + 1882 + 143 + 50 + 24 + + + 1907.574 + 155.2027 + + + + + + + + + + d3d195ea-2d59-4ffa-90b1-8b7ff3369f69 + Unit Y + + + + + Unit vector parallel to the world {y} axis. + true + 780dd7ff-1451-4d20-b77b-f1c386f8a9ed + Unit Y + Y + + + + + + -202 + 675 + 63 + 28 + + + -173 + 689 + + + + + + Unit multiplication + f46970bc-a15b-434c-931c-2ba17d942c80 + Factor + F + false + 27b7a426-ec72-4334-8c70-78c50fb834e2 + 1 + + + + + + -200 + 677 + 12 + 24 + + + -192.5 + 689 + + + + + + 1 + + + + + 1 + {0} + + + + + 1 + + + + + + + + + + + World {y} vector + 7a214201-5ed9-46c9-afe6-bf48e768f870 + Unit vector + V + false + 0 + + + + + + -158 + 677 + 17 + 24 + + + -149.5 + 689 + + + + + + + + + + + + 57da07bd-ecab-415d-9d86-af36d7073abc + Number Slider + + + + + Numeric slider for single values + 27b7a426-ec72-4334-8c70-78c50fb834e2 + Number Slider + + false + 0 + + + + + + -391 + 680 + 163 + 20 + + + -390.1428 + 680.1044 + + + + + + 3 + 1 + 1 + 4000 + 0 + 0 + 0 + + + + + + + + + b6236720-8d88-4289-93c3-ac4c99f9b97b + Relay + + + + + 2 + A wire relay object + 11e2a181-d4ed-41f3-a20a-fdcafcd837a0 + Relay + + false + 3984d0df-294e-462f-b103-b87bb94e3021 + 1 + + + + + + 1532 + 78 + 40 + 16 + + + 1552 + 86 + + + + + + + + + + 57da07bd-ecab-415d-9d86-af36d7073abc + Number Slider + + + + + Numeric slider for single values + 244dd1d8-93d4-4afc-851d-064a07e08680 + Number Slider + length + false + 0 + + + + + + 1405 + 180 + 188 + 20 + + + 1405.231 + 180.6459 + + + + + + 3 + 1 + 1 + 200 + 0 + 0 + 60 + + + + + + + + + 59e0b89a-e487-49f8-bab8-b5bab16be14c + Panel + + + + + A panel for custom notes and text values + 2b407bc7-5cf8-4769-888d-66de2e16c241 + Panel + + false + 1 + fb966008-8d47-4262-8c09-ce71985833b0 + 1 + Double click to edit panel content… + + + + + + 1350 + -64 + 513 + 121 + + 0 + 0 + 0 + + 1350.064 + -63.0608 + + + + + + + 255;255;250;90 + + true + true + true + false + false + true + + + + + + + + + 0148a65d-6f42-414a-9db7-9a9b2eb78437 + Brep Edges + + + + + Extract the edge curves of a brep. + true + 1771f48a-32bc-4ec4-a491-a17a3372bf88 + Brep Edges + Edges + + + + + + 1207 + 428 + 72 + 64 + + + 1237 + 460 + + + + + + Base Brep + 12303237-bf2d-4c37-8f21-77c88e2212c3 + Brep + B + false + b0a6568e-06d8-4c68-b393-d8274f661640 + 1 + + + + + + 1209 + 430 + 13 + 60 + + + 1217 + 460 + + + + + + + + 1 + Naked edge curves + 9d38d3ad-c615-4e0c-9887-f61b66e89609 + Naked + En + false + 0 + + + + + + 1252 + 430 + 25 + 20 + + + 1264.5 + 440 + + + + + + + + 1 + Interior edge curves + 4669d49b-b906-42b2-9f31-ddced1ed687a + Interior + Ei + false + 0 + + + + + + 1252 + 450 + 25 + 20 + + + 1264.5 + 460 + + + + + + + + 1 + Non-Manifold edge curves + 9df9e457-8774-4250-9633-480660cd74b1 + Non-Manifold + Em + false + 0 + + + + + + 1252 + 470 + 25 + 20 + + + 1264.5 + 480 + + + + + + + + + + + + 919e146f-30ae-4aae-be34-4d72f555e7da + Brep + + + + + Contains a collection of Breps (Boundary REPresentations) + 5c4473da-8ccb-4a9f-8e42-e0447b1eaa1e + Brep + Brep + false + 0 + + + + + + 1881 + 203 + 50 + 24 + + + 1906.776 + 215.0259 + + + + + + + + + + 0148a65d-6f42-414a-9db7-9a9b2eb78437 + Brep Edges + + + + + Extract the edge curves of a brep. + true + 4e2a1cbf-d18f-4fe9-8794-b8b9b5231f50 + Brep Edges + Edges + + + + + + 1206 + 588 + 72 + 64 + + + 1236 + 620 + + + + + + Base Brep + 500ce405-e491-465b-930c-ffb6ef47822c + Brep + B + false + 55c66290-a177-4518-824a-6a531a1e8086 + 1 + + + + + + 1208 + 590 + 13 + 60 + + + 1216 + 620 + + + + + + + + 1 + Naked edge curves + c03958c4-925a-40e8-b29d-7363a45c43bd + Naked + En + false + 0 + + + + + + 1251 + 590 + 25 + 20 + + + 1263.5 + 600 + + + + + + + + 1 + Interior edge curves + 3d9ed233-4f5c-4cc4-b393-8cc7cf7457fe + Interior + Ei + false + 0 + + + + + + 1251 + 610 + 25 + 20 + + + 1263.5 + 620 + + + + + + + + 1 + Non-Manifold edge curves + 37f9aefb-3c7e-4731-b6c7-692870e8c6ec + Non-Manifold + Em + false + 0 + + + + + + 1251 + 630 + 25 + 20 + + + 1263.5 + 640 + + + + + + + + + + + + f31d8d7a-7536-4ac8-9c96-fde6ecda4d0a + Cluster + + + + + + 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 + + Contains a cluster of Grasshopper components + true + bc365232-0216-46dd-a05a-493450f8e675 + Cluster + Cluster + false + + + + + 1 + 6f220e32-ede9-4588-85dd-6f6d0925be60 + 2c214118-98c8-4e19-a17e-3d29b171a059 + + + + + + 1969 + 216 + 60 + 28 + + + 2015 + 230 + + + + + + 1 + 919e146f-30ae-4aae-be34-4d72f555e7da + 0 + + + + + Contains a collection of Breps (Boundary REPresentations) + true + 6f220e32-ede9-4588-85dd-6f6d0925be60 + Brep + Brep + true + 5c4473da-8ccb-4a9f-8e42-e0447b1eaa1e + 1 + + + + + + 1971 + 218 + 29 + 24 + + + 1987 + 230 + + + + + + + + + + + + + + 59e0b89a-e487-49f8-bab8-b5bab16be14c + Panel + + + + + A panel for custom notes and text values + f2af4f17-9955-4d61-93cd-76e0d692f241 + Panel + BTLx Params + false + 0 + 83bdcfa8-afbb-420d-9faa-98a158aee68d + 1 + Double click to edit panel content… + + + + + + 2239 + -241 + 206 + 333 + + 0 + 0 + 0 + + 2239.95 + -240.1865 + + + + + + + 255;255;255;255 + + true + true + true + false + false + true + + + + + + + + + c552a431-af5b-46a9-a8a4-0fcbc27ef596 + Group + + + + + 1 + + 150;255;56;56 + + A group of Grasshopper objects + f2af4f17-9955-4d61-93cd-76e0d692f241 + 1 + 02293eca-caf6-4507-8376-6b1f1bbf6472 + Group + + + + + + + + + + + 919e146f-30ae-4aae-be34-4d72f555e7da + Brep + + + + + Contains a collection of Breps (Boundary REPresentations) + 17656a19-e22f-448a-b26f-e7d025b96e6f + Brep + Brep + false + be1a6a99-5e4d-460c-a8f0-e8312a661132 + 1 + + + + + + 1880 + 250 + 50 + 24 + + + 1905.77 + 262.0995 + + + + + + + + + + d5967b9f-e8ee-436b-a8ad-29fdcecf32d5 + Curve + + + + + Contains a collection of generic curves + true + 2ace07a8-3cff-4c52-b37f-cd3e591aeddd + Curve + Crv + false + b37fb167-edb3-4a28-b8fd-a498a7811210 + 1 + + + + + + 320 + 595 + 50 + 24 + + + 345.5917 + 607.4064 + + + + + + 1 + + + + + 1 + {0} + + + + + -1 + + Y2BkYGD4DwQgGgR4mIBEeFBGZl6+c35ubn6ejkJYalFxZn6erbmesYmekbGRmbmeoaGBgaGOgnNpTklpUaptXmppSVFijo5CQGlSTmayd2plSH52ap6tqamRkYVhqqV5srmpqamxASvIFhmw4Xruqfm5qSVFlXoB+TmVOZl5qc6lRWWpLEAF7GUQC7kSi5IzMstSjVNyOfMLUvPySouSillSEksSQYo4ODiYQG4VUGdgMALS9f/4eDiZgQx+EDEViJl+1TMxdEL99fs/E4MIlP3synU/95eXBfc/ZRBoZlT64Ddn9+0/QPlAqLwAyNy4bxpcrDPvOXz4/18+rHjlAagcQwpU3KHb70cS13q4OBMDAuxtjlpjZzvZAeSS6oIuv//1TA0wOW6GwQQA + + 00000000-0000-0000-0000-000000000000 + + + + + + + + + + + + + c552a431-af5b-46a9-a8a4-0fcbc27ef596 + Group + + + + + 1 + + 150;255;255;255 + + A group of Grasshopper objects + 2ace07a8-3cff-4c52-b37f-cd3e591aeddd + ca0ea429-34fb-4ce1-a30e-2c14ddc1552e + 2 + e0450338-f168-4770-aeb4-8e3b1b320b54 + Group + + + + + + + + + + + 4f8984c4-7c7a-4d69-b0a2-183cbb330d20 + Plane + + + + + Contains a collection of three-dimensional axis-systems + 405d1b89-3c83-4839-aa6a-7e9ca46c1aad + Plane + Pln + false + 213adbf5-0592-48ab-b06a-06543b55b8da + 1 + + + + + + 1882 + 54 + 50 + 24 + + + 1907.971 + 66.13251 + + + + + + 1 + + + + + 1 + {0;0} + + + + + + 31287.423274001 + -3231.69101859564 + 48.2812000254933 + -0.259942858411819 + -0.929978182927627 + 0.259942858411848 + 0.963085199245858 + -0.269196766313353 + -1.70731025626947E-15 + + + + + + + + + + + + + + 2a3f7078-2e25-4dd4-96f7-0efb491bd61c + Vector Display + + + + + false + 0 + Preview vectors in the viewport + 0.1 + 15 + 46f3ebed-d260-45ff-81a6-62a31526fd14 + Vector Display + VDis + + + + + 3 + false + false + + + + + + 255;255;0;0 + + + 255;255;0;0 + + 0 + d0118fff-b195-4eeb-a27a-c2272bc6d756 + + + + + + 255;255;165;0 + + + 255;255;165;0 + + 0.5 + fac92592-5639-4b42-ba35-fbddafa8c63c + + + + + + 255;124;252;0 + + + 255;124;252;0 + + 1 + 6cf11a22-adb8-4caf-8bcb-23b1fa704b5d + + + + + + + + 794 + 464 + 61 + 44 + + + 841 + 486 + + + + + + Anchor point for preview vector + 5df1f47b-dea7-432b-bc6a-2040fee0fa83 + Anchor + A + true + 7542c648-8889-4d83-8f10-9e8b260fa85a + 1 + + + + + + 796 + 466 + 30 + 20 + + + 820.5 + 476 + + + + + + + + Vector to preview + 904658e2-ff3e-49c1-9c42-4f56b7d4805b + x*75 + Vector + V + true + 4c90d06e-9810-41a6-baa8-91b753ccec55 + 1 + + + + + + 796 + 486 + 30 + 20 + + + 820.5 + 496 + + + + + + + + + + + + 2a5cfb31-028a-4b34-b4e1-9b20ae15312e + Cross Product + + + + + Compute vector cross product. + true + 4dfed18b-2ae2-4401-932a-d35c5687b48f + Cross Product + XProd + + + + + + 672 + 510 + 66 + 64 + + + 704 + 542 + + + + + + First vector + 20c07560-21cb-462a-913e-33c8dfba4f1a + Vector A + A + false + 36c4deda-ceed-4920-a3f3-2d18ca613b35 + 1 + + + + + + 674 + 512 + 15 + 20 + + + 683 + 522 + + + + + + + + Second vector + d792952b-8d14-4258-93a8-f657e9d4e328 + Vector B + B + false + d2d51328-18f6-4ef3-8dd6-1df6b6af922e + 1 + + + + + + 674 + 532 + 15 + 20 + + + 683 + 542 + + + + + + + + Unitize output + ec1aee98-3178-4b5f-8943-45ff0468d2ad + Unitize + U + false + 0 + + + + + + 674 + 552 + 15 + 20 + + + 683 + 562 + + + + + + 1 + + + + + 1 + {0} + + + + + false + + + + + + + + + + + Cross product vector + 4c90d06e-9810-41a6-baa8-91b753ccec55 + Vector + V + false + 0 + + + + + + 719 + 512 + 17 + 30 + + + 727.5 + 527 + + + + + + + + Vector length + 6cfe60aa-eede-4d54-9b2b-e48c3287be06 + Length + L + false + 0 + + + + + + 719 + 542 + 17 + 30 + + + 727.5 + 557 + + + + + + + + + + + + 9103c240-a6a9-4223-9b42-dbd19bf38e2b + Unit Z + + + + + Unit vector parallel to the world {z} axis. + true + e98b3a90-fae4-407f-a38e-3f9d806bc67d + Unit Z + Z + + + + + + 313 + 412 + 63 + 28 + + + 342 + 426 + + + + + + Unit multiplication + 99a90f24-1411-4423-854c-1d9440888867 + Factor + F + false + 0 + + + + + + 315 + 414 + 12 + 24 + + + 322.5 + 426 + + + + + + 1 + + + + + 1 + {0} + + + + + 1 + + + + + + + + + + + World {z} vector + 3fa223fc-b370-43ff-9286-d236dfc1155a + Unit vector + V + false + 0 + + + + + + 357 + 414 + 17 + 24 + + + 365.5 + 426 + + + + + + + + + + + + 410755b1-224a-4c1e-a407-bf32fb45ea7e + 00000000-0000-0000-0000-000000000000 + GhPython Script + + + + + import Rhino.Geometry as rg +import math +import copy + +def rotate_vector(vector, angle, axis, point): + angle_rad = math.radians(angle) + vec_trans = rg.Transform.Rotation(angle_rad, axis.Direction, point) + rotated_vector = rg.Vector3d(vector) + rotated_vector.Transform(vec_trans) + return rotated_vector + +def sort_list(lst, key=None, reverse=False): + return sorted(lst, key=key, reverse=reverse) + + +def new_shifted_lines(main_centerline, cross_centerline, normal_cross): + + param = rg.Intersect.Intersection.LineLine(main_centerline, cross_centerline)[2] + int_point = cross_centerline.PointAt(param) + + ### Get normal vectors in all directions + all_vectors = [] + for i in range(4): + normal = rotate_vector(normal_cross, i*90, cross_centerline, int_point) + all_vectors.append(normal) + + ### Get angles between main centerline and all vectors + vec_angles = [] + for vec in all_vectors: + plane = rg.Plane(int_point, all_vectors[0], all_vectors[1]) + vec_angle = rg.Vector3d.VectorAngle(main_centerline.Direction, vec, plane) + vec_angles.append(vec_angle) + + ### Sort vectors based on angles + fisrt_vec_angle_pairs = list(zip(all_vectors, vec_angles)) + first_sorted_vec_angle_pairs = sort_list(fisrt_vec_angle_pairs, key=lambda x: x[1]) + + sorted_angles = [abs((math.pi/2)-sg) for sg in vec_angles] + second_vec_angle_pairs = list(zip(all_vectors, sorted_angles)) + second_sorted_vec_angle_pairs = sort_list(second_vec_angle_pairs, key=lambda x: x[1], reverse=True ) + + + ### if the angle between the main centerline and the normal vector of the cross centerline is greater than 45 degrees, shift the main centerline to the right + if min(vec_angles) > math.pi/4: + x = (math.pi*2) - max(vec_angles) + length = abs((((beam_width)/2)/math.cos(x)+(math.tan(x)*((beam_width)/2)))-((beam_width)/2)) + print ("offset_length: ", length) + second_sorted_vectors = [pair[0] for pair in second_sorted_vec_angle_pairs] + selected_products = [] + for i, vecs in enumerate(second_sorted_vectors): + if i%2 == 1: + selected_products.append(rg.Vector3d.Multiply(main_centerline.Direction, vecs)) + index = selected_products.index(min(selected_products)) + if index == 0: + shift_vector = second_sorted_vectors[1] + else: + shift_vector = second_sorted_vectors[3] + shift_vector.Unitize() + shift_length = (length) * shift_vector + s_pt_to_move = copy.deepcopy(main_centerline.From) + new_start_point = s_pt_to_move + shift_length + new_line = rg.Line(new_start_point, main_centerline.To) + + ### if the angle between the main centerline and the normal vector of the cross centerline is less than 45 degrees, shift the main centerline to the left + else: + min_vec_angle = abs(min(vec_angles)) + if min_vec_angle > 1: + x = min(vec_angles) % (2 * math.pi) + else: + x = min(vec_angles) + length = abs((((beam_width)/2)/math.cos(x)+(math.tan(x)*((beam_width)/2)))-((beam_width)/2)) + print ("offset_length: ", length) + first_sorted_vectors = [pair[0] for pair in first_sorted_vec_angle_pairs] + selected_products = [] + for i, vecs in enumerate(first_sorted_vectors): + if i%2 == 1: + selected_products.append(rg.Vector3d.Multiply(main_centerline.Direction, vecs)) + index = selected_products.index(min(selected_products)) + if index == 0: + shift_vector = first_sorted_vectors[1] + else: + shift_vector = first_sorted_vectors[3] + shift_vector.Unitize() + shift_length = (length) * shift_vector + s_pt_to_move = copy.deepcopy(main_centerline.From) + new_start_point = s_pt_to_move + shift_length + new_line = rg.Line(new_start_point, main_centerline.To) + + print(length) + return new_line, shift_vector, length + +eccentricity_line, shift_vector, length = new_shifted_lines(main_centerline, cross_centerline, normal_cross) + + +e = rg.Vector3d.VectorAngle(-new_line.Direction, main_centerline.Direction) +error_length = ((math.sqrt(2) * 60) * math.sin(e)) + GhPython provides a Python script component + + 94 + 147 + + + 1199 + 815 + + true + true + false + false + 667cfcd7-2db6-4d00-960b-e6e200fc891b + false + true + true + GhPython Script + Python + + + + + + 1008 + 698 + 205 + 84 + + + 1109 + 740 + + + + + + 4 + 84fa917c-1ed8-4db3-8be1-7bdc4a6495a2 + 84fa917c-1ed8-4db3-8be1-7bdc4a6495a2 + 84fa917c-1ed8-4db3-8be1-7bdc4a6495a2 + 84fa917c-1ed8-4db3-8be1-7bdc4a6495a2 + 4 + 3ede854e-c753-40eb-84cb-b48008f14fd4 + 8ec86459-bf01-4409-baee-174d0d2b13d0 + 8ec86459-bf01-4409-baee-174d0d2b13d0 + 8ec86459-bf01-4409-baee-174d0d2b13d0 + + + + + true + Script variable Python + e26f6eb2-85f0-477b-ad84-3f5ba206efcf + true + main_centerline + main_centerline + true + 0 + true + 09fe95fb-9551-4690-abf6-4a0665002914 + 1 + f802a8cd-e699-4a94-97ea-83b5406271de + + + + + + 1010 + 700 + 84 + 20 + + + 1053.5 + 710 + + + + + + + + true + Script input cross_centerline. + 461e1986-9089-47c0-a179-1d0f268214e4 + true + cross_centerline + cross_centerline + true + 0 + true + 36c4deda-ceed-4920-a3f3-2d18ca613b35 + 1 + f802a8cd-e699-4a94-97ea-83b5406271de + + + + + + 1010 + 720 + 84 + 20 + + + 1053.5 + 730 + + + + + + + + true + Script input normal_cross. + 37b9bbe5-61ab-4d4e-94f8-d7521a2b14e7 + true + normal_cross + normal_cross + true + 0 + true + 0 + 15a50725-e3d3-4075-9f7c-142ba5f40747 + + + + + + 1010 + 740 + 84 + 20 + + + 1053.5 + 750 + + + + + + + + true + Script input beam_width. + 6c170075-c66b-47d4-8b87-88b886f172d5 + true + beam_width + beam_width + true + 0 + true + 478fc3b1-c4f8-4087-8fe6-c2905e21be9a + 1 + 87f87f55-5b71-41f4-8aea-21d494016f81 + + + + + + 1010 + 760 + 84 + 20 + + + 1053.5 + 770 + + + + + + + + The execution information, as output and error streams + 4e45fb34-2889-49d1-bf07-b5da2de1bbaa + true + out + out + false + 0 + + + + + + 1124 + 700 + 87 + 20 + + + 1167.5 + 710 + + + + + + + + Script output eccentricity_line. + 0aaaefae-ce6a-46bd-a098-afb849f637c9 + true + eccentricity_line + eccentricity_line + false + 0 + + + + + + 1124 + 720 + 87 + 20 + + + 1167.5 + 730 + + + + + + + + Script output shift_vector. + 4f86109c-f46b-4b49-9ac2-c179dadcae58 + true + shift_vector + shift_vector + false + 0 + + + + + + 1124 + 740 + 87 + 20 + + + 1167.5 + 750 + + + + + + + + Script output length. + 551bea16-0f41-4616-9a4f-418db04bc354 + true + length + length + false + 0 + + + + + + 1124 + 760 + 87 + 20 + + + 1167.5 + 770 + + + + + + + + + + + + + + 59e0b89a-e487-49f8-bab8-b5bab16be14c + Panel + + + + + A panel for custom notes and text values + be283d6e-d4d9-46e1-9a9d-f0f142489cc4 + Panel + Tenon BTLx Params + false + 0 + 6d4fe408-c656-48f6-88c9-67f2fbcdb1df + 1 + Double click to edit panel content… + + + + + + 2245 + 124 + 206 + 333 + + 0 + 0 + 0 + + 2245.078 + 124.5497 + + + + + + + 255;255;255;255 + + true + true + true + false + false + true + + + + + + + + + c552a431-af5b-46a9-a8a4-0fcbc27ef596 + Group + + + + + 1 + + 150;255;56;56 + + A group of Grasshopper objects + be283d6e-d4d9-46e1-9a9d-f0f142489cc4 + 1 + 87fa8c87-c926-4271-b108-197a068dafee + Group + + + + + + + + + + + 57da07bd-ecab-415d-9d86-af36d7073abc + Number Slider + + + + + Numeric slider for single values + bbcbfdee-5b0e-4c92-bf29-b5811037ea03 + Number Slider + width + false + 0 + + + + + + 2050 + 782 + 160 + 20 + + + 2050.36 + 782.0133 + + + + + + 3 + 1 + 1 + 200 + 0 + 0 + 25 + + + + + + + + + 57da07bd-ecab-415d-9d86-af36d7073abc + Number Slider + + + + + Numeric slider for single values + 3ee8dc00-1eea-48ba-bc11-c2d1911c58a5 + Number Slider + cone_angle + false + 0 + + + + + + 1970 + 807 + 240 + 20 + + + 1970.97 + 807.3304 + + + + + + 3 + 1 + 1 + 100 + 0 + 0 + 10 + + + + + + + + + 57da07bd-ecab-415d-9d86-af36d7073abc + Number Slider + + + + + Numeric slider for single values + b29d9bba-e05a-4fa4-91a3-bf1ac143a41d + Number Slider + length + false + 0 + + + + + + 2039 + 756 + 171 + 20 + + + 2039.322 + 756.2523 + + + + + + 3 + 1 + 1 + 200 + 0 + 0 + 60 + + + + + + + + + f31d8d7a-7536-4ac8-9c96-fde6ecda4d0a + Cluster + + + + + + 7V0HWFNX+79AZE8ndfW6wYl++jlaNeMGDDJSwL2I5ALRkMQMBK0aRa1WS90LrSiKow5UnFhEnHWiFutscVXc2Kqt9lP+597cRHJHSMxF+L5/eR4eHu7NvTnn9+73vO85rogyTpeEKrTl4McBgiAO+PVWyXUJMsWoZFStkSkV2C0xuIzdxn5csI8Yn+uHSqSoGvtILeK2u/GWCMEuu4FLa5QQv/Gf5/sv3DJ0/Y35Txq6iNVosgwdj913B/edoxPBW6Q+xOVwVJMYk6pCsdtOxBd7EfcilOokiRy70xL/tiWmp6JRORqnRaWme0sgaV0EjZcpZFowC7FaqULVWhmqMb4W++UgEi3+Pa7gn++eJaZzd99w9UBQTZxaptISk8eGCHEiJEmo8T8/LarRjpIqk1GtRCbvmJCY4hYFBoGhpTEiif14Ga8KlDoDxk5GlMB4xoDxEl/hSFx2jpGoE1D8k03Bv53A9U2/l5dzhiqVSUaQ3bU3+9YaCGZs9lVu2BXK17hFxanCJKlKnbbiZ91D1EqdivJhv/doRY7GBmf6gk7g18Nwzewp7Lqz4Tr2AkeCWJyQAe+JP+enPD/3vEaRUxPeBOrm9fQyA9JdqJDCYqVModW4CZQKgKbCwE0cghfpaNFcmKJVS+K0sDYRhVHwAhX+AlgZD0vgOJ06Ge3oKVJotBJFHBqik0mNI9nitbl/lkgctvLz7yf73YTOMI3ENUIWN7biPSdwz52n1aplo3VaAwOZCMYHeEjxSy3wC558SOwuhCA9H4KC+LXEsmQlDten+N3pfCjHQ+ihkqglSaNkCpVOa5QoB4bJ+gmwCcFaJYwmS+Q6wK60c5vfHM4K+0zE3ZR59LzT0GM6s7nVwt9BmZaDwDUS/y6DQOHc4Ryt1KnjUJx3wK9Qmb2zjU+2aMf4Qb81HIWWehhumzEB9py16NTlQyoMnVgeBMEUdP6No+NpQAfwLAGPc+XwAEDUWgMf0MKTtqfB9vYvZ4RsgqZfX8XvyTOHJxp7mgpPNBUeuvlDNsx/LTF/PZh/Jo88/518yJc0f4dK5u9jmL9JCmhnrz68+LszWzNE+x+dazn+rudMDpm5qXMXVsncAXUtzF3sKSSUiQOTMunSZ8vf0PMF/JkLxrd0579taE5HXHzN9YirBew6E5/UYGpDKZcb1DGmRrSJahTtIJUBM6bBQSBUjHM/mVSKKoyQONCi3Xj8pqTljTnCg5e2TA7dHzqCZowUvB3F2kplkczD9sqigwBSuQF69AOaqoBEjzqdGwoadXY30sORiR7nmzx68GpBePj0bUuad4h9OdZsrpxwYCCpar0WAznaxqglCo0caDg4IAk8GQhLADHw74clcqUiAZApGfyjVNOr97yEgG7a2Qj/0HDYOSK/fnfqWCiw41etxStaAHEx2Z1Jq9k1AijTNs3uxZdoUDgBVSahWnUq7ZRSz1866K2fy9/wW90/siXHgsym5BpCPEqV3hAKMzmQmInMpPYy0wABFIuBo6IV7pECAIEZOA4EOI4M4NQ2MgMmkQaq0yKU8cRneL3yEciOE/Hntw9uvMgMIedwJfY8FZ+YSoUtBklf+oyTFbJrzqveXZvxJrGAT6aHJXy4nkJvMeZwa7TAdwZuqcREOkxmKF/szAcAxSVCFSST+iGOWKJNNCn6iUGTOCItmgRB7515ZwO22Gf+BTH9tOTaZJPrmARZapm9b/2QWdistFfIt72XbvF2uDj/w9nbbuM0leDfeXT00c/A+dcmw2wAIR4LV3AelgKK0oLwr9IzS0Lyj0fmOk/q0rpua3PXxM30FioKg6sEBZxLGVHgmky0E8RgEh4NjAjjdHotWJx1JnhNpGiouRoOA5bAek+/mUCNYvZAAsvBx+HRqHY8iipg7XglYZDpLcFwzrBumqc9RVsfrb0++2mnF9QhUA1wmMJKkPRLCDvwJa0dyLbZDvhiA6rUg9XwNO73B+TyZpwY8/X0t+3vm83JA/dgYXrfwoFXqbYjC6Kd2k6/lJCmWDo+KshgtAZMEHnjEFl2cs8tc77ziac6JGfmqVdOZ70fmsuRKbqjwsOvFB6y/8wCPLiYMcIDxMwmjetpYCE0Acu70KLjN2Fo6cWFXcNzTzeZ1ELZ/LIVIuEQxrqC0W+t4CZQ4j/uDhwXQsFwmBTM6f9Ic++Lf+AtnT29x/WMh6vN/WueIkFO0jC1CPTokGstUCYBgFE8lyDBnjXTMgbrqOloleOvXTJhX1d1YvA0Qd62wtRfeDQDo4BsuGytfk4FAJUjEDSNjyUYyKpnrgAqcrBJ9XgGy9QarSX/auCB0C1nn/3CW7F8RkLT4YnnzU30QPxBmPchOoft1MJEAVSCYUMvVMCyzYNs0jle0WicEmgNC+AE13Z6vPvrRGRmx3ZbPS+OfU4LDv9DNM6214lZY6W3uEt/Dyh17tO6LwvgBDkwapypFMZxrEThNDJOAFbJJUD1AA8F7oIYBIgWqkEPFw9YendR5DIP978nF4x8RgqKsZdQcRJTwxh7PRwAhMrREhC+Trap3ua4AMMBMgWslkhlwFsLNGkQQnvQAnJXuKNRp32PBDM6bcq5N+vxK2uUBZ1U2auRFxJiMwsAUkYBBFiqEsg2xzcwCo2XoymEMrUZF98hTUSCunnI9lMba6fP/tkcF2fDu6nARFUJMEWYyDACU+BoNFWYgaE1VdxCp/mbslvwM/jXL2T8kd7OfDIDFDItPJRqq5wYkG2HP2AAD8ZIIpejciwzjNmu8Uq1XApPnDAJlqTIrDRY0448lQ8uaNb/m0Nx95sMXf073fCoWA+1VtZe8aFYD8JRLqHEvP64m0NnrZhyAnXwESXp5FqZSi6Lw+Mq2nmFXFlSN2Vy07At7SXO6Q+K7pjPK1iCAUidV3ClWvmJx4Oikk2a4ANbpsMdIycj9mrlN3xIjwHE5VEzcJCeU31JAYUuabSBH50h409ZX5v0ou8gE0NasKDkNIt5VFOB36nkGsh++NtGYKDHLFp6dKoY/joziTxnArTyatHdfquyMtbwD4YHk+I03Wh86QqMmyr3THzvHw4IhmJ5hDhgcDXvX2GVkEsuPlEf664RTR3CPVgui3RmHBB12YvXgW8tdJuAofib8EwpQbF+H/Bb3yFeYox/UC2qNnKyUdu5izD5N6Mb9k0u+GWR1Ch/Q6Qn0x0OuSEHnjYI92pXp8h434HhvkckzqoUCXA1XGd+sWE8+HCNSomJz5sYXGhscVkCvEXMEapAIVqKNFhUVBZwuihk88zMBe6DDyk5JBtvhdknp3Nz4nz4+9p8zp93Y23zklbD3ezVTJsFEPwfJmeauw1EcW+RCiBV5ks3JXxpW1Dae194aeYrb+HmpuvX/bQgYCGH5EJb4VWTUSL7XCygBNjaAkrAsyJ40MRLlnRmvShUA6wbtgJVGTqXm5wb0X/qMFF6i67rwudkx5LdJOw9H8NNggoITplFG9EfxREidKbRtlN05q7z7b14q4aELIq6NiaqfZ/NZpNxFMusd5EaRKFanRpf0YvHzTyGpVhmnTv03diiWc1nt+Qu/feuROlxmT95GDSrdjJrYUoAQGD6MZPOF9Kn4QJF9oWMNoZe7RimB7y/0ajJJ0Kl8OhUMF/a6a1b0alF4K/tRXkc9eZjt+uusM4rimCfZWSErVDRmVlIjWNRk90eixkVg7jDWK0GfSRMVtW1RQpgEQG+whSVGtVoKrzMKaVTF3MqGd5OpVIq+1SaQ1DpLzoqFaTjVCIE25VJsJeUvlzVYcDcsP2nIg+HlGc2J1XcmCZsLuCW+L6+kKiCwRaHUdML3KngtZaM1gQEpHbqEtgpTqkJSAlsFwBoAf62xS8GBnbA/1qlGdJbhTZMdJsg+rqV17Ghp989YZoHhTAV7lkbd74UQConIaQ/IKDxpAoCEKiII7Tbk1r2Vn9gzfzBoj0bNkVw77W7TvakyPc/hidV5z1WQHzUMslohlxSZMRCp+yoSN7mI+Pa/HjhWZo5NQYSj8I0aYKUSl0EXuDEU0N6OPByf31UFp4ni7c31f9KAM1zIkI8qovwRgAFcYQ2OFJWY+Tba/RU/sKSyP3KpaIU4cxRDBjRLK5SVQkZo9/KShs3KX0RvPbdT6s7ZDu8YwEjwNMWMFLVEtriRtV970a91xC0KJED+uryorjRiIFNaL2o2AG40BPK1o1J2UbsDT73I3RUoB/6yWeyh6F+ZpPxDjdLm1gffDYyCz7Nky9W6U4fh1s31h7S8FefSr32Q4Or31oaFgVr0n0bdKjeGcCZQxuNOiJQiYv9OrQGR6My4NngQZYVybKvYno/mtBru3DRkNPt5tc95MRCNCo8kdj+9pwiwaz47vv5Tsdvs6AgMp0Zk/ZAQXBdbFGixmjUFpQ2D7no3GTfu9A9wxf57dyYv4WFaLQK1Chgawso6V1tUqP+79WoFQA53N14vUnBdWGuyvVNTOhIf6tUqfE66/rUGTFwDMyni0o9cAVA6FN3Jn2a9ywued/VMWGZ147o3CfmjzdfMo+mFLJYiksbVVxm1mCL9XixOh4lWKVFr57RHm77x/3Q3MlfRrzYJxhBHQy1uBG7ai3zlAmgAqwY9Aptrr4WgNOdkqu3VFvrgYujhTDI2f3EkvMrXwfv6PhiefOWY/ubr38NxJ6jLpYbLleWpp+9Zpa++1pH4aabtfn+vcvFjII1QIMiaIIaRd93UliL13MgbBherjTFs7iwAbxsK+GoNHA8HPo0/LfdSu6KF+MXtz/3qqhaJazAFTEAANMC4IUzDCFhHkwStlcesvXCnpO8ZWO9fhdk88i58nE6iRqFo5RKrfWC1txM0AxvUIM32Cpvg4TLHmZ0cxTs3HAh8dzlL8YxDo2CeMWb1nJTLoimnRml7xxu4GzJDlUqfcMmfZNV/Gnb0O2Hy0RvnpeNsEf67F1o3U04a/SilI9P/mOmg4wDdcV60CoC0CJELdFoEpUqFaru2B9VK1B5R+wzmo4h/UZhWZwEVF0hh2T0G1lWA2TnqlrVgL6YIB6tGtD/ihOPUAOeTGrgB5drg75/PIK3aub2f//s0uug2Yy8InBA4Wi5TIqqrU8UweA5VC2LgzX4g4YlBxlek4BjS199QPbJLAyFgjHpNutQpxFhcn8B1b9TzEkTXLoOYkTDl2OXXYzvQWQJMq1Z16NHCAALkWlUcklqRZlwxVORyYYxGy86hUtSjGAbfoZwncJlCvNrwPJGKyTUnkKD0iA/T3CEFxNH1NBQdqTApfEU4dSwlQ2aTclzTk37GKEsH4EKGENZbigCwa7/hLIEfTxfThzR+tuuEd81TL0SrXhxmYVQlhzW2LtkKABuE2MoCwsRSF/loewLaFzj4iu/9F8dtlvf6V5WEguhLNk1ZQElwNYWUCqoylC244+lAY7wEu56qT6y5MX059VqYaEIgmNoQ1koGkeK0KfeTPp0UYvLmf5rOvC/3zXmeb0Tw1qZF6rypFKZbZq0npkmlRDPW6VDxxRP2XmmgaLfnHzxr/U5CWL6oVDgNd2xNgMwHIF8OUx6E5IiWFL5f1JvNiDpTSN1aKnR81Vg4/JBUf3Wd10VenVo6VsWNCbLRXIFgJBBHCZdUDIST43boDH9yRrTIj4lJ1wn6q/+3m9L+vqkE59H32RBV5K1Cwv4AFa2gI+vs026svZ7XWkRGsWqd3PgRWHIbPWaS3duLB9WvemIBIJLaLVkgRzHiNCSPkxassY2lpRHtZV2/fxZ2O6/Lvfr81dJS/YbSwoF0Dwsm0PbWKK/AO7a1tNWeWPJ3SlfNUTc9grXLo1frytPWVZzG0uOCKAcNyYB454QYPt9sNxY8uqnp6sywm9EbNzb/tfA8o7/Yq+xhNwHxgI4ZYydfhg4JMZhvbGke+CwL+6+3NZ//62VY5p26PZZdTWWACCCPC0BUWRjT9+HNpaQs9/V1FgCXSXEhrZ/gnuTmiF3qAQQOxtL0POTPPL2dQ/JHpajzotZFV5djSUYMPM8LAGjN1WZ+0I1t8n6xtWBaocrDcLSU/1HIsvWtaQOwY4m69h84Lc4MjVZcwsRrEaK/Sbr2l4eYb4FyyLX15nksaXX4+msNlknjXKZrbh3KuSHX5T+Z87M6W2n6s0EEAUxtrLlHAKOsSPrTdZpF+8/+/LCYf608gNzl9aFu9XcJmsMHqyOjhkeEB6y3GT9btLDjB/Oj4rcvfVIr4yV2plWiEQVNFnDRwnGoG2y9j2G40IoGD8mBVPDO9fODYZie73MEWXei95QOzu3N8udaxMQaB6HqXMtU49HE1XSuZZblH3zj2YD+i2orToywjeuK1uda6Nz37S538otckNEwoinXi2P2FtrAgDKMZbsUZZ9Yr/EAarJJdz2d65FDY14fu5i89ANQ8vjWmT1/LFaO9f00wh60HauzUurGP7WZhL5psfGe0UOLkW2XSy8+ULfY5bZjFyIQgnrZT4IfDIZBQZToiBcNo0KjZPFY00K7303TAlIDa+2SvC3B7d78YDXJDx/3dvDq9Lv5tIOktrJBm7YUAnj6yXEt2qkWYuHEKjMyzbPw+DHv58y/f4cB0/PP6j2Ddvl0jh2s8e2n83nFWV40ir39EOLYawvfAnyIswqdbX+pQCDxzZxM+FD8AEtPmEOJZ4Xvv+i/76frhXGP2g2yDq6OyDsZ5qcEAMA8+gAgJxx/iBErQ6TqK3YVzLkq7t/iQ7s231gx9hbYaSgFcSscnNBMxb90gEYwAMmFTyBx8pxOo1WmQQrlGAaMNZqp0VTtJZWvI+c09R90rdYuDx03c3t2q0DaYZCgRWiQzVOrZTLozDTZq59KRxp7WKR6wANqo4Bwzd+bStEqcOK1uOACR2L74gqBVrWMPc4gBYwNLf12SQ6Grclpth4IOdl3sQCgS/fPVyiTpApwtB4M6bwMFyOkiUkml13M1yPUaoqXjVnhhWiMoH7H95CHxxE8/2HTYvzAqUczB67hO1ZPPRNebkHopaMFymksrj3ZWkObthFzPpVuISvJ2Mh23uNGY1pWIlcoJRWKGlzjtaCAC/J9D9nkFqiMj1DsGpdJlb9r7AKL5o0bRKlqBux9ZOirHzviGTWrQJ0UQCV/IUwWAX9LQGU+YbSv2e/VeB9Xb7/uGMkN73rDyEXjy58wZpVILeP2psGvCSA4NcIg1Xg/ozDUwVWoX+n/4yt/8iDl1NYK3XTHrLV/IhWAXpAAEBrFfRPcAAIUavHJGo1yCrcT5clNhj5ZcTqX4oj/Hvc1lW9VSCTsnqsgn6/AIp9DOgICyDoZJVYhYL9AlfpY+S/wirUZ2LVxaLaAcvPL+PveXFiaHr31f6kVfzRGqUcQG29WYDNVsuIxyvU7FqXFfgmZ0rUo90pIXnJbY67+J/+in5UNPtcjNbYYAcgbLskLa0dKAHcA9kUHVRaqXts76nigwFjkK8Vp+8nDNgQak2lLk2f40cwAFwMF9qEQMFlHBeWC2HJrZrWNe9VQad0KTF32n72gkf43AmBasAkUDVpI+3hdxrUE21OF23/l0vv3645qtjeSHsXAokdGDfS3otAOY5sb6T9+tsePecEHxdkNF5SPky9Aa6xG2lzATgqB6aNomNz8WWRqthIO+vsgbmKQnG/TWuREXtjXqawtZH2l3MV9/aOuCLIa7csFfle95gFfHIcLeEjdvr/t5E2eWmp+jbSLtlH8C/tFtLQAZx/q2Yj7TD0eAvn0JXIPvECdGyxVF+NG2ljKOBcyoiC2LQE4w8xmITVE5cUPy0dK9i4NfDtufNdimmOdDG3CZZ0ZBvGsy4SUAXeJ4EfnEMfDTTt7rVwQ2LX4HnS24OW1tt3h2YkVKdKoE6uVDmQF8zsTU6eJGCnO9JCvOk04j3FBPsnNR/27ZDi3qCJieG7/KffKh9espw12L3q9Dg+LD8seOeLuQGX0kZvsNf5Ky2EYMwBooOdMzC38J7Y5AA1ZIJd1+dZ/Xp3lvfb3flS/bH8a+brKc6Gkifrce9iw9EulopGyPvw0o2KSgNwvVIavDnsNH9R8e/IunMdxurrP2hvLw0eFkLQ3ww0uLN+T+GMtP8YExCNmGjwbsbOdXU6cvh7hvuvyFaFr6+U9d2Jt/gJ8AQD3blp7oZbZokCb+IaOYFA69wFy2UqFTCAuKDQEmnC90sUE7tfCNk764vIYCR7MEtnX33H9VYeiEkJT59xxembtkdnWUkhk0NAtgnXEYiLrdGpcAoRtGjMRIv6t79Bavc6L1jT7c6h7SfHI5XSwq3qaWGXNiMrnQ/XZlYaZyZCQIOPGNSVGSGafBAhaBXTR6AEW/qNrIXs0G/2UmXIEYMCM6NKUyaq5GfP7bPx7fbQ3QWfx5zM09Q2H7ihE9N6c9GJEc54uRK4nSBkxmuiYEN5AT2W2Yv6etxb8rsob8dnn6VPzM2kGxIVS3D9Y+87rD+KQHqmk7/i808gZ8NMvfuffpCtoCXARzAWrNAxqsEk3S+ltYN3Z/aO3qPkPGSPjmQOsdOixF4A4uJKtigwE8U+9Vje/LeML0O2zv5Td7bruFrmW4JhZhamMSuWEk3t8IeIMzdhHdZejaeajCXcCQBV1NKJnBNWpP0dU9Y9cu+uqNzTffUTmYZEzTdh96x1je4WQioOUdFBzTetK4SK6Ku5GPNNpiM548EoaCc21sXxsx13PYVzt217Po67K48ll4QcEdjrNJYUGurc6E8lO1cIBdWiJJss7rwWSUN6WoAEA2cVN4uYHj7N88njxDrZpPMgsE/SAESTjbM3d1uCU98CAipn2/LWVvirs89FDPBsfDw47bdZDdYXxy35UOawd+4zCerPop37GJz6NqVqPAz6gHnX4u4pZe63nmby0gP7Ht/y9PE1DkmmJQlWFTeyMHGc6IwTB0QntGkzJm068MftT9NbNwlO1zytdfzP9g7mO0UYFtfgYJlcSzaDxnON6ABsZXiAar5w6YM1+FvprZYi6vaSxqf7h88dclD2bPCt1RbGQ+3KM1y3Fr4rh6EyLMW3gq5ZDMrkQmVONK21GONgc6dprcV+ya21qv29Ogpn5AWnyRv73ZN4nKustdZ435HhPputt0zkqytSSNEUjF4h2OKLgVy01Nr1+sp4n53zgw90aBv6x5n7pP4N7GmrkrUk8zAi6OyhrjFTIlblTRs4v+jIdHvNw8nDhiYMeuWoP4wl26q5ytck+uYdwMYQzJkXF4dqTJufMBGugaiCfMESLZA3jJBBtLTb3cYP/TtwXIh+Q/a4jqdT17uE67TYZqomqphn3wnRC6ISNKjSxSW27T0gqNjJEkGB5FZA0pE1JDvTIul6Zdo25+Jx/FVfJ73Oe4fGWIdkZyqSnStFkmxzWUDSgucEkAyi7lnrahuUPgatDFwJC4qE3OxqGUKi6INmq8CAzoHs29lMnkF/lPCwGIUMkisfYzfCzjaHPiT94sJXKuWoRFGDMmGjDSOyVFyVVX8YrMh/EbbnZMzIOvCECbRTotIIu8F+2kV+xGDMzWLIFkzUmH198WlUskewkjfI8eCw592toMaHLY9YASPZ2tkFI0ldkElkr7poSqBMl3F5PY1zZHOe6TCxlkzYQzOLg75veJ2vz3r+UM6XmeeXPMIk6gRUDcckkvG3FMB/QjylBU/BAUo1jI7TSbDGrEBaxPs07Fj/2esRoQcXbvgzVzCVeQRUD9Nw0+pT+hAIwg5WWUAXscdORWytsvImtmbDOs5QDX3zXtjiuKA9u+/zNi7MaqX65Lmn2eSI/QwYkj9WNH+yXG+lBwhxGY+eyZyEQPpyxJYykfrmCMGSBEws6ZH65uh3fk1d0NDlDmPejmp/Q0wKNQy7GzBBVXkjaAvRiwUPV98Lz9/n7X0r7NFRFqAC7GIBKsihur3YinWxtmUbYtQ6FJbFwzy4D8ynX3fJuX0wueglsnDg7Ai0Ry9XWqHV0gmtQx/2N2ZOI/j2JB0xiqbjfGtTzsG7AgK9GSB4OWJi/8gJvYQbbqxXjT95xjxX7XNbnw0D1SckVB+1a71P7yrBAWdKRhwAUxIGoRXEVr248ZwW9uvFyTJbBfXiTIhTCsIdgqws9i5QIpAKI0IZToQqKPbe+kCJLEyG/jtagFoz8RnXt3tW1L2T/XPKfTdyW6WbtwC5RqAJgFx0ywZWFXsriMcrFnvTslhpl+L9z1fKQjeWX1LJApdl0o+CZmHG6iaf2K8QKAjbao+2uBuag0C+1G2Y7SruLl0/Vhf4hyjiOx9d1otG1yUsFXeTe5Pt3UoB4CJ2Zur2njcLx4Xl4u5nP9XLW6cpDVvQ7M6ClfnJK6uruLtkLjF32uLuom/wuRMC1IZJgGparrhn+yWCWChBsP7Nm7/29t3TtepyxbELEUPGiTZXHLsU3KXbhvGfXHEFas0d5bAtuKdv2NLgOb82bNb/LDu5YrJbaK+OWEhst0KbECtYhG+38v8vV/zu6cKFm/PduesuSvXfePd5W4W5YraV/kJiSwlGggLJ/Yi54nXX7zW9dPc75NshRzLndTjtX4W5YrLtYQHJMsY1ZgzJedQdOqsiV0xuxfjgXHEQ+7li7nJCf9DnijNwdiPsbADEYGc/ZJtjSyaWfptj+o1ir2/+9X7iUFHaVzG3LjfvNtrKbY2deO2sPmI7CbCRi5DhiO0CLUDI7Z8djaFjxYHwsAcnQtd2GyA+fo5LOjb6g3Y0Ju90a29CChDSl3F380wQlOa4Vt2Oxtyx44Pctl3v/zUUJb72cKAvCzsa176889TNLt1C0w9LXDntdvdkAR8x44arGD5lblWyozF5k6zqOg1Sn0IwCO1pkJkTcHgIXRjIpAstly3T1Wd+hGUzVuoz5zjxkOelalH+rMnp3hmipnbUZ9q5eMbtjkCZ9cmLZ22ZSFIDa5bJosterSuZSvauoH2BQEX1mGqW4wchK5bXN8pEOyYC1KBjEN6O2tVmctp6Qcaakbk7ts0NttJfsPUYhIJkoCx8mI5B0E8Gtsb3H6cBOhaxyLnzlKvBe09CTf4M7bOo5h2DwB0P/GMfJqMYmwoshm/VOQ09cu/5tS/PEe091unK2cJDZ1lwGlg+BgHDB7CyBXzEflXiNCB/b1O379M9JKf9rLrT7iI+1XoMAjyV4BLaYxAy03CMCC3ZnklLciZAK68W3e23KitjDf9gOPlURt1ordpQ4mt9IOVvpig1719BC+kvr76P6VO3Qdi27Nt9/W5ljGAcAE041cHqcGoi8CM9mMIprMZA7PW/qRmJg7WwtShsaQ8/GK8SijQOOJY/sPHx0A1otPe4n79mI6hiu6cJkNOXcf99bJU/x/NDDtayBaXmU7afTe7hFbbryN5vH6269nfN05IYSmIvSyhhm5vaoCXrvdeSlaEz/9Rz0dTfhP3W5EWKt7dpmlxd0RW29I5zCm10JZ6OI0ToyA5MOrIG9lJlTtLMPZMzhLvqkzndpR5nO1RJLxX3AtHETNtLxS0G8RDrvVTIr6LHLxRdQnfoWt3/AWkZxFIvFXlTC3tFCyATy5gvz7yIgLHatB2/9b1UT4dzOcseTOftbdFLIP1lx4AP7aWq2uJzDKBMxjQ4BhCX/VYrcg9/NbVawT8TzEHbcRR0BWcOdlutOrb9kh9wO1XwrXCHw6dPu0+rnlYrbOKZjK1W2MS5plarjkzKds5PeX7ueY0ipya8CdTN6+llrtlMh05orD/ypLkwBbdVeAWK6cQLjaEGxYKOzcsJnnft26+Ea+N7rd+1VR3ENBKqYwruWeuYbjwMlWBbUOpp21VhHpT5zKatZv1MKhbF6i0kWnpRSdvx0Ed/cDE3vekfk5NL4J01tGV12WEIfkIUvlKPwww4jKFjkxYh4KnsaJjWXZJWBSqGCHbda3jKITok3xwe/GgYKjzR7PdUnOQZ5q+nEyiIw4din9pWQOljmL/lc19+XrcOzlo3Mmxd/0Cf/MVz7nPIzE2du7BK5g6oa2HuJWXI/wE= + + Contains a cluster of Grasshopper components + true + 9c0f68ef-d76a-47b0-8e49-544cf9321db9 + Cluster + Cluster + false + + + + + 6 + 889b2da6-e09d-4486-879f-2ca5073fe873 + 91116c7c-c6f6-4b91-b30e-651548dbb2cd + 957b3f47-111f-4a0f-9dfa-3dede52b857e + c08a55b9-8d03-46e8-9856-18fdb41e1daf + cf4db125-1c5e-46b5-899e-ec4fbe113fdd + d2d51328-18f6-4ef3-8dd6-1df6b6af922e + 205b16a3-be6e-4cf4-b6c8-545e13207a7a + 757d1752-e8de-4612-b5a0-3d53b66f04ea + c638130d-be5b-464c-b2f4-8c28d38362a7 + 4441038b-e8f1-4972-be88-7e8e0e9b491e + 9103c1fa-d594-44f2-a4ce-2d6b8016e92c + 6e96ab7a-377b-47d1-b788-514f4644a658 + + + + + + 573 + 575 + 99 + 84 + + + 621 + 617 + + + + + + 4 + 16ef3e75-e315-4899-b531-d3166b42dac9 + d5967b9f-e8ee-436b-a8ad-29fdcecf32d5 + cb95db89-6165-43b6-9c41-5702bc5bf137 + 3e8ca6be-fda8-4aaf-b5c0-3c54c8bb7312 + 2 + d5967b9f-e8ee-436b-a8ad-29fdcecf32d5 + 3e8ca6be-fda8-4aaf-b5c0-3c54c8bb7312 + + + + + Contains a collection of three-dimensional vectors + cf4db125-1c5e-46b5-899e-ec4fbe113fdd + Vector + Vec + true + 3fa223fc-b370-43ff-9286-d236dfc1155a + 1 + + + + + + 575 + 577 + 31 + 20 + + + 592 + 587 + + + + + + + + Contains a collection of generic curves + 957b3f47-111f-4a0f-9dfa-3dede52b857e + Curve + Crv + true + 09fe95fb-9551-4690-abf6-4a0665002914 + 1 + + + + + + 575 + 597 + 31 + 20 + + + 592 + 607 + + + + + + + + Contains a collection of boolean values + 889b2da6-e09d-4486-879f-2ca5073fe873 + Boolean + Bool + true + ca0ea429-34fb-4ce1-a30e-2c14ddc1552e + 1 + + + + + + 575 + 617 + 31 + 20 + + + 592 + 627 + + + + + + + + Contains a collection of floating point numbers + c08a55b9-8d03-46e8-9856-18fdb41e1daf + Number + Num + true + c0dd7c3a-81ff-41c2-b62c-72cfdce3816b + 1 + + + + + + 575 + 637 + 31 + 20 + + + 592 + 647 + + + + + + + + Contains a collection of generic curves + d2d51328-18f6-4ef3-8dd6-1df6b6af922e + Curve + Crv + false + 0 + + + + + + 636 + 577 + 34 + 40 + + + 653 + 597 + + + + + + + + Contains a collection of floating point numbers + 91116c7c-c6f6-4b91-b30e-651548dbb2cd + Number + Num + false + 0 + + + + + + 636 + 617 + 34 + 40 + + + 653 + 637 + + + + + + + + + + + + + + b6236720-8d88-4289-93c3-ac4c99f9b97b + Relay + + + + + 2 + A wire relay object + ed4dd89a-026f-43eb-81d4-433cea857301 + Relay + + false + 3984d0df-294e-462f-b103-b87bb94e3021 + 1 + + + + + + 2164 + 686 + 40 + 16 + + + 2184 + 694 + + + + + + + + + + 57da07bd-ecab-415d-9d86-af36d7073abc + Number Slider + + + + + Numeric slider for single values + c0dd7c3a-81ff-41c2-b62c-72cfdce3816b + Number Slider + + false + 0 + + + + + + 371 + 665 + 173 + 20 + + + 371.0457 + 665.6685 + + + + + + 1 + 1 + 0 + 20 + 0 + 0 + 2 + + + + + + + + + 57da07bd-ecab-415d-9d86-af36d7073abc + Number Slider + + + + + Numeric slider for single values + 4d710107-34a1-4586-be44-d6f80294f0e6 + Number Slider + + false + 0 + + + + + + 2018 + 731 + 192 + 20 + + + 2018.859 + 731.8153 + + + + + + 2 + 1 + 0 + 100 + 0 + 0 + 0 + + + + + + + + + 4f8984c4-7c7a-4d69-b0a2-183cbb330d20 + Plane + + + + + Contains a collection of three-dimensional axis-systems + 39d6e475-c4b6-4330-98aa-9fdff81c5724 + Plane + Pln + false + 389410e6-fbd0-4b13-ad00-528c4c2075fd + 1 + + + + + + 2021 + 280 + 50 + 24 + + + 2046.047 + 292.3867 + + + + + + + + + + 57da07bd-ecab-415d-9d86-af36d7073abc + Number Slider + + + + + Numeric slider for single values + e1131cec-2fb2-499e-93e5-55dce618ee47 + Number Slider + + false + 0 + + + + + + 2020 + 834 + 192 + 20 + + + 2020.459 + 834.2153 + + + + + + 2 + 1 + 0 + 100 + 0 + 0 + 0 + + + + + + + + + 57da07bd-ecab-415d-9d86-af36d7073abc + Number Slider + + + + + Numeric slider for single values + 78334c0a-357d-4e7c-b3e4-fc940bcf9f1c + Number Slider + rotation + false + 0 + + + + + + 1971 + 858 + 240 + 20 + + + 1971.77 + 858.5305 + + + + + + 3 + 1 + 1 + 100 + 0 + 0 + 0 + + + + + + + + + 57da07bd-ecab-415d-9d86-af36d7073abc + Number Slider + + + + + Numeric slider for single values + 6b6d7ea8-7a08-4879-939c-5f96604c4678 + Number Slider + + false + 0 + + + + + + 1407 + 246 + 186 + 20 + + + 1407.689 + 246.8536 + + + + + + 2 + 1 + 0 + 200 + 0 + 0 + 20 + + + + + + + + + 57da07bd-ecab-415d-9d86-af36d7073abc + Number Slider + + + + + Numeric slider for single values + 89a39aa2-89d0-4ad6-9043-6ef61b51023c + Number Slider + + false + 0 + + + + + + 1405 + 224 + 188 + 20 + + + 1405.608 + 224.0857 + + + + + + 3 + 1 + 1 + 50 + 0 + 0 + 10 + + + + + + + + + c552a431-af5b-46a9-a8a4-0fcbc27ef596 + Group + + + + + 1 + + 150;255;255;255 + + A group of Grasshopper objects + bbcbfdee-5b0e-4c92-bf29-b5811037ea03 + 3ee8dc00-1eea-48ba-bc11-c2d1911c58a5 + b29d9bba-e05a-4fa4-91a3-bf1ac143a41d + 4d710107-34a1-4586-be44-d6f80294f0e6 + e1131cec-2fb2-499e-93e5-55dce618ee47 + 78334c0a-357d-4e7c-b3e4-fc940bcf9f1c + 6 + 792ba2cf-3af6-498a-971d-99f5e5a1dea5 + Group + + + + + + + + + + + 410755b1-224a-4c1e-a407-bf32fb45ea7e + 00000000-0000-0000-0000-000000000000 + CT: T Topological Joint Rules + + + + + import inspect + +from ghpythonlib.componentbase import executingcomponent as component +from Grasshopper.Kernel.GH_RuntimeMessageLevel import Warning + +from compas_timber.connections import Joint +from compas_timber.connections import JointTopology +from compas_timber.connections import TButtJoint +from compas_timber.design import TopologyRule +from compas_timber.ghpython.ghcomponent_helpers import get_leaf_subclasses +from compas_timber.ghpython.ghcomponent_helpers import manage_dynamic_params +from compas_timber.ghpython.ghcomponent_helpers import rename_gh_output + + +class T_TopologyJointRule(component): + def __init__(self): + super(T_TopologyJointRule, self).__init__() + self.classes = {} + for cls in get_leaf_subclasses(Joint): + if cls.SUPPORTED_TOPOLOGY == JointTopology.TOPO_T: + self.classes[cls.__name__] = cls + if ghenv.Component.Params.Output[0].NickName == "Rule": + self.joint_type = TButtJoint + self.clicked = False + else: + self.joint_type = self.classes.get(ghenv.Component.Params.Output[0].NickName, None) + self.clicked = True + + def RunScript(self, *args): + if not self.clicked: + ghenv.Component.Message = "Default: TButtJoint" + self.AddRuntimeMessage(Warning, "TButtJoint is default, change in context menu (right click)") + return TopologyRule(JointTopology.TOPO_T, TButtJoint) + else: + ghenv.Component.Message = self.joint_type.__name__ + kwargs = {} + for i, val in enumerate(args): + if val is not None: + kwargs[self.arg_names()[i]] = val + if self.joint_type.SUPPORTED_TOPOLOGY != JointTopology.TOPO_T: + self.AddRuntimeMessage(Warning, "Joint type does not match topology. Joint may not be generated.") + return TopologyRule(JointTopology.TOPO_T, self.joint_type, **kwargs) + + def arg_names(self): + names = inspect.getargspec(self.joint_type.__init__)[0][3:] + return [name for name in names if (name != "key") and (name != "frame")] + + def AppendAdditionalMenuItems(self, menu): + for name in self.classes.keys(): + item = menu.Items.Add(name, None, self.on_item_click) + if self.joint_type and name == self.joint_type.__name__: + item.Checked = True + + def on_item_click(self, sender, event_info): + self.clicked = True + self.joint_type = self.classes[str(sender)] + rename_gh_output(self.joint_type.__name__, 0, ghenv) + manage_dynamic_params(self.arg_names(), ghenv, rename_count=0, permanent_param_count=0) + ghenv.Component.ExpireSolution(True) + + GhPython provides a Python script component + + 96 + 96 + + + 741 + 702 + + true + true + true + + iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAALDAAACwwBP0AiyAAAAN5JREFUSEvtk7ENwkAMRTNC6JGIxAIZhREYgY6WCRAjpKbKCCkoKRiBEdjAfJ/uImPMXS6EAinFsy4++3/JvhRE9FPM5HpVdYA8J6tmKP0BQhUowcELS3ayKQcXILBRgp+4g1qLxHDBN1qCFq0WieECmuTMmQbwuHhsN58LdFokhgtoapVIGQpwrtXdKAO92MkNeCRShL95PJONSO8gxqglD32mD5D/TBluBNZImG2oWx4vC0AJzqG+N5BAUO7k5S9G8/cGOUDs6kX31v1bIpfZIMlskOT/DVKYyemg4glf8MWtha38JQAAAABJRU5ErkJggg== + + false + 68447cb4-778f-4e2c-a7ca-627a868c84e1 + true + true + CT: T Topological Joint Rules + T_Topo_Joint + + + + + + 977 + 1089 + 192 + 204 + + + 1073 + 1191 + + + + + + 10 + 8ec86459-bf01-4409-baee-174d0d2b13d0 + 8ec86459-bf01-4409-baee-174d0d2b13d0 + 8ec86459-bf01-4409-baee-174d0d2b13d0 + 8ec86459-bf01-4409-baee-174d0d2b13d0 + 8ec86459-bf01-4409-baee-174d0d2b13d0 + 8ec86459-bf01-4409-baee-174d0d2b13d0 + 8ec86459-bf01-4409-baee-174d0d2b13d0 + 8ec86459-bf01-4409-baee-174d0d2b13d0 + 8ec86459-bf01-4409-baee-174d0d2b13d0 + 8ec86459-bf01-4409-baee-174d0d2b13d0 + 1 + 8ec86459-bf01-4409-baee-174d0d2b13d0 + + + + + start_y + 534626fc-7966-4736-9188-176756493fcc + start_y + start_y + true + a3e72ece-c329-479f-b17f-94062e976ee8 + 1 + + + + + + 979 + 1091 + 79 + 20 + + + 1020 + 1101 + + + + + + + + Script input start_depth. + 7597de76-5e70-4808-9b8e-e81cbcc37925 + start_depth + start_depth + true + fe8fa9fd-21f2-43ef-b49f-767b0753a1ae + 1 + + + + + + 979 + 1111 + 79 + 20 + + + 1020 + 1121 + + + + + + + + Script input rotation. + 151edfa6-82d1-4b59-96ef-2d94ed57e39e + rotation + rotation + true + 48898955-f881-4e56-b6e5-3c7ee5cdc4a2 + 1 + + + + + + 979 + 1131 + 79 + 20 + + + 1020 + 1141 + + + + + + + + Script input length. + d502ab54-077d-4a58-8129-a281e626ea22 + length + length + true + fb924a22-fb82-45ca-81fd-fb8a3ef8536a + 1 + + + + + + 979 + 1151 + 79 + 20 + + + 1020 + 1161 + + + + + + + + Script input width. + cf534226-abf9-4421-af45-780bfb2a9218 + width + width + true + dcc01bf8-eaab-475a-9a3d-34524bf3dfa7 + 1 + + + + + + 979 + 1171 + 79 + 20 + + + 1020 + 1181 + + + + + + + + Script input cone_angle. + d99cde1f-d7fd-4021-b596-b00c93659492 + cone_angle + cone_angle + true + 0cae4f91-95c7-4cfa-8d10-c084807de87e + 1 + + + + + + 979 + 1191 + 79 + 20 + + + 1020 + 1201 + + + + + + + + Script input dovetail_shape. + 347275da-3171-4d50-89da-fb719a724122 + dovetail_shape + dovetail_shape + true + dfa57b10-b759-4c01-8ef6-e8f27c2261c0 + 1 + + + + + + 979 + 1211 + 79 + 20 + + + 1020 + 1221 + + + + + + + + Script input tool_angle. + 3f15676d-0938-42ad-a0d7-8090bcb46e4f + tool_angle + tool_angle + true + 19c8f5b9-f818-4553-9fec-96ae2df55628 + 1 + + + + + + 979 + 1231 + 79 + 20 + + + 1020 + 1241 + + + + + + + + Script input tool_diameter. + aef3de77-ddf0-4ec7-ab3f-93300c6c15ae + tool_diameter + tool_diameter + true + 10946ab9-030f-41c8-8192-8fc73ecb49e4 + 1 + + + + + + 979 + 1251 + 79 + 20 + + + 1020 + 1261 + + + + + + + + Script input tool_height. + 883bd64a-9859-44a6-b61e-8c025315b39c + tool_height + tool_height + true + 697f731c-faf2-45c5-a9c4-a947431b741b + 1 + + + + + + 979 + 1271 + 79 + 20 + + + 1020 + 1281 + + + + + + + + Script output TDovetailJoint. + 9ef43da6-ada7-4c17-a8ec-f321d9379183 + TDovetailJoint + TDovetailJoint + false + 0 + + + + + + 1088 + 1091 + 79 + 200 + + + 1127.5 + 1191 + + + + + + + + + + + + + + 57da07bd-ecab-415d-9d86-af36d7073abc + Number Slider + + + + + Numeric slider for single values + dcc01bf8-eaab-475a-9a3d-34524bf3dfa7 + true + Number Slider + width + false + 0 + + + + + + 771 + 1153 + 160 + 20 + + + 771.9711 + 1153.291 + + + + + + 3 + 1 + 1 + 200 + 0 + 0 + 25 + + + + + + + + + 57da07bd-ecab-415d-9d86-af36d7073abc + Number Slider + + + + + Numeric slider for single values + 0cae4f91-95c7-4cfa-8d10-c084807de87e + true + Number Slider + cone_angle + false + 0 + + + + + + 692 + 1178 + 240 + 20 + + + 692.5809 + 1178.608 + + + + + + 3 + 1 + 1 + 100 + 0 + 0 + 10 + + + + + + + + + 57da07bd-ecab-415d-9d86-af36d7073abc + Number Slider + + + + + Numeric slider for single values + fb924a22-fb82-45ca-81fd-fb8a3ef8536a + true + Number Slider + length + false + 0 + + + + + + 760 + 1127 + 171 + 20 + + + 760.933 + 1127.53 + + + + + + 3 + 1 + 1 + 200 + 0 + 0 + 60 + + + + + + + + + 57da07bd-ecab-415d-9d86-af36d7073abc + Number Slider + + + + + Numeric slider for single values + fe8fa9fd-21f2-43ef-b49f-767b0753a1ae + true + Number Slider + + false + 0 + + + + + + 741 + 1077 + 192 + 20 + + + 741.048 + 1077.661 + + + + + + 2 + 1 + 0 + 100 + 0 + 0 + 10 + + + + + + + + + 57da07bd-ecab-415d-9d86-af36d7073abc + Number Slider + + + + + Numeric slider for single values + a3e72ece-c329-479f-b17f-94062e976ee8 + true + Number Slider + + false + 0 + + + + + + 741 + 1054 + 192 + 20 + + + 741.4919 + 1054.057 + + + + + + 2 + 1 + 0 + 100 + 0 + 0 + 10 + + + + + + + + + 57da07bd-ecab-415d-9d86-af36d7073abc + Number Slider + + + + + Numeric slider for single values + 48898955-f881-4e56-b6e5-3c7ee5cdc4a2 + true + Number Slider + rotation + false + 0 + + + + + + 693 + 1102 + 238 + 20 + + + 693.3779 + 1102.07 + + + + + + 3 + 1 + 1 + 100 + 0 + 0 + 20 + + + + + + + + + c552a431-af5b-46a9-a8a4-0fcbc27ef596 + Group + + + + + 1 + + 150;255;255;255 + + A group of Grasshopper objects + dcc01bf8-eaab-475a-9a3d-34524bf3dfa7 + 0cae4f91-95c7-4cfa-8d10-c084807de87e + fb924a22-fb82-45ca-81fd-fb8a3ef8536a + fe8fa9fd-21f2-43ef-b49f-767b0753a1ae + a3e72ece-c329-479f-b17f-94062e976ee8 + 48898955-f881-4e56-b6e5-3c7ee5cdc4a2 + dfa57b10-b759-4c01-8ef6-e8f27c2261c0 + 7 + 0d673e88-5ab0-4910-98ed-aa50c52a5acc + Group + + + + + + + + + + + 00027467-0d24-4fa7-b178-8dc0ac5f42ec + Value List + + + + + Provides a list of preset values to choose from + true + dfa57b10-b759-4c01-8ef6-e8f27c2261c0 + 5 + 1 + true + Value List + List + false + 0 + + + + + 0 + Automatic + false + + + + + 1 + Square + false + + + + + 2 + Round + false + + + + + 3 + Rounded + false + + + + + 4 + Radius + true + + + + + + 833 + 1204 + 99 + 22 + + + + + + + + + + 410755b1-224a-4c1e-a407-bf32fb45ea7e + 00000000-0000-0000-0000-000000000000 + CT: Model + + + + + from compas.scene import Scene +from compas.tolerance import TOL +from ghpythonlib.componentbase import executingcomponent as component +from Grasshopper.Kernel.GH_RuntimeMessageLevel import Warning + +from compas_timber.connections import BeamJoinningError +from compas_timber.connections import ConnectionSolver +from compas_timber.connections import JointTopology +from compas_timber.connections import LMiterJoint +from compas_timber.connections import TButtJoint +from compas_timber.connections import XHalfLapJoint +from compas_timber.design import CategoryRule +from compas_timber.design import DebugInfomation +from compas_timber.design import DirectRule +from compas_timber.design import JointDefinition +from compas_timber.design import TopologyRule +from compas_timber.model import TimberModel + +JOINT_DEFAULTS = { + JointTopology.TOPO_X: XHalfLapJoint, + JointTopology.TOPO_T: TButtJoint, + JointTopology.TOPO_L: LMiterJoint, +} + + +# workaround for https://github.com/gramaziokohler/compas_timber/issues/280 +TOL.absolute = 1e-6 + + +class ModelComponent(component): + def get_joints_from_rules(self, beams, rules, topologies): + if not isinstance(rules, list): + rules = [rules] + rules = [r for r in rules if r is not None] + + joints = [] + # rules have to be resolved into joint definitions + topo_rules = {} + cat_rules = [] + direct_rules = [] + + # TODO: refactor this into some kind of a rule reloving class/function + for r in rules: # separate category and topo and direct joint rules + if isinstance(r, TopologyRule): + if topo_rules.get(r.topology_type, None): # if rule for this Topo exists + if (r.joint_type != JOINT_DEFAULTS[r.topology_type]) or ( + len(r.kwargs) != 0 + ): # if this rule is NOT default + topo_rules[r.topology_type] = r + else: + topo_rules[r.topology_type] = r + elif isinstance(r, CategoryRule): + cat_rules.append(r) + if isinstance(r, DirectRule): + direct_rules.append(r) + + for topo in topologies: + beam_a = topo["beam_a"] + beam_b = topo["beam_b"] + detected_topo = topo["detected_topo"] + pair = beam_a, beam_b + pair_joined = False + + if detected_topo == JointTopology.TOPO_UNKNOWN: + continue + + for rule in direct_rules: # apply direct rules first + if rule.comply(pair): + joints.append(JointDefinition(rule.joint_type, rule.beams, **rule.kwargs)) + pair_joined = True + break + + if not pair_joined: # if no direct rule applies, apply category rules next + for rule in cat_rules: + if not rule.comply(pair): + continue + if rule.joint_type.SUPPORTED_TOPOLOGY != detected_topo: + msg = "Conflict detected! Beams: {}, {} meet with topology: {} but rule assigns: {}" + self.AddRuntimeMessage( + Warning, + msg.format( + beam_a.guid, + beam_b.guid, + JointTopology.get_name(detected_topo), + rule.joint_type.__name__, + ), + ) + continue + if rule.topos and detected_topo not in rule.topos: + msg = "Conflict detected! Beams: {}, {} meet with topology: {} but rule allows: {}" + self.AddRuntimeMessage( + Warning, + msg.format( + beam_a.guid, + beam_b.guid, + JointTopology.get_name(detected_topo), + [JointTopology.get_name(topo) for topo in rule.topos], + ), + ) + continue + # sort by category to allow beam role by order (main beam first, cross beam second) + beam_a, beam_b = rule.reorder([beam_a, beam_b]) + joints.append(JointDefinition(rule.joint_type, [beam_a, beam_b], **rule.kwargs)) + break # first matching rule + + else: # no category rule applies, apply topology rules + if detected_topo not in topo_rules: + continue + else: + joints.append( + JointDefinition( + topo_rules[detected_topo].joint_type, + [beam_a, beam_b], + **topo_rules[detected_topo].kwargs + ) + ) + return joints + + def RunScript(self, Elements, JointRules, Features, MaxDistance, CreateGeometry): + if not Elements: + self.AddRuntimeMessage(Warning, "Input parameter Beams failed to collect data") + if not JointRules: + self.AddRuntimeMessage(Warning, "Input parameter JointRules failed to collect data") + if not (Elements): # shows beams even if no joints are found + return + if MaxDistance is None: + MaxDistance = TOL.ABSOLUTE # compared to calculted distance, so shouldn't be just 0.0 + + Model = TimberModel() + debug_info = DebugInfomation() + for element in Elements: + # prepare elements for downstream processing + if element is None: + continue + element.remove_features() + if hasattr(element, "remove_blank_extension"): + element.remove_blank_extension() + element.debug_info = [] + Model.add_element(element) + + topologies = [] + solver = ConnectionSolver() + found_pairs = solver.find_intersecting_pairs(list(Model.beams), rtree=True, max_distance=MaxDistance) + for pair in found_pairs: + beam_a, beam_b = pair + detected_topo, beam_a, beam_b = solver.find_topology(beam_a, beam_b, max_distance=MaxDistance) + if not detected_topo == JointTopology.TOPO_UNKNOWN: + topologies.append({"detected_topo": detected_topo, "beam_a": beam_a, "beam_b": beam_b}) + Model.set_topologies(topologies) + + joints = self.get_joints_from_rules(Model.beams, JointRules, topologies) + + if joints: + handled_beams = [] + joints = [j for j in joints if j is not None] + # apply reversed. later joints in orginal list override ealier ones + for joint in joints[::-1]: + beams_to_pair = joint.beams + beam_pair_ids = set([id(beam) for beam in beams_to_pair]) + if beam_pair_ids in handled_beams: + continue + try: + joint.joint_type.create(Model, *beams_to_pair, **joint.kwargs) + except BeamJoinningError as bje: + debug_info.add_joint_error(bje) + else: + handled_beams.append(beam_pair_ids) + + # applies extensions and features resulting from joints + Model.process_joinery() + + if Features: + features = [f for f in Features if f is not None] + for f_def in features: + for element in f_def.elements: + element.add_features(f_def.feature) + + Geometry = None + scene = Scene() + for element in Model.elements(): + if CreateGeometry: + scene.add(element.geometry) + if element.debug_info: + debug_info.add_feature_error(element.debug_info) + else: + scene.add(element.blank) + + if debug_info.has_errors: + self.AddRuntimeMessage(Warning, "Error found during joint creation. See DebugInfo output for details.") + + Geometry = scene.draw() + return Model, Geometry, debug_info + + GhPython provides a Python script component + + 160 + 160 + + + 741 + 702 + + true + true + true + + iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAALDAAACwwBP0AiyAAAAa1JREFUSEu1lc1NA0EMRqeElJASUkJK4MgxJaSElEAJKSFHjikhBw4cERUgcUNI4XsTe+Td8S4bJCw9MePPP7uezFKu1+tiZA+QaVOkzgzZQbCAQxaTkTojspU4WeFXgzW+VZYTSZ2ObC0ugg1vEMGHts5yndQJsq34MOrcZbW4rTkP17cxN5I7S9kJFm9iE/ytge03ghg2O/dHekcpR0s4i8GMZYMG5uOMiGVzjFrV2+IW6PPuAi2maxA0fzBqtAdz8ddXBdlkAyBXsGijxeJhtXlnyGYbADWE16N2m99/QO3yJL4FM+SnGcHHTH3/aPgebSqPmtSur83MeKXBpTHtHH0RNGJGPi4ntahJ/q2IOU6j4L804BNCrarh8Aa8GhHtVsruaiBrNcSwQUi4hIR7G3AmsVbXgPmx2Nt+cQPZXrCo5yjrG5jAnkPiZi9qIIglJ77NZAMP5ie2tAGx9aESrS8i8yv/bH/n8JjBJ0Y23SAEvIt40cagETOVP9uAAixexpqDZjHdPxzZoAHzwzHmU3yNfBE0YjKtHrqoT8oig28KNzPTAI2YTBNl+wPOvtI8NDHinQAAAABJRU5ErkJggg== + + false + fb2bee0a-2791-40d8-ba2d-3facd193d280 + true + true + CT: Model + Model + + + + + + 1424 + 1159 + 198 + 104 + + + 1543 + 1211 + + + + + + 5 + 84fa917c-1ed8-4db3-8be1-7bdc4a6495a2 + 84fa917c-1ed8-4db3-8be1-7bdc4a6495a2 + 84fa917c-1ed8-4db3-8be1-7bdc4a6495a2 + 84fa917c-1ed8-4db3-8be1-7bdc4a6495a2 + 84fa917c-1ed8-4db3-8be1-7bdc4a6495a2 + 3 + 8ec86459-bf01-4409-baee-174d0d2b13d0 + 8ec86459-bf01-4409-baee-174d0d2b13d0 + 8ec86459-bf01-4409-baee-174d0d2b13d0 + + + + + 1 + true + Collection of Beams + 75b9cd26-8234-4296-a759-24e24b94d514 + 1 + Elements + Elements + true + 1 + true + 3984d0df-294e-462f-b103-b87bb94e3021 + 90fae02f-7750-462d-828f-cf5f2a0d39ae + 2 + 35915213-5534-4277-81b8-1bdc9e7383d2 + + + + + + 1426 + 1161 + 102 + 20 + + + 1486.5 + 1171 + + + + + + + + 1 + true + Script input JointRules. + 962ac6c9-86fb-4b6f-bb76-9baafd776740 + 1 + JointRules + JointRules + true + 1 + true + 9ef43da6-ada7-4c17-a8ec-f321d9379183 + 1 + 35915213-5534-4277-81b8-1bdc9e7383d2 + + + + + + 1426 + 1181 + 102 + 20 + + + 1486.5 + 1191 + + + + + + + + 1 + true + Script input Features. + b9026523-5f20-46e2-9ab8-e0dc7abcdd97 + Features + Features + true + 1 + true + 0 + 35915213-5534-4277-81b8-1bdc9e7383d2 + + + + + + 1426 + 1201 + 102 + 20 + + + 1486.5 + 1211 + + + + + + + + true + Script input MaxDistance. + f9095c56-0432-4f5d-9a51-f28ccd44fe4c + MaxDistance + MaxDistance + true + 0 + true + 09833aa5-1994-4337-85d5-341624343d48 + 1 + 39fbc626-7a01-46ab-a18e-ec1c0c41685b + + + + + + 1426 + 1221 + 102 + 20 + + + 1486.5 + 1231 + + + + + + + + true + Script input CreateGeometry. + 72e58a16-32c5-47b7-8f31-dd3b2de0d42a + CreateGeometry + CreateGeometry + true + 0 + true + ee37f1a7-f247-4dd1-9926-3498f06eec02 + 1 + d60527f5-b5af-4ef6-8970-5f96fe412559 + + + + + + 1426 + 1241 + 102 + 20 + + + 1486.5 + 1251 + + + + + + + + Script output Model. + 420b8f1c-7df1-405b-a1f3-387f3716017a + Model + Model + false + 0 + + + + + + 1558 + 1161 + 62 + 33 + + + 1589 + 1177.667 + + + + + + + + Script output Geometry. + 486668a9-de54-49fb-b662-03694889d429 + Geometry + Geometry + false + 0 + + + + + + 1558 + 1194 + 62 + 33 + + + 1589 + 1211 + + + + + + + + Script output DebugInfo. + 6c5ffdba-503c-435e-9b82-201c7ce03292 + DebugInfo + DebugInfo + false + 0 + + + + + + 1558 + 1227 + 62 + 34 + + + 1589 + 1244.333 + + + + + + + + + + + + + + 2e78987b-9dfb-42a2-8b76-3923ac8bd91a + Boolean Toggle + + + + + Boolean (true/false) toggle + ee37f1a7-f247-4dd1-9926-3498f06eec02 + Boolean Toggle + Toggle + false + 0 + true + + + + + + 1302 + 1246 + 104 + 22 + + + + + + + + + + 57da07bd-ecab-415d-9d86-af36d7073abc + Number Slider + + + + + Numeric slider for single values + 09833aa5-1994-4337-85d5-341624343d48 + Number Slider + + false + 0 + + + + + + 1211 + 1221 + 195 + 20 + + + 1211.673 + 1221.001 + + + + + + 3 + 1 + 1 + 100 + 0 + 0 + 40 + + + + + + + + + 57da07bd-ecab-415d-9d86-af36d7073abc + Number Slider + + + + + Numeric slider for single values + 19c8f5b9-f818-4553-9fec-96ae2df55628 + true + Number Slider + + false + 0 + + + + + + 731 + 1253 + 184 + 20 + + + 731.2643 + 1253.693 + + + + + + 3 + 1 + 1 + 100 + 0 + 0 + 15 + + + + + + + + + 57da07bd-ecab-415d-9d86-af36d7073abc + Number Slider + + + + + Numeric slider for single values + 10946ab9-030f-41c8-8192-8fc73ecb49e4 + true + Number Slider + + false + 0 + + + + + + 731 + 1277 + 200 + 20 + + + 731.2643 + 1277.886 + + + + + + 3 + 1 + 1 + 100 + 0 + 0 + 60 + + + + + + + + + 57da07bd-ecab-415d-9d86-af36d7073abc + Number Slider + + + + + Numeric slider for single values + 697f731c-faf2-45c5-a9c4-a947431b741b + true + Number Slider + + false + 0 + + + + + + 731 + 1301 + 188 + 20 + + + 731.3165 + 1301.079 + + + + + + 3 + 1 + 1 + 100 + 0 + 0 + 28 + + + + + + + + + c552a431-af5b-46a9-a8a4-0fcbc27ef596 + Group + + + + + 1 + + 150;255;255;255 + + A group of Grasshopper objects + 19c8f5b9-f818-4553-9fec-96ae2df55628 + 10946ab9-030f-41c8-8192-8fc73ecb49e4 + 697f731c-faf2-45c5-a9c4-a947431b741b + 3 + 74f12b35-8c0f-4016-92da-4457ad7215e5 + Group + + + + + + + + + + + 57da07bd-ecab-415d-9d86-af36d7073abc + Number Slider + + + + + Numeric slider for single values + 13835904-23ad-43b2-a586-0ae141339040 + Number Slider + + false + 0 + + + + + + 828 + 452 + 166 + 20 + + + 828.7086 + 452.7064 + + + + + + 3 + 1 + 1 + 1000 + 0 + 0 + 120 + + + + + + + + + 57da07bd-ecab-415d-9d86-af36d7073abc + Number Slider + + + + + Numeric slider for single values + b0579e2b-7024-4c03-8e13-7ddcca5e08a6 + Number Slider + + false + 0 + + + + + + 827 + 396 + 162 + 20 + + + 827.0381 + 396.3297 + + + + + + 3 + 1 + 1 + 100 + 0 + 0 + 60 + + + + + + + + + 57da07bd-ecab-415d-9d86-af36d7073abc + Number Slider + + + + + Numeric slider for single values + 81d19eff-02b1-4406-a0c3-47dd897a152f + Number Slider + + false + 0 + + + + + + 825 + 570 + 162 + 20 + + + 825.3678 + 570.3737 + + + + + + 3 + 1 + 1 + 100 + 0 + 0 + 80 + + + + + + + + + 4f8984c4-7c7a-4d69-b0a2-183cbb330d20 + Plane + + + + + Contains a collection of three-dimensional axis-systems + 61d94f4d-5239-4d5e-a848-ab890c2a7194 + Plane + Pln + false + cc5eda18-08de-4b0d-91bd-9eefbfcc5dc7 + 1 + + + + + + 1891 + 103 + 50 + 24 + + + 1916.005 + 115.5407 + + + + + + + + + + 4f8984c4-7c7a-4d69-b0a2-183cbb330d20 + Plane + + + + + Contains a collection of three-dimensional axis-systems + a5781cd0-b305-4ed7-99aa-69c993f66d09 + Plane + Pln + false + a8110476-8bb3-4892-9423-09704614c7a3 + 1 + + + + + + 1882 + 169 + 50 + 24 + + + 1907.219 + 181.47 + + + + + + + + + + 59daf374-bc21-4a5e-8282-5504fb7ae9ae + List Item + + + + + 0 + Retrieve a specific item from a list. + true + 4140c91c-1f45-4cd9-88e8-e8e12d1f8da2 + List Item + Item + + + + + + 1952 + 285 + 74 + 124 + + + 1986 + 347 + + + + + + 3 + 8ec86459-bf01-4409-baee-174d0d2b13d0 + 2e3ab970-8545-46bb-836c-1c11e5610bce + cb95db89-6165-43b6-9c41-5702bc5bf137 + 6 + 8ec86459-bf01-4409-baee-174d0d2b13d0 + 8ec86459-bf01-4409-baee-174d0d2b13d0 + 8ec86459-bf01-4409-baee-174d0d2b13d0 + 8ec86459-bf01-4409-baee-174d0d2b13d0 + 8ec86459-bf01-4409-baee-174d0d2b13d0 + 8ec86459-bf01-4409-baee-174d0d2b13d0 + + + + + 1 + Base list + 8024f13e-5f87-450a-8acd-b1813506d465 + List + L + false + 39d6e475-c4b6-4330-98aa-9fdff81c5724 + 1 + + + + + + 1954 + 287 + 17 + 40 + + + 1964 + 307 + + + + + + + + Item index + e9f2b2f9-1321-4844-b859-c172906e7d3a + Index + i + false + 0 + + + + + + 1954 + 327 + 17 + 40 + + + 1964 + 347 + + + + + + 1 + + + + + 1 + {0} + + + + + 0 + + + + + + + + + + + Wrap index to list bounds + 2844a904-d023-453b-a70c-b31dc032c930 + Wrap + W + false + 0 + + + + + + 1954 + 367 + 17 + 40 + + + 1964 + 387 + + + + + + 1 + + + + + 1 + {0} + + + + + true + + + + + + + + + + + Item at {i'} + 67a488e6-002d-4aa5-b61d-394d61348dc5 + false + Item + i + false + 0 + + + + + + 2001 + 287 + 23 + 20 + + + 2012.5 + 297 + + + + + + + + Item at {+1'} + d0209c5d-de16-4aa2-b549-02ed8c1a1c40 + false + Item +1 + +1 + false + 0 + + + + + + 2001 + 307 + 23 + 20 + + + 2012.5 + 317 + + + + + + + + Item at {+2'} + 2e6fd5ba-24aa-449f-a464-5831bd9c15fd + false + Item +2 + +2 + false + 0 + + + + + + 2001 + 327 + 23 + 20 + + + 2012.5 + 337 + + + + + + + + Item at {+3'} + d1ec9303-8326-4408-afb6-c1f5634a5e22 + false + Item +3 + +3 + false + 0 + + + + + + 2001 + 347 + 23 + 20 + + + 2012.5 + 357 + + + + + + + + Item at {+4'} + e2d3c2fa-8609-48f0-aa19-696b94a64fa7 + false + Item +4 + +4 + false + 0 + + + + + + 2001 + 367 + 23 + 20 + + + 2012.5 + 377 + + + + + + + + Item at {+5'} + 970e00a8-0c70-4691-9ec4-ad4e788ebc0e + false + Item +5 + +5 + false + 0 + + + + + + 2001 + 387 + 23 + 20 + + + 2012.5 + 397 + + + + + + + + + + + + + + 410755b1-224a-4c1e-a407-bf32fb45ea7e + 00000000-0000-0000-0000-000000000000 + CT: WriteBTLx + + + + + import Rhino +from ghpythonlib.componentbase import executingcomponent as component +from Grasshopper.Kernel.GH_RuntimeMessageLevel import Warning + +from compas_timber.fabrication import BTLx + + +class WriteBTLx(component): + def RunScript(self, model, path, write): + if not model: + self.AddRuntimeMessage(Warning, "Input parameter Model failed to collect data") + return + + btlx = BTLx(model) + btlx.history["FileName"] = Rhino.RhinoDoc.ActiveDoc.Name + + if write: + if not path: + self.AddRuntimeMessage(Warning, "Input parameter Path failed to collect data") + return + if path[-5:] != ".btlx": + path += ".btlx" + with open(path, "w") as f: + f.write(btlx.btlx_string()) + return btlx.btlx_string() + + GhPython provides a Python script component + true + true + true + + iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAALDAAACwwBP0AiyAAAAShJREFUSEu1lFERwjAQRCsBAYhAQiUgAQlIQAGDBCQggQ8EVAIScBB2MxyzCZvwQbiZB/S6d9smF6aU0l+xyZHY5EhsciQ26UAcAne/hU06EPzI1Pd6TOvj7QxW7qbyi0ECyzeTXw3IHWyciNQGiBnEvuxeOf4ueqgBeYBZBW/hpwGbRe5aaS4gr0htEOQnUqQYl/m6Z0DytLUMyImCQItf126J3hrwAKuewRLN62LNK4id6sC+ZXA2xVrYG4ZFdAdnYE8q4qqFDQ2XLTRZVxt8bG6A4GRoMZ/0BPIegC24A9XMYcDxbL42yeKyWOHb6VSRS65DY57ibvMAwSfldGgjUhsU5+Dr/5DCQkAjNuQSsTm/Y2yLg1oU/wObHIlNjsQmR2KT40jTExovdFkNMNc1AAAAAElFTkSuQmCC + + false + 04a8d391-5de1-43a3-bc1e-9148489eba13 + true + true + CT: WriteBTLx + WriteBTLx + + + + + + 1732 + 1232 + 104 + 64 + + + 1787 + 1264 + + + + + + 3 + 84fa917c-1ed8-4db3-8be1-7bdc4a6495a2 + 84fa917c-1ed8-4db3-8be1-7bdc4a6495a2 + 84fa917c-1ed8-4db3-8be1-7bdc4a6495a2 + 1 + 8ec86459-bf01-4409-baee-174d0d2b13d0 + + + + + true + Model object. + a3efc012-177d-446f-8246-57f6fdc66a3e + Model + Model + true + 0 + true + 420b8f1c-7df1-405b-a1f3-387f3716017a + 1 + 35915213-5534-4277-81b8-1bdc9e7383d2 + + + + + + 1734 + 1234 + 38 + 20 + + + 1754.5 + 1244 + + + + + + + + true + Script input Path. + 9ec97830-a244-4a9a-9eaa-7025e1ca2aad + Path + Path + true + 0 + true + 6b5f5a99-98ff-4322-b2f6-76ed37c15310 + 1 + 35915213-5534-4277-81b8-1bdc9e7383d2 + + + + + + 1734 + 1254 + 38 + 20 + + + 1754.5 + 1264 + + + + + + + + true + Script input Write. + 15ed91b8-1bee-4f81-b07a-68f57f0c7c24 + Write + Write + true + 0 + true + fe63683a-84ba-462b-b688-9e3f081f52fb + 1 + d60527f5-b5af-4ef6-8970-5f96fe412559 + + + + + + 1734 + 1274 + 38 + 20 + + + 1754.5 + 1284 + + + + + + + + Script output BTLx. + ec68082b-6816-42e0-b242-ac0746b13092 + BTLx + BTLx + false + 0 + + + + + + 1802 + 1234 + 32 + 60 + + + 1818 + 1264 + + + + + + + + + + + + + + 59e0b89a-e487-49f8-bab8-b5bab16be14c + Panel + + + + + A panel for custom notes and text values + 6b5f5a99-98ff-4322-b2f6-76ed37c15310 + Panel + + false + 0 + 0 + D:\Papachap\Desktop\BTLx\TDovetailJoint.btlx + + + + + + 1472 + 1273 + 147 + 100 + + 0 + 0 + 0 + + 1472.726 + 1273.2 + + + + + + + 255;255;250;90 + + true + true + true + false + false + true + + + + + + + + + 2e78987b-9dfb-42a2-8b76-3923ac8bd91a + Boolean Toggle + + + + + Boolean (true/false) toggle + fe63683a-84ba-462b-b688-9e3f081f52fb + Boolean Toggle + Toggle + false + 0 + true + + + + + + 1536 + 1413 + 104 + 22 + + + + + + + + + + 4c4e56eb-2f04-43f9-95a3-cc46a14f495a + Line + + + + + Create a line between two points. + c34a085b-356b-4622-a2c2-8c93324c04f1 + Line + Ln + + + + + + 1975 + 54 + 63 + 44 + + + 2006 + 76 + + + + + + Line start point + 72e8522f-80cb-457b-91fe-8f0cc13c0ab7 + Start Point + A + false + 213adbf5-0592-48ab-b06a-06543b55b8da + 1 + + + + + + 1977 + 56 + 14 + 20 + + + 1985.5 + 66 + + + + + + + + Line end point + 47da55d9-8df7-4c19-b1e3-9e5d90055133 + End Point + B + false + a5781cd0-b305-4ed7-99aa-69c993f66d09 + 1 + + + + + + 1977 + 76 + 14 + 20 + + + 1985.5 + 86 + + + + + + + + Line segment + ce6dc462-63ba-4c7e-8670-c2e4d46bb69d + Line + L + false + 0 + + + + + + 2021 + 56 + 15 + 40 + + + 2028.5 + 76 + + + + + + + + + + + + a50fcd4a-cf42-4c3f-8616-022761e6cc93 + Deconstruct Vector + + + + + Deconstruct a vector into its component parts. + 35af3385-8992-4e83-b784-69eb595c78d1 + Deconstruct Vector + DeVec + + + + + + 1972 + -172 + 65 + 64 + + + 2003 + -140 + + + + + + Input vector + d91e9f12-67cf-4c4f-9fcd-db715d88daf4 + Vector + V + false + ce6dc462-63ba-4c7e-8670-c2e4d46bb69d + 1 + + + + + + 1974 + -170 + 14 + 60 + + + 1982.5 + -140 + + + + + + + + Vector {x} component + 56510800-a7a4-48c5-b46a-bc699b26dc50 + X component + X + false + 0 + + + + + + 2018 + -170 + 17 + 20 + + + 2026.5 + -160 + + + + + + + + Vector {y} component + 9c53ff46-e961-44c3-ae46-a4e377106bea + Y component + Y + false + 0 + + + + + + 2018 + -150 + 17 + 20 + + + 2026.5 + -140 + + + + + + + + Vector {z} component + 31ffbde6-9094-4a18-89bf-9a50fede1842 + Z component + Z + false + 0 + + + + + + 2018 + -130 + 17 + 20 + + + 2026.5 + -120 + + + + + + + + + + + + 59e0b89a-e487-49f8-bab8-b5bab16be14c + Panel + + + + + A panel for custom notes and text values + 587cf7d1-353f-422d-a57b-364604683ef6 + Panel + + false + 0 + 31ffbde6-9094-4a18-89bf-9a50fede1842 + 1 + Double click to edit panel content… + + + + + + 1935 + -88 + 160 + 100 + + 0 + 0 + 0 + + 1935.486 + -87.57 + + + + + + + 255;255;250;90 + + true + true + true + false + false + true + + + + + + + + + 8ec86459-bf01-4409-baee-174d0d2b13d0 + Data + + + + + Contains a collection of generic data + b89acdcc-fd6b-4db1-9f2d-3460b1c61f9f + Data + Data + false + 0 + + + + + + 2084 + -136 + 50 + 24 + + + 2109.122 + -123.692 + + + + + + 1 + + + + + 1 + {0;0} + + + + + Grasshopper.Kernel.Types.GH_Number + -123.85757554219845 + + + + + + + + + + + + + 56b92eab-d121-43f7-94d3-6cd8f0ddead8 + Vector XYZ + + + + + Create a vector from {xyz} components. + 1d8c0f26-dd97-4a50-aea3-d223cf427dea + Vector XYZ + Vec + + + + + + 1654 + -294 + 65 + 64 + + + 1685 + -262 + + + + + + Vector {x} component + 3c06af3c-420e-4235-9f23-2c5b6b8caf10 + X component + X + false + 81b2d562-f2e4-45fd-b695-c1cbaef757e4 + 1 + + + + + + 1656 + -292 + 14 + 20 + + + 1664.5 + -282 + + + + + + 1 + + + + + 1 + {0} + + + + + 0 + + + + + + + + + + + Vector {y} component + 4f98aafa-d128-4471-9453-1527c155e3e8 + Y component + Y + false + 57f61497-d466-442e-8d37-c9954804167d + 1 + + + + + + 1656 + -272 + 14 + 20 + + + 1664.5 + -262 + + + + + + 1 + + + + + 1 + {0} + + + + + 0 + + + + + + + + + + + Vector {z} component + 19ca8d8b-7ee4-443a-b1ed-32a9a0c65f0d + Z component + Z + false + 8def7c05-9133-40c5-a740-0ed9e7a41229 + 1 + + + + + + 1656 + -252 + 14 + 20 + + + 1664.5 + -242 + + + + + + 1 + + + + + 1 + {0} + + + + + 0 + + + + + + + + + + + Vector construct + 260659d9-0c72-4130-820d-16c404b4e462 + Vector + V + false + 0 + + + + + + 1700 + -292 + 17 + 30 + + + 1708.5 + -277 + + + + + + + + Vector length + 83e0c8b0-74fa-4620-9852-7e57c1f4586e + Length + L + false + 0 + + + + + + 1700 + -262 + 17 + 30 + + + 1708.5 + -247 + + + + + + + + + + + + 56b92eab-d121-43f7-94d3-6cd8f0ddead8 + Vector XYZ + + + + + Create a vector from {xyz} components. + 55af1f9d-c8f0-4329-979f-c4fda3f1fd9e + Vector XYZ + Vec + + + + + + 1655 + -205 + 65 + 64 + + + 1686 + -173 + + + + + + Vector {x} component + 77215e04-a8d4-45cd-93d8-886b01c83e71 + X component + X + false + 6655ae15-3d89-4b93-99e5-c57fcab0da34 + 1 + + + + + + 1657 + -203 + 14 + 20 + + + 1665.5 + -193 + + + + + + 1 + + + + + 1 + {0} + + + + + 0 + + + + + + + + + + + Vector {y} component + 7e327792-ec79-4823-aa41-ab4303285025 + Y component + Y + false + b82aee47-37f2-4478-8eeb-89065f3d2272 + 1 + + + + + + 1657 + -183 + 14 + 20 + + + 1665.5 + -173 + + + + + + 1 + + + + + 1 + {0} + + + + + 0 + + + + + + + + + + + Vector {z} component + c9829849-c054-420a-8dce-cdf7bded82eb + Z component + Z + false + 8a0da811-cdf8-40a9-8bf6-b9ca6e808e93 + 1 + + + + + + 1657 + -163 + 14 + 20 + + + 1665.5 + -153 + + + + + + 1 + + + + + 1 + {0} + + + + + 0 + + + + + + + + + + + Vector construct + ab9d29a4-cf3d-48d5-af54-608beb680f54 + Vector + V + false + 0 + + + + + + 1701 + -203 + 17 + 30 + + + 1709.5 + -188 + + + + + + + + Vector length + b47023cf-4dd2-4a55-a2d9-f8b3bc343308 + Length + L + false + 0 + + + + + + 1701 + -173 + 17 + 30 + + + 1709.5 + -158 + + + + + + + + + + + + bc3e379e-7206-4e7b-b63a-ff61f4b38a3e + Construct Plane + + + + + Construct a plane from an origin point and {x}, {y} axes. + 2e6c7a1b-d345-4966-b81c-10969bf5f760 + Construct Plane + Pl + + + + + + 1751 + -309 + 68 + 64 + + + 1783 + -277 + + + + + + Origin of plane + 67ddfd78-b229-4feb-965e-3344a07f7190 + Origin + O + false + 86990d0f-35da-4def-8414-369478a9cd09 + 1 + + + + + + 1753 + -307 + 15 + 20 + + + 1762 + -297 + + + + + + 1 + + + + + 1 + {0} + + + + + + + 0 + 0 + 0 + + + + + + + + + + + + X-Axis direction of plane + d3b445f2-8916-453d-a6b3-bdd628258c0b + X-Axis + X + false + 260659d9-0c72-4130-820d-16c404b4e462 + 1 + + + + + + 1753 + -287 + 15 + 20 + + + 1762 + -277 + + + + + + 1 + + + + + 1 + {0} + + + + + + 1 + 0 + 0 + + + + + + + + + + + + Y-Axis direction of plane + 20543322-9a73-4694-9338-0c99d9169227 + Y-Axis + Y + false + ab9d29a4-cf3d-48d5-af54-608beb680f54 + 1 + + + + + + 1753 + -267 + 15 + 20 + + + 1762 + -257 + + + + + + 1 + + + + + 1 + {0} + + + + + + 0 + 1 + 0 + + + + + + + + + + + + Constructed plane + a029cb6f-5b4e-49f4-88b4-5b60063bbc9f + Plane + Pl + false + 0 + + + + + + 1798 + -307 + 19 + 60 + + + 1807.5 + -277 + + + + + + + + + + + + 3581f42a-9592-4549-bd6b-1c0fc39d067b + Construct Point + + + + + Construct a point from {xyz} coordinates. + 68be8389-b3bd-406f-b874-05df8e51d3f4 + Construct Point + Pt + + + + + + 1660 + -379 + 68 + 64 + + + 1691 + -347 + + + + + + {x} coordinate + 7b110d33-1894-45b6-bc07-f08da80e6257 + X coordinate + X + false + 559cb751-0e45-4685-808e-8040a029dde6 + 1 + + + + + + 1662 + -377 + 14 + 20 + + + 1670.5 + -367 + + + + + + 1 + + + + + 1 + {0} + + + + + 0 + + + + + + + + + + + {y} coordinate + e2e25de6-ed1c-42b0-aa56-37f0fe63a41b + Y coordinate + Y + false + 8ebde2e8-e3d0-4cf0-be05-661ffc26be01 + 1 + + + + + + 1662 + -357 + 14 + 20 + + + 1670.5 + -347 + + + + + + 1 + + + + + 1 + {0} + + + + + 0 + + + + + + + + + + + {z} coordinate + 874021cb-57cf-4bf0-9ce7-91d99fdeaca3 + Z coordinate + Z + false + fe2b2e28-edc4-464a-9506-878d8c6f1c5e + 1 + + + + + + 1662 + -337 + 14 + 20 + + + 1670.5 + -327 + + + + + + 1 + + + + + 1 + {0} + + + + + 0 + + + + + + + + + + + Point coordinate + 86990d0f-35da-4def-8414-369478a9cd09 + Point + Pt + false + 0 + + + + + + 1706 + -377 + 20 + 60 + + + 1716 + -347 + + + + + + + + + + + + 57da07bd-ecab-415d-9d86-af36d7073abc + Number Slider + + + + + Numeric slider for single values + 559cb751-0e45-4685-808e-8040a029dde6 + Number Slider + + false + 0 + + + + + + 1388 + -419 + 195 + 20 + + + 1388 + -418.8 + + + + + + 1 + 1 + 0 + 100000 + 0 + 0 + 31332.1 + + + + + + + + + 9c007a04-d0d9-48e4-9da3-9ba142bc4d46 + Subtraction + + + + + Mathematical subtraction + 8d08acff-fbba-4a7b-a85e-5d28d9b37d13 + Subtraction + A-B + + + + + + 1321 + -382 + 65 + 44 + + + 1352 + -360 + + + + + + 2 + 8ec86459-bf01-4409-baee-174d0d2b13d0 + 8ec86459-bf01-4409-baee-174d0d2b13d0 + 1 + 8ec86459-bf01-4409-baee-174d0d2b13d0 + + + + + First operand for subtraction + f1996b6d-7e59-4e9b-872b-daa2abd15661 + A + A + true + 0 + + + + + + 1323 + -380 + 14 + 20 + + + 1331.5 + -370 + + + + + + + + Second operand for subtraction + 314c5234-8b41-43d3-b455-f3517783d8dc + B + B + true + 0 + + + + + + 1323 + -360 + 14 + 20 + + + 1331.5 + -350 + + + + + + 1 + + + + + 1 + {0} + + + + + Grasshopper.Kernel.Types.GH_Number + 3287.543 + + + + + + + + + + + Result of subtraction + 0f236a19-5b41-4c02-8f8a-3feb4a99d165 + Result + R + false + 0 + + + + + + 1367 + -380 + 17 + 40 + + + 1375.5 + -360 + + + + + + + + + + + + + + a3371040-e552-4bc8-b0ff-10a840258e88 + Negative + + + + + Compute the negative of a value. + f81ccc3f-2b82-494a-844a-8425db0adb84 + Negative + Neg + + + + + + 1436 + -370 + 61 + 28 + + + 1465 + -356 + + + + + + Input value + b882bd9d-3905-411d-b237-c608471237b6 + Value + x + false + 0f236a19-5b41-4c02-8f8a-3feb4a99d165 + 1 + + + + + + 1438 + -368 + 12 + 24 + + + 1445.5 + -356 + + + + + + + + Output value + 8ebde2e8-e3d0-4cf0-be05-661ffc26be01 + Result + y + false + 0 + + + + + + 1480 + -368 + 15 + 24 + + + 1487.5 + -356 + + + + + + + + + + + + 57da07bd-ecab-415d-9d86-af36d7073abc + Number Slider + + + + + Numeric slider for single values + fe2b2e28-edc4-464a-9506-878d8c6f1c5e + Number Slider + + false + 0 + + + + + + 1354 + -338 + 195 + 20 + + + 1354.4 + -337.2 + + + + + + 4 + 1 + 0 + 100 + 0 + 0 + 54.3408 + + + + + + + + + 57da07bd-ecab-415d-9d86-af36d7073abc + Number Slider + + + + + Numeric slider for single values + f91d1868-865b-4b09-84ee-18c9589f121f + Number Slider + + false + 0 + + + + + + 1324 + -298 + 160 + 20 + + + 1324.4 + -297.2 + + + + + + 4 + 1 + 0 + 1 + 0 + 0 + 0.9397 + + + + + + + + + a3371040-e552-4bc8-b0ff-10a840258e88 + Negative + + + + + Compute the negative of a value. + 7133e965-5be9-44f6-9acc-ef9f5ab72d6e + Negative + Neg + + + + + + 1525 + -302 + 61 + 28 + + + 1554 + -288 + + + + + + Input value + be7ffc81-5af3-4599-9452-76d7182da4fb + Value + x + false + f91d1868-865b-4b09-84ee-18c9589f121f + 1 + + + + + + 1527 + -300 + 12 + 24 + + + 1534.5 + -288 + + + + + + + + Output value + 81b2d562-f2e4-45fd-b695-c1cbaef757e4 + Result + y + false + 0 + + + + + + 1569 + -300 + 15 + 24 + + + 1576.5 + -288 + + + + + + + + + + + + 57da07bd-ecab-415d-9d86-af36d7073abc + Number Slider + + + + + Numeric slider for single values + 57f61497-d466-442e-8d37-c9954804167d + Number Slider + + false + 0 + + + + + + 1324 + -259 + 198 + 20 + + + 1324.344 + -258.8 + + + + + + 4 + 1 + 0 + 10 + 0 + 0 + 0 + + + + + + + + + 57da07bd-ecab-415d-9d86-af36d7073abc + Number Slider + + + + + Numeric slider for single values + 21ef7a03-065e-4239-9090-d0ec4672a335 + Number Slider + + false + 0 + + + + + + 1324 + -226 + 160 + 20 + + + 1324.8 + -226 + + + + + + 3 + 1 + 0 + 1 + 0 + 0 + 0.342 + + + + + + + + + a3371040-e552-4bc8-b0ff-10a840258e88 + Negative + + + + + Compute the negative of a value. + dbc590d5-832a-4015-b497-96f86456345b + Negative + Neg + + + + + + 1522 + -230 + 61 + 28 + + + 1551 + -216 + + + + + + Input value + c1f8e07d-4f57-4e12-a867-3db1ef9b622b + Value + x + false + 21ef7a03-065e-4239-9090-d0ec4672a335 + 1 + + + + + + 1524 + -228 + 12 + 24 + + + 1531.5 + -216 + + + + + + + + Output value + 8def7c05-9133-40c5-a740-0ed9e7a41229 + Result + y + false + 0 + + + + + + 1566 + -228 + 15 + 24 + + + 1573.5 + -216 + + + + + + + + + + + + 57da07bd-ecab-415d-9d86-af36d7073abc + Number Slider + + + + + Numeric slider for single values + 41f882f0-98c7-4dee-a166-f1d8a7ac30cb + Number Slider + + false + 0 + + + + + + 1320 + -184 + 160 + 20 + + + + + + 4 + 1 + 0 + 1 + 0 + 0 + 0.342 + + + + + + + + + a3371040-e552-4bc8-b0ff-10a840258e88 + Negative + + + + + Compute the negative of a value. + 70c11d4f-3dab-4c19-a059-cac49c426f21 + Negative + Neg + + + + + + 1520 + -189 + 61 + 28 + + + 1549 + -175 + + + + + + Input value + f45919ba-f892-4023-a0cf-bfc2a641959f + Value + x + false + 41f882f0-98c7-4dee-a166-f1d8a7ac30cb + 1 + + + + + + 1522 + -187 + 12 + 24 + + + 1529.5 + -175 + + + + + + + + Output value + 6655ae15-3d89-4b93-99e5-c57fcab0da34 + Result + y + false + 0 + + + + + + 1564 + -187 + 15 + 24 + + + 1571.5 + -175 + + + + + + + + + + + + 57da07bd-ecab-415d-9d86-af36d7073abc + Number Slider + + + + + Numeric slider for single values + b82aee47-37f2-4478-8eeb-89065f3d2272 + Number Slider + + false + 0 + + + + + + 1319 + -146 + 198 + 20 + + + 1319.944 + -145.6 + + + + + + 4 + 1 + 0 + 10 + 0 + 0 + 0 + + + + + + + + + 57da07bd-ecab-415d-9d86-af36d7073abc + Number Slider + + + + + Numeric slider for single values + 8a0da811-cdf8-40a9-8bf6-b9ca6e808e93 + Number Slider + + false + 0 + + + + + + 1320 + -113 + 198 + 20 + + + 1320.4 + -112.8 + + + + + + 3 + 1 + 0 + 1 + 0 + 0 + 0.94 + + + + + + + + + bc3e379e-7206-4e7b-b63a-ff61f4b38a3e + Construct Plane + + + + + Construct a plane from an origin point and {x}, {y} axes. + ecac92db-7448-402c-b3cf-ff3e6e6899df + Construct Plane + Pl + + + + + + 1778 + -467 + 68 + 64 + + + 1810 + -435 + + + + + + Origin of plane + 499578c3-f0b0-4532-ad9c-d8f29a0317f3 + Origin + O + false + 86990d0f-35da-4def-8414-369478a9cd09 + 1 + + + + + + 1780 + -465 + 15 + 20 + + + 1789 + -455 + + + + + + 1 + + + + + 1 + {0} + + + + + + + 0 + 0 + 0 + + + + + + + + + + + + X-Axis direction of plane + 9ab3ee88-a0cf-4f8c-9c00-655609c50c66 + X-Axis + X + false + 0 + + + + + + 1780 + -445 + 15 + 20 + + + 1789 + -435 + + + + + + 1 + + + + + 1 + {0} + + + + + + 1 + 0 + 0 + + + + + + + + + + + + Y-Axis direction of plane + 1242e202-ab1c-4bd4-8758-3e8a01c6ad1e + Y-Axis + Y + false + 0 + + + + + + 1780 + -425 + 15 + 20 + + + 1789 + -415 + + + + + + 1 + + + + + 1 + {0} + + + + + + 0 + 1 + 0 + + + + + + + + + + + + Constructed plane + 95c8982c-bf0e-42d6-878b-a91bdf0ce279 + Plane + Pl + false + 0 + + + + + + 1825 + -465 + 19 + 60 + + + 1834.5 + -435 + + + + + + + + + + + + b464fccb-50e7-41bd-9789-8438db9bea9f + Angle + + + + + Compute the angle between two vectors. + 675ea98b-dcbe-4567-8b05-b956161c3abd + Angle + Angle + + + + + + 1934 + -370 + 65 + 64 + + + 1965 + -338 + + + + + + First vector + f7ce62cc-b107-4528-850c-5262e0477a2f + Vector A + A + false + 72567da6-0377-4dd3-813f-06c874b0500a + 1 + + + + + + 1936 + -368 + 14 + 20 + + + 1944.5 + -358 + + + + + + + + Second vector + 0ec1d0fe-fb8d-4dc7-a70b-5413f7315163 + Vector B + B + false + 260659d9-0c72-4130-820d-16c404b4e462 + 1 + + + + + + 1936 + -348 + 14 + 20 + + + 1944.5 + -338 + + + + + + + + Optional plane for 2D angle + 600f5434-81cc-4422-8787-742266d9cc43 + Plane + P + true + 0 + + + + + + 1936 + -328 + 14 + 20 + + + 1944.5 + -318 + + + + + + + + Angle (in radians) between vectors + 8b46d8d5-832a-4eeb-a447-207623299a11 + Angle + A + false + 0 + + + + + + 1980 + -368 + 17 + 30 + + + 1988.5 + -353 + + + + + + + + Reflex angle (in radians) between vectors + 603840f6-b261-4ebc-aa08-c748d4d266db + Reflex + R + false + 0 + + + + + + 1980 + -338 + 17 + 30 + + + 1988.5 + -323 + + + + + + + + + + + + c73e1ed0-82a2-40b0-b4df-8f10e445d60b + Flip Plane + + + + + Flip or swap the axes of a plane + 40b4dcec-ab3d-48ae-900a-7fa7065785dd + Flip Plane + PFlip + + + + + + 1863 + -504 + 80 + 84 + + + 1910 + -462 + + + + + + Plane to adjust + 0dbcaff4-7f38-4225-a0a9-bee4947ea2a8 + Plane + P + false + 95c8982c-bf0e-42d6-878b-a91bdf0ce279 + 1 + + + + + + 1865 + -502 + 30 + 20 + + + 1889.5 + -492 + + + + + + 1 + + + + + 1 + {0} + + + + + + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + + + + + + + + + + + + Reverse the x-axis direction + e7d8f9ef-4171-4b1a-ab69-4a15ed947dfc + true + Reverse X + X + false + 0 + + + + + + 1865 + -482 + 30 + 20 + + + 1889.5 + -472 + + + + + + 1 + + + + + 1 + {0} + + + + + false + + + + + + + + + + + Reverse the y-axis direction + 55be9c4c-99e0-4792-848b-97dc21ccbd72 + Reverse Y + Y + false + 0 + + + + + + 1865 + -462 + 30 + 20 + + + 1889.5 + -452 + + + + + + 1 + + + + + 1 + {0} + + + + + false + + + + + + + + + + + Swap the x and y axis directions + 014e4a00-fc61-439a-a08d-ee676ad1a7c0 + true + Swap axes + S + false + 0 + + + + + + 1865 + -442 + 30 + 20 + + + 1889.5 + -432 + + + + + + 1 + + + + + 1 + {0} + + + + + true + + + + + + + + + + + Flipped plane + 3d42bdab-d3a7-4999-9fa1-5775536451be + Plane + P + false + 0 + + + + + + 1925 + -502 + 16 + 80 + + + 1933 + -462 + + + + + + + + + + + + 0d77c51e-584f-44e8-aed2-c2ddf4803888 + Degrees + + + + + Convert an angle specified in radians to degrees + 67c0d4e8-0067-4e24-a46a-0639442d92fb + Degrees + Deg + + + + + + 2016 + -367 + 66 + 28 + + + 2047 + -353 + + + + + + Angle in radians + 3f198962-b4e3-4cde-a697-da7232934f3f + Radians + R + false + 8b46d8d5-832a-4eeb-a447-207623299a11 + 1 + + + + + + 2018 + -365 + 14 + 24 + + + 2026.5 + -353 + + + + + + + + Angle in degrees + 900686d0-979a-486d-9fa9-1bfcfc37d4d2 + Degrees + D + false + 0 + + + + + + 2062 + -365 + 18 + 24 + + + 2071 + -353 + + + + + + + + + + + + a50fcd4a-cf42-4c3f-8616-022761e6cc93 + Deconstruct Vector + + + + + Deconstruct a vector into its component parts. + 9ecc241f-225a-4955-acf8-1678ddef6b39 + Deconstruct Vector + DeVec + + + + + + 1989 + -494 + 65 + 64 + + + 2020 + -462 + + + + + + Input vector + cfbe4b74-1485-474d-ad23-b0354f98ed2b + Vector + V + false + 95c8982c-bf0e-42d6-878b-a91bdf0ce279 + 1 + + + + + + 1991 + -492 + 14 + 60 + + + 1999.5 + -462 + + + + + + + + Vector {x} component + 129fe0e4-95ad-4604-91ff-48cb3619ef0b + X component + X + false + 0 + + + + + + 2035 + -492 + 17 + 20 + + + 2043.5 + -482 + + + + + + + + Vector {y} component + e1aaa859-ee1d-4b6c-945d-5977b6e6bc78 + Y component + Y + false + 0 + + + + + + 2035 + -472 + 17 + 20 + + + 2043.5 + -462 + + + + + + + + Vector {z} component + e11b04e9-327f-4e26-9d53-af632808ba50 + Z component + Z + false + 0 + + + + + + 2035 + -452 + 17 + 20 + + + 2043.5 + -442 + + + + + + + + + + + + a50fcd4a-cf42-4c3f-8616-022761e6cc93 + Deconstruct Vector + + + + + Deconstruct a vector into its component parts. + cc4f2080-8879-46f9-9bd4-33fe0e6f06a7 + Deconstruct Vector + DeVec + + + + + + 1847 + -309 + 65 + 64 + + + 1878 + -277 + + + + + + Input vector + 25f78783-acd2-4be5-8d67-cb35df6adacc + Vector + V + false + a029cb6f-5b4e-49f4-88b4-5b60063bbc9f + 1 + + + + + + 1849 + -307 + 14 + 60 + + + 1857.5 + -277 + + + + + + + + Vector {x} component + 2d5ca2ed-4cdf-4467-a388-6b1ce2a32327 + X component + X + false + 0 + + + + + + 1893 + -307 + 17 + 20 + + + 1901.5 + -297 + + + + + + + + Vector {y} component + 4c32d96e-93e6-4032-a2eb-b03984ab1412 + Y component + Y + false + 0 + + + + + + 1893 + -287 + 17 + 20 + + + 1901.5 + -277 + + + + + + + + Vector {z} component + 06bb0fa2-4579-46a1-a5c6-328b32ee8b4f + Z component + Z + false + 0 + + + + + + 1893 + -267 + 17 + 20 + + + 1901.5 + -257 + + + + + + + + + + + + 59e0b89a-e487-49f8-bab8-b5bab16be14c + Panel + + + + + A panel for custom notes and text values + 859bdc6a-fb55-4907-95fa-42bcb11b161e + Panel + + false + 0 + 0 + 1 + + + + + + 1791 + -368 + 36 + 20 + + 0 + 0 + 0 + + 1791.14 + -367.948 + + + + + + + 255;255;250;90 + + true + true + true + false + false + true + + + + + + + + + 79f9fbb3-8f1d-4d9a-88a9-f7961b1012cd + Unit X + + + + + Unit vector parallel to the world {x} axis. + 4528f945-8eab-4d55-b3a6-a9b08362cacf + Unit X + X + + + + + + 1849 + -372 + 63 + 28 + + + 1878 + -358 + + + + + + Unit multiplication + bb427121-1130-486d-a1a7-a496ae0e9a39 + Factor + F + false + 859bdc6a-fb55-4907-95fa-42bcb11b161e + 1 + + + + + + 1851 + -370 + 12 + 24 + + + 1858.5 + -358 + + + + + + 1 + + + + + 1 + {0} + + + + + 1 + + + + + + + + + + + World {x} vector + 72567da6-0377-4dd3-813f-06c874b0500a + Unit vector + V + false + 0 + + + + + + 1893 + -370 + 17 + 24 + + + 1901.5 + -358 + + + + + + + + + + + + 22990b1f-9be6-477c-ad89-f775cd347105 + Flip Curve + + + + + Flip a curve using an optional guide curve. + true + 906af723-08ba-430d-ad6e-ce07b5200482 + Flip Curve + Flip + + + + + + 306 + 247 + 66 + 44 + + + 338 + 269 + + + + + + Curve to flip + bd1ba6e6-2103-43e7-879b-abc57ddb7e9b + Curve + C + false + e2fec716-2636-48c7-b126-85ba3955f210 + 1 + + + + + + 308 + 249 + 15 + 20 + + + 317 + 259 + + + + + + + + Optional guide curve + 5008ee96-d5d7-4eb5-9a37-ea1e8e51e171 + Guide + G + true + 0 + + + + + + 308 + 269 + 15 + 20 + + + 317 + 279 + + + + + + + + Flipped curve + acefae73-90c1-40be-b9e0-9dacb241a229 + Curve + C + false + 0 + + + + + + 353 + 249 + 17 + 20 + + + 361.5 + 259 + + + + + + + + Flip action + cf03ee7a-3fab-46f1-ad0d-ca67432e84fe + Flag + F + false + 0 + + + + + + 353 + 269 + 17 + 20 + + + 361.5 + 279 + + + + + + + + + + + + 2e78987b-9dfb-42a2-8b76-3923ac8bd91a + Boolean Toggle + + + + + Boolean (true/false) toggle + f0df0e81-ffad-41f2-a721-b13a2d9713ac + Boolean Toggle + FlipCurve + false + 0 + false + + + + + + 173 + 201 + 115 + 22 + + + + + + + + + + eeafc956-268e-461d-8e73-ee05c6f72c01 + Stream Filter + + + + + Filters a collection of input streams + true + c8935f57-0127-4738-af16-55bef27daede + Stream Filter + Filter + + + + + + 384 + 207 + 77 + 64 + + + 416 + 239 + + + + + + 3 + 2e3ab970-8545-46bb-836c-1c11e5610bce + 8ec86459-bf01-4409-baee-174d0d2b13d0 + 8ec86459-bf01-4409-baee-174d0d2b13d0 + 1 + 8ec86459-bf01-4409-baee-174d0d2b13d0 + + + + + Index of Gate stream + 2150f2e4-0536-4061-bcc7-82a78f2ce165 + Gate + G + false + f0df0e81-ffad-41f2-a721-b13a2d9713ac + 1 + + + + + + 386 + 209 + 15 + 20 + + + 395 + 219 + + + + + + 1 + + + + + 1 + {0} + + + + + 0 + + + + + + + + + + + 2 + Input stream at index 0 + 52165d88-897d-4411-b229-40c33d6dcacf + false + Stream 0 + 0 + true + e2fec716-2636-48c7-b126-85ba3955f210 + 1 + + + + + + 386 + 229 + 15 + 20 + + + 395 + 239 + + + + + + + + 2 + Input stream at index 1 + ef6ce42a-3047-4a1a-9ecd-2c6bcc799ad5 + false + Stream 1 + 1 + true + acefae73-90c1-40be-b9e0-9dacb241a229 + 1 + + + + + + 386 + 249 + 15 + 20 + + + 395 + 259 + + + + + + + + 2 + Filtered stream + 36c4deda-ceed-4920-a3f3-2d18ca613b35 + false + Stream + S(0) + false + 0 + + + + + + 431 + 209 + 28 + 60 + + + 445 + 239 + + + + + + + + + + + + + + d5967b9f-e8ee-436b-a8ad-29fdcecf32d5 + Curve + + + + + Contains a collection of generic curves + true + e2fec716-2636-48c7-b126-85ba3955f210 + Curve + Crv + false + 18d2fa4a-de1b-4cfd-bf80-4b8d5fa0182f + 1 + + + + + + 236 + 227 + 50 + 24 + + + 261.4741 + 239.9949 + + + + + + 1 + + + + + 1 + {0} + + + + + -1 + + Y2BkYGD4DwQgGgR4mIBEeFBGZl6+c35ubn6ejkJYalFxZn6erbmesYmekbGRmbmeoaGBgaGOgnNpTklpUaptXmppSVFijo5CQGlSTmayd2plSH52ap6tqamRkYVhqqV5srmpqamxASvIFhmw4Xruqfm5qSVFlXoB+TmVOZl5qc6lRWWpLEAF7GUQC7kSi5IzMstSjVNyOfMLUvPySouSillSEksSQYo4ODiYQG4VUGdgMALS9f/4eDiZgQx+EDEViJl+1TMxdEL99fs/E4MIlP3synU/95eXBfc/ZRBoZlT64Ddn9+0/QPlAqLwAyNy4bxpcrDPvOXz4/18+rHjlAagcQwpU3KHb70cS13q4OBMDAuxtjlpjZzvZAeSS6oIuv//1TA0wOW6GwQQA + + 00000000-0000-0000-0000-000000000000 + + + + + + + + + + + + + c552a431-af5b-46a9-a8a4-0fcbc27ef596 + Group + + + + + 1 + + 150;255;255;255 + + A group of Grasshopper objects + e2fec716-2636-48c7-b126-85ba3955f210 + f0df0e81-ffad-41f2-a721-b13a2d9713ac + 2 + c4bae22a-eb6f-4c74-a53a-12421686e963 + Group + + + + + + + + + + + 59e0b89a-e487-49f8-bab8-b5bab16be14c + Panel + + + + + A panel for custom notes and text values + e96e7f8c-fbda-4b4e-b878-1e3a5af5d17b + Panel + BTLx Params + false + 0 + 9d870029-3b28-4cf0-b9f2-93c9376334f2 + 1 + Double click to edit panel content… + + + + + + 2611 + -367 + 206 + 333 + + 0 + 0 + 0 + + 2611.206 + -366.2764 + + + + + + + 255;255;255;255 + + true + true + true + false + false + true + + + + + + + + + 8ec86459-bf01-4409-baee-174d0d2b13d0 + Data + + + + + Contains a collection of generic data + 9d870029-3b28-4cf0-b9f2-93c9376334f2 + Data + Data + false + 0 + + + + + + 2123 + -273 + 50 + 24 + + + 2148 + -260.8 + + + + + + 1 + + + + + 20 + {0;0;0} + + + + + Grasshopper.Kernel.Types.GH_String + false + Name: DovetailMortise + + + + + Grasshopper.Kernel.Types.GH_String + false + Process: yes + + + + + Grasshopper.Kernel.Types.GH_String + false + Priority: 0 + + + + + Grasshopper.Kernel.Types.GH_String + false + ProcessID: 0 + + + + + Grasshopper.Kernel.Types.GH_String + false + ReferencePlaneID: 4 + + + + + Grasshopper.Kernel.Types.GH_String + false + StartX: 549.649 + + + + + Grasshopper.Kernel.Types.GH_String + false + StartY: 19.205 + + + + + Grasshopper.Kernel.Types.GH_String + false + StartDepth: 0.000 + + + + + Grasshopper.Kernel.Types.GH_String + false + Angle: 90.000 + + + + + Grasshopper.Kernel.Types.GH_String + false + Slope: 90.000 + + + + + Grasshopper.Kernel.Types.GH_String + false + LimitationTop: limited + + + + + Grasshopper.Kernel.Types.GH_String + false + LengthLimitedBottom: yes + + + + + Grasshopper.Kernel.Types.GH_String + false + Length: 60.000 + + + + + Grasshopper.Kernel.Types.GH_String + false + Width: 45.029 + + + + + Grasshopper.Kernel.Types.GH_String + false + Depth: 28.000 + + + + + Grasshopper.Kernel.Types.GH_String + false + ConeAngle: 10.000 + + + + + Grasshopper.Kernel.Types.GH_String + false + UseFlankAngle: no + + + + + Grasshopper.Kernel.Types.GH_String + false + FlankAngle: 15.000 + + + + + Grasshopper.Kernel.Types.GH_String + false + Shape: automatic + + + + + Grasshopper.Kernel.Types.GH_String + false + ShapeRadius: 22.514 + + + + + + + + + + + + + 6b7ba278-5c9d-42f1-a61d-6209cbd44907 + Curve Proximity + + + + + Find the pair of closest points between two curves. + f108ae57-2009-4f60-8f2d-15faa523d976 + Curve Proximity + CrvProx + + + + + + 667 + 342 + 66 + 64 + + + 698 + 374 + + + + + + First curve + c738ea7a-d604-42f9-a5ef-30c6c6ddce02 + Curve A + A + false + 36c4deda-ceed-4920-a3f3-2d18ca613b35 + 1 + + + + + + 669 + 344 + 14 + 30 + + + 677.5 + 359 + + + + + + + + Second curve + 71da35f8-2834-47e3-a13e-7a2936ab42c0 + Curve B + B + false + d2d51328-18f6-4ef3-8dd6-1df6b6af922e + 1 + + + + + + 669 + 374 + 14 + 30 + + + 677.5 + 389 + + + + + + + + Point on curve A closest to curve B + 7542c648-8889-4d83-8f10-9e8b260fa85a + Point A + A + false + 0 + + + + + + 713 + 344 + 18 + 20 + + + 722 + 354 + + + + + + + + Point on curve B closest to curve A + 3819d11f-35d9-4e55-b397-0509416544db + Point B + B + false + 0 + + + + + + 713 + 364 + 18 + 20 + + + 722 + 374 + + + + + + + + Smallest distance between two curves + 51674317-ad4b-4b8d-aefb-154a53c3be9f + Distance + D + false + 0 + + + + + + 713 + 384 + 18 + 20 + + + 722 + 394 + + + + + + + + + + + + 919e146f-30ae-4aae-be34-4d72f555e7da + Brep + + + + + Contains a collection of Breps (Boundary REPresentations) + 8bd29712-ff83-438b-b8f1-1d5f93dba350 + Brep + Brep + false + 389410e6-fbd0-4b13-ad00-528c4c2075fd + 1 + + + + + + 1878 + 304 + 50 + 24 + + + 1903.265 + 316.8058 + + + + + + + + + + d2da1306-259a-4994-85a4-672d8a4c7805 + Unit Vector + + + + + Unitize vector. + a9436695-3954-44b4-aae3-32ec284e7381 + Unit Vector + Unit + + + + + + 788 + 272 + 65 + 28 + + + 819 + 286 + + + + + + Base vector + 6016a9d4-590c-4c16-9666-d6f61bb42855 + Vector + V + false + 4c90d06e-9810-41a6-baa8-91b753ccec55 + 1 + + + + + + 790 + 274 + 14 + 24 + + + 798.5 + 286 + + + + + + + + Unit vector + 7340ce74-ba19-4774-8009-78fd26d19e4b + Vector + V + false + 0 + + + + + + 834 + 274 + 17 + 24 + + + 842.5 + 286 + + + + + + + + + + + + 59e0b89a-e487-49f8-bab8-b5bab16be14c + Panel + + + + + A panel for custom notes and text values + 2344469e-f0a8-4b55-bd93-416708bd4b8f + Panel + + false + 0 + 7340ce74-ba19-4774-8009-78fd26d19e4b + 1 + Double click to edit panel content… + + + + + + 714 + 146 + 187 + 100 + + 0 + 0 + 0 + + 714.4128 + 146.7059 + + + + + + + 255;255;250;90 + + true + true + true + false + false + true + + + + + + + + + 59e0b89a-e487-49f8-bab8-b5bab16be14c + Panel + + + + + A panel for custom notes and text values + 0c4e35ca-557d-470f-95d2-4cf5846483d2 + Panel + BTLx Params + false + 0 + 83bdcfa8-afbb-420d-9faa-98a158aee68d + 1 + Double click to edit panel content… + + + + + + 129 + 232 + 206 + 333 + + 0 + 0 + 0 + + 129.2169 + 232.1959 + + + + + + + 255;255;255;255 + + true + true + true + false + false + true + + + + + + + + + 59e0b89a-e487-49f8-bab8-b5bab16be14c + Panel + + + + + A panel for custom notes and text values + be041dc1-b473-45b4-a41f-6e3d871e6182 + Panel + + false + 0.81688592955470085 + ec68082b-6816-42e0-b242-ac0746b13092 + 1 + G:\Shared drives\2024_MAS\T2\03_finalization\Fabrication - Joints\BTLx\step_joint_joint_test.btlx + + + + + + -288 + 74 + 455 + 420 + + 0 + 0 + 0 + + -287.0021 + 74.40424 + + + + + + + 255;255;250;90 + + true + true + true + false + true + D:\Papachap\Desktop\BTLx\dovetail_joint.btlx + true + + + + + + + + + 3cd2949b-4ea8-4ffb-a70c-5c380f9f46ea + Deconstruct Plane + + + + + Deconstruct a plane into its component parts. + 865c30f6-b561-4871-a50c-ecd9f31c5e9e + Deconstruct Plane + DePlane + + + + + + 1964 + 139 + 65 + 84 + + + 1994 + 181 + + + + + + Plane to deconstruct + 4921bd95-db88-4938-a2ab-031a1bdbd283 + Plane + P + false + a5781cd0-b305-4ed7-99aa-69c993f66d09 + 1 + + + + + + 1966 + 141 + 13 + 80 + + + 1974 + 181 + + + + + + 1 + + + + + 1 + {0} + + + + + + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + + + + + + + + + + + + Origin point + cdcaa9a0-4d91-4482-86b7-86be2cda44b1 + Origin + O + false + 0 + + + + + + 2009 + 141 + 18 + 20 + + + 2018 + 151 + + + + + + + + X-Axis vector + d1e3671d-a71b-4d78-b593-81a36a647a0e + X-Axis + X + false + 0 + + + + + + 2009 + 161 + 18 + 20 + + + 2018 + 171 + + + + + + + + Y-Axis vector + 7d146df4-fb05-4842-baef-b21818d33e42 + Y-Axis + Y + false + 0 + + + + + + 2009 + 181 + 18 + 20 + + + 2018 + 191 + + + + + + + + Z-Axis vector + 88bddd76-b302-4c78-a0e9-35bc4b391b6b + Z-Axis + Z + false + 0 + + + + + + 2009 + 201 + 18 + 20 + + + 2018 + 211 + + + + + + + + + + + + e9eb1dcf-92f6-4d4d-84ae-96222d60f56b + Move + + + + + Translate (move) an object along a vector. + 69027599-f85b-4df6-a905-640e9354acc8 + Move + Move + + + + + + 2135 + 61 + 67 + 44 + + + 2167 + 83 + + + + + + Base geometry + 11854871-a4b0-4632-9734-2c92f39bd6e2 + Geometry + G + true + 0 + + + + + + 2137 + 63 + 15 + 20 + + + 2146 + 73 + + + + + + + + Translation vector + fa75702c-db09-4f3d-8b4d-9c4113d2c4fd + Motion + T + false + 0 + + + + + + 2137 + 83 + 15 + 20 + + + 2146 + 93 + + + + + + 1 + + + + + 1 + {0} + + + + + + 0 + 0 + 10 + + + + + + + + + + + + Translated geometry + 0060d1e4-4dbd-4989-ad5c-c635ced49a6d + Geometry + G + false + 0 + + + + + + 2182 + 63 + 18 + 20 + + + 2191 + 73 + + + + + + + + Transformation data + e630803b-d52f-410b-a74e-0533b5adb7fd + Transform + X + false + 0 + + + + + + 2182 + 83 + 18 + 20 + + + 2191 + 93 + + + + + + + + + + + + a3371040-e552-4bc8-b0ff-10a840258e88 + Negative + + + + + Compute the negative of a value. + 7526f9a6-8359-4b37-9c5e-cbee913b3ef8 + Negative + Neg + + + + + + 949 + 1023 + 61 + 28 + + + 978 + 1037 + + + + + + Input value + 5afb0642-aa38-48c2-866b-65ac743bd828 + Value + x + false + a3e72ece-c329-479f-b17f-94062e976ee8 + 1 + + + + + + 951 + 1025 + 12 + 24 + + + 958.5 + 1037 + + + + + + + + Output value + 268a388f-8c6d-4802-b21e-c9fdcefc3839 + Result + y + false + 0 + + + + + + 993 + 1025 + 15 + 24 + + + 1000.5 + 1037 + + + + + + + + + + + + a3371040-e552-4bc8-b0ff-10a840258e88 + Negative + + + + + Compute the negative of a value. + 685030bd-f114-48e8-8d19-4be9039d542e + Negative + Neg + + + + + + 1552 + 294 + 61 + 28 + + + 1581 + 308 + + + + + + Input value + b86e2643-6e28-4ba1-ba12-340d36937ccf + Value + x + false + 6b6d7ea8-7a08-4879-939c-5f96604c4678 + 1 + + + + + + 1554 + 296 + 12 + 24 + + + 1561.5 + 308 + + + + + + + + Output value + 5a7f3b31-fffa-478e-b8ef-7eb827144b57 + Result + y + false + 0 + + + + + + 1596 + 296 + 15 + 24 + + + 1603.5 + 308 + + + + + + + + + + + + 410755b1-224a-4c1e-a407-bf32fb45ea7e + 00000000-0000-0000-0000-000000000000 + CT: DecomposeBeam + + + + + # flake8: noqa +from compas.geometry import Line +from compas_rhino.conversions import frame_to_rhino_plane +from compas_rhino.conversions import line_to_rhino +from compas_rhino.conversions import point_to_rhino +from compas_rhino.conversions import box_to_rhino +from ghpythonlib.componentbase import executingcomponent as component +from System.Drawing import Color + + +class BeamDecompose(component): + RED = Color.FromArgb(255, 255, 100, 100) + GREEN = Color.FromArgb(200, 50, 220, 100) + BLUE = Color.FromArgb(200, 50, 150, 255) + WHITE = Color.FromArgb(255, 255, 255, 255) + YELLOW = Color.FromArgb(255, 255, 255, 0) + SCREEN_SIZE = 10 + RELATIVE_SIZE = 0 + + def RunScript(self, beam, show_frame, show_faces): + self.show_faces = show_faces if show_faces is not None else False + self.show_frame = show_frame if show_frame is not None else False + self.frames = [] + self.rhino_frames = [] + self.scales = [] + self.faces = [] + self.width = [] + self.height = [] + self.centerline = [] + self.shapes = [] + + for b in beam: + self.frames.append(b.frame) + self.rhino_frames.append(frame_to_rhino_plane(b.frame)) + self.scales.append(b.width + b.height) + self.centerline.append(line_to_rhino(b.centerline)) + self.shapes.append(box_to_rhino(b.shape)) + self.width.append(b.width) + self.height.append(b.height) + self.faces.append(b.faces) + + return self.rhino_frames, self.centerline, self.shapes, self.width, self.height + + def DrawViewportWires(self, arg): + if self.Locked: + return + + for f, s, faces in zip(self.frames, self.scales, self.faces): + if self.show_frame: + self._draw_frame(arg.Display, f, s) + if self.show_faces: + self._draw_faces(arg.Display, faces, s) + + def _draw_frame(self, display, frame, scale): + x = Line.from_point_and_vector(frame.point, frame.xaxis * scale) + y = Line.from_point_and_vector(frame.point, frame.yaxis * scale) + z = Line.from_point_and_vector(frame.point, frame.zaxis * scale) + display.DrawArrow(line_to_rhino(x), self.RED, self.SCREEN_SIZE, self.RELATIVE_SIZE) + display.DrawArrow(line_to_rhino(y), self.GREEN, self.SCREEN_SIZE, self.RELATIVE_SIZE) + display.DrawArrow(line_to_rhino(z), self.BLUE, self.SCREEN_SIZE, self.RELATIVE_SIZE) + + x_loc = x.end + x.vector * scale * 1.1 + y_loc = y.end + y.vector * scale * 1.1 + z_loc = z.end + z.vector * scale * 1.1 + display.Draw2dText("X", self.RED, point_to_rhino(x_loc), True, 16, "Verdana") + display.Draw2dText("Y", self.GREEN, point_to_rhino(y_loc), True, 16, "Verdana") + display.Draw2dText("Z", self.BLUE, point_to_rhino(z_loc), True, 16, "Verdana") + + def _draw_faces(self, display, faces, scale): + for index, face in enumerate(faces): + normal = Line.from_point_and_vector(face.point, face.normal * scale) + text = str(index) + display.Draw2dText(text, self.WHITE, point_to_rhino(face.point), True, 16, "Verdana") + display.DrawArrow(line_to_rhino(normal), self.YELLOW, self.SCREEN_SIZE, self.RELATIVE_SIZE) + + GhPython provides a Python script component + true + true + + iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAALDAAACwwBP0AiyAAAAhJJREFUSEullLFLQlEUxt/WECgETQYKCS6BJLQlSoSCg7ZIRILiEEKbDQ5v0K1Baskx0L3gbREtz6Hd/oDAsdGmCKJu97t57L77zkulAz8e7977fYd33rnHEkKwbFw+dgC3Z9JsNg8TicSbZVlCp1qt8gJpHJO8TolxZ4h6vX4dCoW+TPN+vy/G4zEvkqYDiZgy4M7Yth3N5XLPpnE0GhWj0Ui4rivC4bBfKA2zmjmR1c8ElSSTyYjJZCLa7fbvui4E0mxomIMh7VcqFUc3JWAK81Kp5N0zzGuG8Yyt7v1ZOp1+8YglKIPjOKosKI+5zyVA93j+QercuYnkjz9NcTKZVD8SP1TV29hX6Am0RLP/sH/SeuKEaEGURLUisz9jXoKV+LZPRC2ILzD3fHAJCvZVj0vga0HdKAjTvFwuD7mDbAsuAhm3Wq1drrdBYAtOWd0pKMx13HJl3mg0zrnrPrcFp4TydVXO9dOeKmkkEvnAZYS3VSwWR5xoXgvCiFg7slUCYvPCvZVPNcPwBYHxVwvqhgFgUHbkWd4Awa0ThhnH/xJQeeJ7B++J7sOdYY5J8FMiXaQzLwHAuMbYlmYYLzDGoPRMXoRPCBDcOoEWJBNpihlWo3cdhE8MENy63oKLgPCZAIS5lkqlJigJiRcB4TMCCP0dI4REy4DwGBEIPHHDcdNJsCwIjzGBwGzCjKLDyyOsb83ZZgK8ltr3AAAAAElFTkSuQmCC + + false + a9ce3e7a-6c1c-4572-a3de-4c9f2b90a244 + true + true + CT: DecomposeBeam + DecomposeBeam + + + + + + 1491 + 474 + 156 + 104 + + + 1571 + 526 + + + + + + 3 + 84fa917c-1ed8-4db3-8be1-7bdc4a6495a2 + 84fa917c-1ed8-4db3-8be1-7bdc4a6495a2 + 84fa917c-1ed8-4db3-8be1-7bdc4a6495a2 + 5 + 8ec86459-bf01-4409-baee-174d0d2b13d0 + 8ec86459-bf01-4409-baee-174d0d2b13d0 + 8ec86459-bf01-4409-baee-174d0d2b13d0 + 8ec86459-bf01-4409-baee-174d0d2b13d0 + 8ec86459-bf01-4409-baee-174d0d2b13d0 + + + + + 1 + true + Beam + e5d6bec6-04a7-4002-ac35-9b4259344b67 + Beam + Beam + true + 1 + true + 90fae02f-7750-462d-828f-cf5f2a0d39ae + 1 + 35915213-5534-4277-81b8-1bdc9e7383d2 + + + + + + 1493 + 476 + 63 + 33 + + + 1526 + 492.6667 + + + + + + + + true + Script input ShowFrame. + 7f4fda41-cbe9-4054-af0d-3fcb07967a5e + ShowFrame + ShowFrame + true + 0 + true + 47ff7ca2-adef-4499-810b-6e4c01b3bc23 + 1 + d60527f5-b5af-4ef6-8970-5f96fe412559 + + + + + + 1493 + 509 + 63 + 33 + + + 1526 + 526 + + + + + + + + true + Script input ShowFaces. + 0610d8df-83d0-4efa-be35-2b855fa8b7c7 + ShowFaces + ShowFaces + true + 0 + true + 0 + d60527f5-b5af-4ef6-8970-5f96fe412559 + + + + + + 1493 + 542 + 63 + 34 + + + 1526 + 559.3334 + + + + + + + + Script output Frame. + 9ce0b74d-024e-4803-ab40-8f1894bddfc7 + Frame + Frame + false + 0 + + + + + + 1586 + 476 + 59 + 20 + + + 1615.5 + 486 + + + + + + + + Script output Centerline. + 406aa56b-e577-4896-9649-5abbf8f8264e + Centerline + Centerline + false + 0 + + + + + + 1586 + 496 + 59 + 20 + + + 1615.5 + 506 + + + + + + + + Script output Box. + c1c31314-d156-4509-8d4e-2ee79ece2b88 + Box + Box + false + 0 + + + + + + 1586 + 516 + 59 + 20 + + + 1615.5 + 526 + + + + + + + + Script output Width. + ebb35db7-496b-4eef-91a1-a7727e7c7abc + Width + Width + false + 0 + + + + + + 1586 + 536 + 59 + 20 + + + 1615.5 + 546 + + + + + + + + Script output Height. + 2f18ca4a-b883-4a4c-afe0-eb75ffe432b2 + Height + Height + false + 0 + + + + + + 1586 + 556 + 59 + 20 + + + 1615.5 + 566 + + + + + + + + + + + + + + 2e78987b-9dfb-42a2-8b76-3923ac8bd91a + Boolean Toggle + + + + + Boolean (true/false) toggle + 47ff7ca2-adef-4499-810b-6e4c01b3bc23 + Boolean Toggle + Toggle + false + 0 + true + + + + + + 1343 + 586 + 104 + 22 + + + + + + + + + + + + + + + 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/fsPHj58zKTJM+bNW/z552kbN24z8WDOEeucPn3x3LnLZ899qXLy5HmL2+IrZWFMXt5REiTFY8eOgUiPHj0mTZp09uxZ8j59+nTfvn2rVas2YcKEVatWpaWlPffcc8uXL9cOW2fOnJkzZ07Hjh2NjDsp7dH/4OnkqVOnDEDs06xZswqDppi+FuF49N9FHz1+/Dh2o/CfUu3fv5932LBhw+effz5//vypU6empKT079+/a1cwateqVavWrVt36NChe/fuSUlJ4f/AWrRo0Zo1a7Zu3Sr4sGx8QYC4/19/Ncasli1bvv322+zHrl27rMCt/Md//Mcnn3wybty4kSNHrlixYsyYMZUrV+aSBDS7d+92vLp1606cOPHChQtQcuLEib1793JhwJSamsqpvf/++x988IEAqBg0T4IIGG6AibwJIy57XaRFwPR+yZIlJEqQgwYNAh2+A4wQSH344YcDBgwI/4maYenp6du3b4dC0AmrwVMcRuQ9ffr0Ro0amVK9evWZM2cGQEDPb3/724EDB4KLRZo2bfrjH/+YKYIMkY0xCxYseOeddzIyMsCCfbJpwDH7BDHWdDaGytxvJWiCDArTfZTvQYhQnzCRdLBJcfuvooX279ixg1kiYIo+fvz45OTkXr16derUCYZQ586dvQ4ePJjZmDt37urVq2ECjKwg8m3WrBkoXLt2jRuaPHkyQyIQ0TJkyJCXX365SZMm//RP/0TiZcqUif/HgIACsh999BGItGjRgrVbuHAhaDqkI3FhnhcvXuTOnOdbBhqRGgVFe/fu2bUre/fuXfGC4IbmFRk6lIzGM8jfLPEjBO8wsh5GCBEqAEHA4sWLiZazIFRhEdwwJ548EcvUs2fPsWPH1qpVS3IEBFCCG7whSHmuXLnSylWrVv3Xf/3Xp5566k9/+hMnZSNX1u7JnFhEeiUthxJzISaRmeq6HONbABpnjROl3LhxI8s5evT4Tz6ZP336nFBmzJg7deqszp270AOQClYnTrGF7k00PmSklEnY+PdJ586dE218+eWXkp2rV69evnz50qVLKrTl008/dXGREDtRqlQpKRLx83EcCkcDZzARcCNI+uI2cXOwyFYJVqAQXkPYhHsxvtxJGMuY/X25J9J1XMYZxuMmMfj+AALA53rffvud73//+z16DJ42bcmECfNCmTTp03Hj5vzyl//P73//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 + + + + + \ No newline at end of file diff --git a/tests/compas_timber/test_dovetail.py b/tests/compas_timber/test_dovetail.py new file mode 100644 index 000000000..9dd2f9a4d --- /dev/null +++ b/tests/compas_timber/test_dovetail.py @@ -0,0 +1,464 @@ +import pytest + +from collections import OrderedDict + +from compas.geometry import Point +from compas.geometry import Line +from compas.geometry import Frame +from compas.geometry import Vector + +from compas_timber.elements import Beam +from compas_timber._fabrication import DovetailMortise +from compas_timber._fabrication import DovetailTenon + + +@pytest.fixture +def cross_beams(): + widths = [60.0, 60.0, 80.0] + heights = [120.0, 120.0, 100.0] + + centerlines = [ + Line(Point(x=33782.4296640, y=-3257.66821289, z=0.0), Point(x=30782.4296640, y=-3257.66821289, z=0.0)), + Line(Point(x=30782.4296640, y=-3257.66821289, z=0.0), Point(x=33782.4296640, y=-3257.66821289, z=0.0)), + Line(Point(x=30782.4296640, y=-3257.66821289, z=0.0), Point(x=33782.4296640, y=-3257.66821289, z=0.0)), + ] + + return [ + Beam.from_centerline(centerline, width, height) + for centerline, width, height in zip(centerlines, widths, heights) + ] + + +@pytest.fixture +def main_beams(): + widths = [80.0, 80.0, 100.0] + heights = [100.0, 80.0, 120.0] + z_vectors = [Vector(x=0.000, y=0.51422, z=0.703021), Vector(x=0, y=0.272144, z=-0.926735), Vector.Zaxis()] + + centerlines = [ + Line( + Point(x=31332.0787451, y=-3257.66821289, z=-23.3661907508), + Point(x=31832.0787451, y=-3973.19330068, z=500.0), + ), + Line( + Point(x=32930.7147438, y=-2542.14312510, z=200.0), + Point(x=32730.7147438, y=-3257.66821289, z=-10.1206126384), + ), + Line(Point(x=32730.7147438, y=-2542.14312510, z=-10.0), Point(x=32730.7147438, y=-3257.66821289, z=-10.0)), + ] + + return [ + Beam.from_centerline(centerline, width, height, z_vector) + for centerline, width, height, z_vector in zip(centerlines, widths, heights, z_vectors) + ] + + +TENON_CUTTING_FRAMES = [ + Frame( + point=Point(x=31330.8001549, y=-3287.66821289, z=-50.9810872474), + xaxis=Vector(x=0.939692620786, y=-4.74845031169e-16, z=-0.342020143326), + yaxis=Vector(x=-0.342020143326, y=2.34059774926e-16, z=-0.939692620786), + ), + Frame( + point=Point(x=32726.3841104, y=-3227.66821289, z=-32.5776239351), + xaxis=Vector(x=-0.996194698092, y=5.64393952025e-16, z=0.0871557427477), + yaxis=Vector(x=-0.0871557427477, y=-1.18522451263e-17, z=-0.996194698092), + ), + Frame( + point=Point(x=32730.7147438, y=-3217.66821289, z=50.0), + xaxis=Vector(x=1.0, y=-6.12303176911e-17, z=-6.12303176911e-17), + yaxis=Vector(x=6.12303176911e-17, y=6.12303176911e-17, z=1.0), + ), +] + +EXPECTED_TENON_PARAMS = [ + OrderedDict( + [ + ("Name", "DovetailTenon"), + ("Priority", "0"), + ("Process", "yes"), + ("ProcessID", "0"), + ("ReferencePlaneID", "1"), + ("Orientation", "start"), + ("StartX", "6.262"), + ("StartY", "45.000"), + ("StartDepth", "10.000"), + ("Angle", "119.424"), + ("Inclination", "53.817"), + ("Rotation", "70.000"), + ("LengthLimitedTop", "yes"), + ("LengthLimitedBottom", "yes"), + ("Length", "60.000"), + ("Width", "45.000"), + ("Height", "28.000"), + ("ConeAngle", "10.000"), + ("UseFlankAngle", "no"), + ("FlankAngle", "15.000"), + ("Shape", "radius"), + ("ShapeRadius", "22.497"), + ] + ), + OrderedDict( + [ + ("Name", "DovetailTenon"), + ("Priority", "0"), + ("Process", "yes"), + ("ProcessID", "0"), + ("ReferencePlaneID", "3"), + ("Orientation", "end"), + ("StartX", "751.524"), + ("StartY", "50.000"), + ("StartDepth", "10.000"), + ("Angle", "105.013"), + ("Inclination", "73.635"), + ("Rotation", "85.000"), + ("LengthLimitedTop", "yes"), + ("LengthLimitedBottom", "yes"), + ("Length", "57.653"), + ("Width", "36.926"), + ("Height", "28.000"), + ("ConeAngle", "15.000"), + ("UseFlankAngle", "no"), + ("FlankAngle", "15.000"), + ("Shape", "radius"), + ("ShapeRadius", "22.497"), + ] + ), + OrderedDict( + [ + ("Name", "DovetailTenon"), + ("Priority", "0"), + ("Process", "yes"), + ("ProcessID", "0"), + ("ReferencePlaneID", "3"), + ("Orientation", "end"), + ("StartX", "675.525"), + ("StartY", "50.000"), + ("StartDepth", "0.000"), + ("Angle", "90.000"), + ("Inclination", "90.000"), + ("Rotation", "90.000"), + ("LengthLimitedTop", "yes"), + ("LengthLimitedBottom", "yes"), + ("Length", "80.000"), + ("Width", "50.000"), + ("Height", "28.000"), + ("ConeAngle", "7.000"), + ("UseFlankAngle", "no"), + ("FlankAngle", "15.000"), + ("Shape", "square"), + ("ShapeRadius", "22.497"), + ] + ), +] + +EXPECTED_MORTISE_PARAMS = [ + OrderedDict( + [ + ("Name", "DovetailMortise"), + ("Priority", "0"), + ("Process", "yes"), + ("ProcessID", "0"), + ("ReferencePlaneID", "2"), + ("StartX", "2451.630"), + ("StartY", "110.981"), + ("StartDepth", "0.000"), + ("Angle", "-110.000"), + ("Slope", "90.000"), + ("LimitationTop", "unlimited"), + ("LengthLimitedBottom", "yes"), + ("Length", "60.000"), + ("Width", "45.000"), + ("Depth", "28.000"), + ("ConeAngle", "10.000"), + ("UseFlankAngle", "no"), + ("FlankAngle", "15.000"), + ("Shape", "radius"), + ("ShapeRadius", "22.497"), + ] + ), + OrderedDict( + [ + ("Name", "DovetailMortise"), + ("Priority", "0"), + ("Process", "yes"), + ("ProcessID", "0"), + ("ReferencePlaneID", "2"), + ("StartX", "1943.954"), + ("StartY", "92.578"), + ("StartDepth", "0.000"), + ("Angle", "-85.000"), + ("Slope", "90.000"), + ("LimitationTop", "unlimited"), + ("LengthLimitedBottom", "yes"), + ("Length", "57.653"), + ("Width", "36.926"), + ("Depth", "28.000"), + ("ConeAngle", "15.000"), + ("UseFlankAngle", "no"), + ("FlankAngle", "15.000"), + ("Shape", "radius"), + ("ShapeRadius", "22.497"), + ] + ), + OrderedDict( + [ + ("Name", "DovetailMortise"), + ("Priority", "0"), + ("Process", "yes"), + ("ProcessID", "0"), + ("ReferencePlaneID", "2"), + ("StartX", "1948.285"), + ("StartY", "0.000"), + ("StartDepth", "0.000"), + ("Angle", "90.000"), + ("Slope", "90.000"), + ("LimitationTop", "unlimited"), + ("LengthLimitedBottom", "yes"), + ("Length", "80.000"), + ("Width", "50.000"), + ("Depth", "28.000"), + ("ConeAngle", "7.000"), + ("UseFlankAngle", "no"), + ("FlankAngle", "15.000"), + ("Shape", "square"), + ("ShapeRadius", "22.497"), + ] + ), +] + + +@pytest.mark.parametrize( + "test_index, expected_tenon_params, cutting_plane_index, start_y, start_depth, rotation, length, width, height, cone_angle, flank_angle, shape, shape_radius, ref_side_index", + [ + ( + 0, + EXPECTED_TENON_PARAMS[0], + 1, + 5.0, + 10.0, + -20.0, + 60.0, + 45.0, + 28.0, + 10.0, + 15.0, + "radius", + 22.497, + 0, + ), # main_beam_a + ( + 1, + EXPECTED_TENON_PARAMS[1], + 1, + -10.0, + 10.0, + 5.0, + 60.0, + 45.0, + 28.0, + 15.0, + 15.0, + "radius", + 22.497, + 2, + ), # main_beam_b + ( + 2, + EXPECTED_TENON_PARAMS[2], + 1, + 0.0, + 0.0, + 0.0, + 80.0, + 50.0, + 28.0, + 7.0, + 15.0, + "square", + 22.497, + 2, + ), # main_beam_c + ], +) +def test_dovetailtenon_params( + main_beams, + cross_beams, + test_index, + expected_tenon_params, + cutting_plane_index, + start_y, + start_depth, + rotation, + length, + width, + height, + cone_angle, + flank_angle, + shape, + shape_radius, + ref_side_index, +): + # Create the DovetailTenon + dovetail_tenon = DovetailTenon.from_plane_and_beam( + plane=cross_beams[test_index].ref_sides[cutting_plane_index], + beam=main_beams[test_index], + start_y=start_y, + start_depth=start_depth, + rotation=rotation, + length=length, + width=width, + height=height, + cone_angle=cone_angle, + flank_angle=flank_angle, + shape=shape, + shape_radius=shape_radius, + ref_side_index=ref_side_index, + ) + # Validate generated parameters + generated_params = dovetail_tenon.params_dict + for key, value in expected_tenon_params.items(): + assert generated_params[key] == value + + +@pytest.mark.parametrize( + "test_index, expected_mortise_params, cutting_frame, start_depth, angle, length, width, depth, cone_angle, flank_angle, shape, shape_radius, ref_side_index", + [ + ( + 0, + EXPECTED_MORTISE_PARAMS[0], + TENON_CUTTING_FRAMES[0], + 0.0, + -20.0, + 60.0, + 45.0, + 28.0, + 10.0, + 15.0, + "radius", + 22.497, + 1, + ), # main_beam_a + ( + 1, + EXPECTED_MORTISE_PARAMS[1], + TENON_CUTTING_FRAMES[1], + 0.0, + 5.0, + 57.65307084966669, + 36.925746566661111, + 28.0, + 15.0, + 15.0, + "radius", + 22.497, + 1, + ), # main_beam_b + ( + 2, + EXPECTED_MORTISE_PARAMS[2], + TENON_CUTTING_FRAMES[2], + 0.0, + 0.0, + 80.0, + 50.0, + 28.0, + 7.0, + 15.0, + "square", + 22.497, + 1, + ), # main_beam_c + ], +) +def test_dovetailmortise_params( + cross_beams, + cutting_frame, + test_index, + expected_mortise_params, + start_depth, + angle, + length, + width, + depth, + cone_angle, + flank_angle, + shape, + shape_radius, + ref_side_index, +): + # Create the StepJoint + dovetail_mortise = DovetailMortise.from_frame_and_beam( + cutting_frame, + cross_beams[test_index], + start_depth, + angle, + length, + width, + depth, + cone_angle, + flank_angle, + shape, + shape_radius, + ref_side_index, + ) + + # Validate generated parameters + generated_params = dovetail_mortise.params_dict + for key, value in expected_mortise_params.items(): + assert generated_params[key] == value + + +@pytest.mark.parametrize( + "test_index, expected_tenon_params, expected_frame", + [ + (0, EXPECTED_TENON_PARAMS[0], TENON_CUTTING_FRAMES[0]), + (1, EXPECTED_TENON_PARAMS[1], TENON_CUTTING_FRAMES[1]), + (2, EXPECTED_TENON_PARAMS[2], TENON_CUTTING_FRAMES[2]), + ], +) +def test_dovetailtenon_frame_from_params( + main_beams, + test_index, + expected_tenon_params, + expected_frame, +): + # convert string values to the appropriate types (float, bool, etc.) + def convert_value(value): + if isinstance(value, str): + # convert to float if the string represents a number + if value.replace(".", "", 1).isdigit(): + return float(value) + # convert specific strings to booleans + if value.lower() in ["yes", "no"]: + return value.lower() == "yes" + return value + + # convert the OrderedDict values to the expected types + params = {key.lower(): convert_value(value) for key, value in expected_tenon_params.items()} + + # instantiate DovetailTenon with unpacked parameters from the OrderedDict + dovetail_tenon = DovetailTenon( + orientation=params["orientation"], + start_x=params["startx"], + start_y=params["starty"], + start_depth=params["startdepth"], + angle=params["angle"], + inclination=params["inclination"], + rotation=params["rotation"], + length_limited_top=params["lengthlimitedtop"], + length_limited_bottom=params["lengthlimitedbottom"], + length=params["length"], + width=params["width"], + height=params["height"], + cone_angle=params["coneangle"], + use_flank_angle=params["useflankangle"], + flank_angle=params["flankangle"], + shape=params["shape"], + shape_radius=params["shaperadius"], + ref_side_index=int(params["referenceplaneid"] - 1), + ) + + # generate frame from the parameters + generated_frame = dovetail_tenon.frame_from_params_and_beam(main_beams[test_index]) + assert generated_frame == expected_frame