diff --git a/src/bonsai/bonsai/bim/module/model/polyline.py b/src/bonsai/bonsai/bim/module/model/polyline.py index 7dd7382127..d8ea5c0eda 100644 --- a/src/bonsai/bonsai/bim/module/model/polyline.py +++ b/src/bonsai/bonsai/bim/module/model/polyline.py @@ -429,71 +429,78 @@ def get_horizontal_profile_preview_data(context, relating_type): case "9": grouped_verts = [(v[0] + x_offset, v[1] - y_offset, v[2]) for v in grouped_verts] - # Create profile curve - scale_mat = Matrix.Scale(-1, 4, (1.0, 0.0, 0.0)) - grouped_verts = [scale_mat @ Vector(v) for v in grouped_verts] - profile_curve = bpy.data.curves.new("Profile", type="CURVE") - profile_curve.dimensions = "2D" - profile_curve.splines.new("POLY") - profile_curve.splines[0].points.add(len(grouped_verts)) - - for i, point in enumerate(profile_curve.splines[0].points): - if i == len(grouped_verts): # Close curve - point.co = Vector((*grouped_verts[0], 0)) - continue - point.co = Vector((*grouped_verts[i], 0)) - profile_obj = bpy.data.objects.new("Profile", profile_curve) - - # Create path curve with profile object as bevel - path_curve = bpy.data.curves.new("Polyline", type="CURVE") - path_curve.dimensions = "2D" - path_curve.splines.new("POLY") - path_curve.splines[0].points.add(len(polyline_verts) - 1) - for i, point in enumerate(path_curve.splines[0].points): - point.co = Vector((*polyline_verts[i], 0)) - path_curve.splines[0].use_smooth = False - path_curve.bevel_mode = "OBJECT" - path_curve.bevel_object = profile_obj - - # Convert path curve to mesh - # This operation throws a warning when done during gpu drawing, so it was removed from the decorator file to be handled here - path_obj = bpy.data.objects.new("Preview", path_curve) - context.scene.collection.objects.link(path_obj) - bpy.context.view_layer.objects.active = path_obj - dg = context.evaluated_depsgraph_get() - path_obj = path_obj.evaluated_get(dg) - me = path_obj.to_mesh() - - # Create bmesh from path mesh - bm = bmesh.new() - new_verts = [bm.verts.new(v.co) for v in me.vertices] - index = [[v for v in edge.vertices] for edge in me.edges] - new_edges = [bm.edges.new((new_verts[i[0]], new_verts[i[1]])) for i in index] - for face in me.polygons: - verts = [new_verts[i] for i in face.vertices] - bm.faces.new(verts) - bm.verts.index_update() - bm.edges.index_update() - tris = [[loop.vert.index for loop in triangles] for triangles in bm.calc_loop_triangles()] - - bpy.data.objects.remove(bpy.data.objects[path_obj.name], do_unlink=True) - bpy.data.objects.remove(bpy.data.objects[profile_obj.name], do_unlink=True) - try: - bpy.data.curves.remove(profile_obj.data, do_unlink=True) - except: - pass - try: - bpy.data.curves.remove(path_obj.data, do_unlink=True) - except: - pass - data = {} - data["verts"] = [tuple(v.co) for v in bm.verts] - data["edges"] = [(edge.verts[0].index, edge.verts[1].index) for edge in bm.edges] + data["verts"] = [] + data["edges"] = [] + data["tris"] = [] + + grouped_verts = [(v) for v in grouped_verts] + + all_bm = bmesh.new() + for i in range(len(polyline_verts) - 1): + mesh = bpy.data.meshes.new("TempMesh") + # Create the initial mesh from the profile verts + bm = create_bmesh_from_vertices(grouped_verts, is_closed=True) + bm.verts.ensure_lookup_table() + # Creates the clipping plane formed by two segments. + # The first one is for the profile start, based on the current and previous segment of the polyline. + # The second is for the profile end, based on the current and the next segment. + if i == 0: + d = (polyline_verts[i+1] - polyline_verts[i]).normalized() + clip_start = d + else: + d1 = (polyline_verts[i] - polyline_verts[i-1]).normalized() + d2 = (polyline_verts[i] - polyline_verts[i+1]).normalized() + clip_start = (d1-d2).normalized() + + if i == len(polyline_verts) - 2: + d = (polyline_verts[i+1] - polyline_verts[i]).normalized() + clip_end = d + else: + d1 = (polyline_verts[i+1] - polyline_verts[i]).normalized() + d2 = (polyline_verts[i+1] - polyline_verts[i+2]).normalized() + clip_end = (d1-d2).normalized() + + # Rotates the profile face to the right direction + direction = polyline_verts[i+1] - polyline_verts[i] + position = polyline_verts[i] + rotation_matrix = direction.to_track_quat('Z', 'Y').to_matrix().to_4x4() + bmesh.ops.transform(bm, verts=bm.verts, matrix=rotation_matrix) + bmesh.ops.translate(bm, verts=bm.verts, vec=position) + bmesh.ops.translate(bm, verts=bm.verts, vec=-direction) + + # Extrude and move the new face + last_face = bmesh.ops.extrude_face_region(bm, geom=bm.edges[:] + bm.faces[:]) + new_verts = [e for e in last_face["geom"] if isinstance(e, bmesh.types.BMVert)] + bmesh.ops.translate(bm, verts=new_verts, vec=direction * 3) + # Apply the cutting planes + cut = bmesh.ops.bisect_plane(bm, geom=bm.verts[:] + bm.edges[:] + bm.faces[:], plane_co=polyline_verts[i], plane_no=clip_start, clear_inner=True) + bm.verts.index_update() + bm.edges.index_update() + cut = bmesh.ops.bisect_plane(bm, geom=bm.verts[:] + bm.edges[:] + bm.faces[:], plane_co=polyline_verts[i+1], plane_no=clip_end, clear_outer=True) + + bm.to_mesh(mesh) + bm.free() + mesh.update() + all_bm.from_mesh(mesh) + bpy.data.meshes.remove(bpy.data.meshes["TempMesh"]) + + # It's necessary to add the mesh to an object to get the expected result. + mesh = bpy.data.meshes.new("TempMesh2") + all_bm.to_mesh(mesh) + all_bm.free() + obj = bpy.data.objects.new('TempObj', mesh) + bm = bmesh.new() + bm.from_mesh(obj.data) + bpy.data.meshes.remove(bpy.data.meshes["TempMesh2"]) + + verts = [tuple(v.co) for v in bm.verts] + edges = [[v.index for v in e.verts] for e in bm.edges] + tris = [[loop.vert.index for loop in triangles] for triangles in bm.calc_loop_triangles()] + data["verts"] = verts + data["edges"] = edges data["tris"] = tris - bm.free() - return data @@ -632,21 +639,21 @@ class PolylineOperator: if x: if event.shift and event.value == "PRESS" and event.type == "X": self.tool_state.use_default_container = False - self.tool_state.plane_method = "YZ" + self.tool_state.plane_method = "YZ" if self.tool_state.plane_method !="YZ" else None self.tool_state.axis_method = None tool.Blender.update_viewport() if y: if event.shift and event.value == "PRESS" and event.type == "Y": self.tool_state.use_default_container = False - self.tool_state.plane_method = "XZ" + self.tool_state.plane_method = "XZ" if self.tool_state.plane_method !="XZ" else None self.tool_state.axis_method = None tool.Blender.update_viewport() if z: if event.shift and event.value == "PRESS" and event.type == "Z": self.tool_state.use_default_container = False - self.tool_state.plane_method = "XY" + self.tool_state.plane_method = "XY" if self.tool_state.plane_method !="XY" else None self.tool_state.axis_method = None tool.Blender.update_viewport() diff --git a/src/bonsai/bonsai/bim/module/model/profile.py b/src/bonsai/bonsai/bim/module/model/profile.py index d96e7d0b51..8448d4d857 100644 --- a/src/bonsai/bonsai/bim/module/model/profile.py +++ b/src/bonsai/bonsai/bim/module/model/profile.py @@ -48,6 +48,7 @@ class DumbProfileGenerator: self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) def generate(self, insertion_type="CURSOR"): + self.insertion_type = insertion_type self.file = tool.Ifc.get() self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) material = ifcopenshell.util.element.get_material(self.relating_type) @@ -70,9 +71,9 @@ class DumbProfileGenerator: self.rotation = 0 self.location = Vector((0, 0, 0)) self.cardinal_point = int(props.cardinal_point) - if insertion_type == "POLYLINE": + if self.insertion_type == "POLYLINE": return self.derive_from_polyline() - elif insertion_type == "CURSOR": + elif self.insertion_type == "CURSOR": return self.derive_from_cursor() def derive_from_polyline(self) -> tuple[list[Union[dict[str, Any], None]], bool]: @@ -107,13 +108,17 @@ class DumbProfileGenerator: matrix_world = Matrix() if self.relating_type.is_a() not in ("IfcColumnType", "IfcPileType"): - matrix_world = Matrix.Rotation(pi / 2, 4, "Z") @ Matrix.Rotation(pi / 2, 4, "X") @ matrix_world - matrix_world = Matrix.Rotation(self.rotation, 4, "Z") @ matrix_world + if self.insertion_type not in {"POLYLINE"}: + matrix_world = Matrix.Rotation(pi / 2, 4, "Z") @ Matrix.Rotation(pi / 2, 4, "X") @ matrix_world + matrix_world = Matrix.Rotation(self.rotation, 4, "Z") @ matrix_world + else: + rotation_matrix = self.direction.to_track_quat('Z', 'Y') + matrix_world = rotation_matrix.to_matrix().to_4x4() @ matrix_world + matrix_world.translation = self.location - if self.container_obj: + if self.insertion_type not in {"POLYLINE"} and self.container_obj: matrix_world.translation.z = self.container_obj.location.z - element = bonsai.core.root.assign_class( tool.Ifc, tool.Collector, @@ -171,19 +176,19 @@ class DumbProfileGenerator: return obj def create_profile_from_2_points(self, coords, should_round=False) -> Union[dict[str, Any], None]: - direction = coords[1] - coords[0] - length = direction.length + self.direction = coords[1] - coords[0] + length = self.direction.length if round(length, 4) < 0.1: return data = {"coords": coords} self.depth = length - self.rotation = atan2(direction[1], direction[0]) + self.rotation = atan2(self.direction[1], self.direction[0]) if should_round: # Round to nearest 50mm (yes, metric for now) self.length = 0.05 * round(length / 0.05) # Round to nearest 5 degrees - nearest_degree = (math.pi / 180) * 5 + nearest_degree = (pi / 180) * 5 self.rotation = nearest_degree * round(self.rotation / nearest_degree) self.location = coords[0] data["obj"] = self.create_profile() @@ -1122,6 +1127,8 @@ class DrawPolylineProfile(bpy.types.Operator, PolylineOperator, tool.Ifc.Operato def __init__(self): super().__init__() + self.input_ui = tool.Polyline.create_input_ui(init_z=True) + self.input_options = ["D", "A", "X", "Y", "Z"] self.relating_type = None props = tool.Model.get_model_props() relating_type_id = props.relating_type_id @@ -1166,7 +1173,9 @@ class DrawPolylineProfile(bpy.types.Operator, PolylineOperator, tool.Ifc.Operato self.handle_mouse_move(context, event, should_round=True) - self.choose_axis(event) + self.choose_axis(event, z=True) + + self.choose_plane(event) self.handle_snap_selection(context, event) @@ -1202,6 +1211,6 @@ class DrawPolylineProfile(bpy.types.Operator, PolylineOperator, tool.Ifc.Operato def _invoke(self, context, event): super().invoke(context, event) ProductDecorator.install(context) - self.tool_state.use_default_container = True - self.tool_state.plane_method = "XY" + self.tool_state.use_default_container = False + self.tool_state.plane_method = None return {"RUNNING_MODAL"}