diff --git a/CHANGELOG.md b/CHANGELOG.md index a7ef6ca75..7aad41246 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,11 +9,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +* Added `SurfaceModelJointOverride` GH Component. * Added `Plate` element. * Added attribute `plates` to `TimberModel`. ### Changed +* Fixed missing input parameter in `SurfaceModelOptions` GH Component. +* Fixed error with tolerances for `SurfaceModel`s modeled in meters. * Renamed `beam` to `element` in different locations to make it more generic. ### Removed @@ -22,7 +25,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Removed `add_plate` from `TimberModel`, use `add_element` instead. * Removed `add_wall` from `TimberModel`, use `add_element` instead. - ## [0.9.1] 2024-07-05 ### Added diff --git a/src/compas_timber/design/wall_from_surface.py b/src/compas_timber/design/wall_from_surface.py index de120d313..a6ebe4ee4 100644 --- a/src/compas_timber/design/wall_from_surface.py +++ b/src/compas_timber/design/wall_from_surface.py @@ -17,6 +17,7 @@ from compas.geometry import matrix_from_frame_to_frame from compas.geometry import offset_line from compas.geometry import offset_polyline +from compas.tolerance import Tolerance from compas_timber.connections import ConnectionSolver from compas_timber.connections import JointTopology @@ -86,6 +87,7 @@ def __init__( beam_width=None, frame_depth=None, z_axis=None, + tolerance=Tolerance(unit="MM", absolute=1e-3, relative=1e-3), sheeting_outside=None, sheeting_inside=None, lintel_posts=True, @@ -114,6 +116,7 @@ def __init__( self.windows = [] self.beam_dimensions = {} self.joint_overrides = joint_overrides + self.dist_tolerance = tolerance.relative for key in self.BEAM_CATEGORY_NAMES: self.beam_dimensions[key] = [self.beam_width, self.frame_depth] @@ -180,10 +183,10 @@ def create_model(self): model.add_beam(beam) topologies = [] solver = ConnectionSolver() - found_pairs = solver.find_intersecting_pairs(model.beams, rtree=True, max_distance=0.1) + found_pairs = solver.find_intersecting_pairs(model.beams, rtree=True, max_distance=self.dist_tolerance) 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=0.1) + detected_topo, beam_a, beam_b = solver.find_topology(beam_a, beam_b, max_distance=self.dist_tolerance) if not detected_topo == JointTopology.TOPO_UNKNOWN: topologies.append({"detected_topo": detected_topo, "beam_a": beam_a, "beam_b": beam_b}) for rule in self.rules: @@ -275,6 +278,7 @@ def beam_category_names(cls): def parse_loops(self): for loop in self.surface.loops: polyline_points = [] + polyline_length = 0.0 for i, edge in enumerate(loop.edges): if not edge.is_line: raise ValueError("function only supprorts polyline edges") @@ -285,15 +289,18 @@ def parse_loops(self): polyline_points.append(edge.start_vertex.point) else: polyline_points.append(edge.end_vertex.point) + polyline_length += edge.length polyline_points.append(polyline_points[0]) + loop_polyline = Polyline(polyline_points) + offset_dist = self.dist_tolerance * 1000 if loop.is_outer: - offset_loop = Polyline(offset_polyline(Polyline(polyline_points), 10, self.normal)) - if offset_loop.length > Polyline(polyline_points).length: + offset_loop = Polyline(offset_polyline(loop_polyline, offset_dist, self.normal)) + if offset_loop.length > loop_polyline.length: polyline_points.reverse() self.outer_polyline = Polyline(polyline_points) else: - offset_loop = Polyline(offset_polyline(Polyline(polyline_points), 10, self.normal)) - if offset_loop.length < Polyline(polyline_points).length: + offset_loop = Polyline(offset_polyline(loop_polyline, offset_dist, self.normal)) + if offset_loop.length < loop_polyline.length: polyline_points.reverse() self.inner_polylines.append(Polyline(polyline_points)) @@ -303,8 +310,8 @@ def generate_perimeter_elements(self): element = self.BeamElement(segment, parent=self) if i in interior_indices: if ( - angle_vectors(segment.direction, self.z_axis, deg=True) < 1 - or angle_vectors(segment.direction, self.z_axis, deg=True) > 179 + angle_vectors(segment.direction, self.z_axis, deg=True) < 45 + or angle_vectors(segment.direction, self.z_axis, deg=True) > 135 ): if self.lintel_posts: element.type = "jack_stud" @@ -314,8 +321,8 @@ def generate_perimeter_elements(self): element.type = "header" else: if ( - angle_vectors(segment.direction, self.z_axis, deg=True) < 1 - or angle_vectors(segment.direction, self.z_axis, deg=True) > 179 + angle_vectors(segment.direction, self.z_axis, deg=True) < 45 + or angle_vectors(segment.direction, self.z_axis, deg=True) > 135 ): element.type = "edge_stud" else: @@ -358,20 +365,29 @@ def offset_elements(self, element_loop): element.offset(self.edge_stud_offset) offset_loop.append(element) # self.edges.append(Line(element.centerline[0], element.centerline[1])) + for i, element in enumerate(offset_loop): if self.edge_stud_offset > 0: if element.type != "plate": element_before = offset_loop[i - 1] element_after = offset_loop[(i + 1) % len(offset_loop)] - start_point = intersection_line_line(element.centerline, element_before.centerline, 0.01)[0] - end_point = intersection_line_line(element.centerline, element_after.centerline, 0.01)[0] + start_point = intersection_line_line( + element.centerline, element_before.centerline, self.dist_tolerance + )[0] + end_point = intersection_line_line( + element.centerline, element_after.centerline, self.dist_tolerance + )[0] if start_point and end_point: element.centerline = Line(start_point, end_point) + else: + raise ValueError("edges are parallel, no intersection found") else: element_before = offset_loop[i - 1] element_after = offset_loop[(i + 1) % len(offset_loop)] - start_point = intersection_line_line(element.centerline, element_before.centerline, 0.01)[0] - end_point = intersection_line_line(element.centerline, element_after.centerline, 0.01)[0] + start_point, _ = intersection_line_line( + element.centerline, element_before.centerline, self.dist_tolerance + ) + end_point, _ = intersection_line_line(element.centerline, element_after.centerline, self.dist_tolerance) if start_point and end_point: element.centerline = Line(start_point, end_point) return offset_loop @@ -389,7 +405,7 @@ def generate_studs(self): def generate_stud_lines(self): x_position = self.stud_spacing - while x_position < self.panel_length: + while x_position < self.panel_length - self.beam_width: start_point = Point(x_position, 0, 0) start_point.transform(matrix_from_frame_to_frame(Frame.worldXY(), self.frame)) line = Line.from_point_and_vector(start_point, self.z_axis * self.panel_height) @@ -538,6 +554,7 @@ def __init__(self, outline, sill_height=None, header_height=None, parent=None): self._length = None self._height = None self._frame = None + self.dist_tolerance = parent.dist_tolerance self.process_outlines() @property @@ -588,7 +605,7 @@ def process_outlines(self): pts = [] for seg in self.outline.lines: if seg != segment: - pt = intersection_line_segment(ray, seg, 0.01)[0] + pt = intersection_line_segment(ray, seg, self.dist_tolerance)[0] if pt: pts.append(Point(*pt)) if len(pts) > 1: diff --git a/src/compas_timber/ghpython/components/CT_Model_From_Surface/code.py b/src/compas_timber/ghpython/components/CT_Model_From_Surface/code.py index 3d4b8a92c..d455bc0f6 100644 --- a/src/compas_timber/ghpython/components/CT_Model_From_Surface/code.py +++ b/src/compas_timber/ghpython/components/CT_Model_From_Surface/code.py @@ -1,7 +1,9 @@ """Creates a Beam from a LineCurve.""" +import Rhino from compas.geometry import Brep from compas.scene import Scene +from compas.tolerance import Tolerance from ghpythonlib.componentbase import executingcomponent as component from Grasshopper.Kernel.GH_RuntimeMessageLevel import Warning from Rhino.Geometry import Brep as RhinoBrep @@ -31,8 +33,17 @@ def RunScript(self, surface, stud_spacing, beam_width, frame_depth, z_axis, opti if not options: options = {} + units = Rhino.RhinoDoc.ActiveDoc.GetUnitSystemName(True, True, True, True) + tol = None + if units == "m": + tol = Tolerance(unit="M", absolute=1e-6, relative=1e-6) + elif units == "cm": + tol = Tolerance(unit="CM", absolute=1e-4, relative=1e-4) + elif units == "mm": + tol = Tolerance(unit="MM", absolute=1e-3, relative=1e-3) + surface_model = SurfaceModel( - Brep.from_native(surface), stud_spacing, beam_width, frame_depth, z_axis, **options + Brep.from_native(surface), stud_spacing, beam_width, frame_depth, z_axis, tol, **options ) debug_info = DebugInfomation() diff --git a/src/compas_timber/ghpython/components/CT_SurfaceModelJointOverride/code.py b/src/compas_timber/ghpython/components/CT_SurfaceModelJointOverride/code.py new file mode 100644 index 000000000..88c2fc498 --- /dev/null +++ b/src/compas_timber/ghpython/components/CT_SurfaceModelJointOverride/code.py @@ -0,0 +1,106 @@ +import inspect + +from ghpythonlib.componentbase import executingcomponent as component +from Grasshopper.Kernel.GH_RuntimeMessageLevel import Warning +from System.Windows.Forms import ToolStripMenuItem +from System.Windows.Forms import ToolStripSeparator + +from compas_timber.connections import Joint +from compas_timber.design import CategoryRule +from compas_timber.design import SurfaceModel +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 SurfaceModelJointRule(component): + def __init__(self): + super(SurfaceModelJointRule, self).__init__() + self.cat_a = None + self.cat_b = None + self.classes = {} + for cls in get_leaf_subclasses(Joint): + self.classes[cls.__name__] = cls + + if ghenv.Component.Params.Output[0].NickName == "Rule": + self.joint_type = None + else: + parsed_output = ghenv.Component.Params.Output[0].NickName.split(" ") + self.joint_type = self.classes.get(parsed_output[0]) + if len(parsed_output) > 1: + self.cat_a = parsed_output[1] + if len(parsed_output) > 2: + self.cat_b = parsed_output[2] + + def RunScript(self, *args): + if not self.joint_type: + ghenv.Component.Message = "Select joint type from context menu (right click)" + self.AddRuntimeMessage(Warning, "Select joint type from context menu (right click)") + return None + + else: + ghenv.Component.Message = self.joint_type.__name__ + + kwargs = {} + for i, val in enumerate(args): + if val: + kwargs[self.arg_names()[i + 2]] = val + + return CategoryRule(self.joint_type, self.cat_a, self.cat_b, **kwargs) + + def arg_names(self): + if self.joint_type: + names = inspect.getargspec(self.joint_type.__init__)[0][1:] + else: + names = ["beam_a", "beam_b"] + for i in range(2): + names[i] += " category" + return [name for name in names if (name != "key") and (name != "frame")] + + def AppendAdditionalMenuItems(self, menu): + if not self.RuntimeMessages(Warning): + menu.Items.Add(ToolStripSeparator()) + beam_a_menu = ToolStripMenuItem(self.arg_names()[0]) + menu.Items.Add(beam_a_menu) + for name in SurfaceModel.beam_category_names(): + item = ToolStripMenuItem(name, None, self.on_beam_a_click) + if name == self.cat_a: + item.Checked = True + beam_a_menu.DropDownItems.Add(item) + + beam_b_menu = ToolStripMenuItem(self.arg_names()[1]) + menu.Items.Add(beam_b_menu) + for name in SurfaceModel.beam_category_names(): + item = ToolStripMenuItem(name, None, self.on_beam_b_click) + if name == self.cat_b: + item.Checked = True + beam_b_menu.DropDownItems.Add(item) + menu.Items.Add(ToolStripSeparator()) + 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 output_name(self): + name = self.joint_type.__name__ + if self.cat_a: + name += " {}".format(self.cat_a) + if self.cat_b: + name += " {}".format(self.cat_b) + return name + + def on_beam_a_click(self, sender, event_info): + self.cat_a = sender.Text + rename_gh_output(self.output_name(), 0, ghenv) + ghenv.Component.ExpireSolution(True) + + def on_beam_b_click(self, sender, event_info): + self.cat_b = sender.Text + rename_gh_output(self.output_name(), 0, ghenv) + ghenv.Component.ExpireSolution(True) + + def on_item_click(self, sender, event_info): + self.joint_type = self.classes[str(sender)] + rename_gh_output(self.output_name(), 0, ghenv) + manage_dynamic_params(self.arg_names()[2:], ghenv, rename_count=0, permanent_param_count=0) + ghenv.Component.ExpireSolution(True) diff --git a/src/compas_timber/ghpython/components/CT_SurfaceModelJointOverride/icon.png b/src/compas_timber/ghpython/components/CT_SurfaceModelJointOverride/icon.png new file mode 100644 index 000000000..adef94214 Binary files /dev/null and b/src/compas_timber/ghpython/components/CT_SurfaceModelJointOverride/icon.png differ diff --git a/src/compas_timber/ghpython/components/CT_SurfaceModelJointOverride/metadata.json b/src/compas_timber/ghpython/components/CT_SurfaceModelJointOverride/metadata.json new file mode 100644 index 000000000..18461ef54 --- /dev/null +++ b/src/compas_timber/ghpython/components/CT_SurfaceModelJointOverride/metadata.json @@ -0,0 +1,20 @@ +{ + "name": "Surface Assembly Joint Override ", + "nickname": "JointOverride", + "category": "COMPAS Timber", + "subcategory": "Joint Rules", + "description": "overrides the standard joint rules for Surface Assembly", + "exposure": 2, + "ghpython": { + "isAdvancedMode": true, + "iconDisplay": 0, + "inputParameters": [ + ], + "outputParameters": [ + { + "name": "Rule", + "description": "A joint rule." + } + ] + } +} diff --git a/src/compas_timber/ghpython/components/CT_SurfaceModelOptions/metadata.json b/src/compas_timber/ghpython/components/CT_SurfaceModelOptions/metadata.json index bd27cac6c..7aa82aee1 100644 --- a/src/compas_timber/ghpython/components/CT_SurfaceModelOptions/metadata.json +++ b/src/compas_timber/ghpython/components/CT_SurfaceModelOptions/metadata.json @@ -38,6 +38,12 @@ "description": "(optional) From beam dimension component. Beam dimensions must either be defined here or in with beam_width and frame_depth inputs.", "typeHintID": "none", "scriptParamAccess": 1 + }, + { + "name": "joint_overrides", + "description": "(optional) From joint overrides component. Allows user to specify joints between specific beam types in surface model.", + "typeHintID": "none", + "scriptParamAccess": 1 } ], "outputParameters": [ @@ -47,4 +53,4 @@ } ] } -} \ No newline at end of file +}