diff --git a/src/blenderbim/blenderbim/bim/module/cad/workspace.py b/src/blenderbim/blenderbim/bim/module/cad/workspace.py index 2203846f36..13a4f2aae3 100644 --- a/src/blenderbim/blenderbim/bim/module/cad/workspace.py +++ b/src/blenderbim/blenderbim/bim/module/cad/workspace.py @@ -37,6 +37,7 @@ class CadTool(WorkSpaceTool): bl_keymap = ( ("bim.cad_hotkey", {"type": "E", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_E")]}), ("bim.cad_hotkey", {"type": "T", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_T")]}), + ("bim.cad_hotkey", {"type": "Q", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_Q")]}), ("bim.cad_fillet", {"type": "F", "value": "PRESS", "shift": True}, {"properties": []}), # Enable Mesh Tools add-on to get this amazing tool ("mesh.offset_edges", {"type": "O", "value": "PRESS", "shift": True}, {"properties": []}), @@ -45,6 +46,12 @@ class CadTool(WorkSpaceTool): ) def draw_settings(context, layout, tool): + row = layout.row(align=True) + row.label(text="", icon="EVENT_SHIFT") + row.label(text="", icon="EVENT_Q") + row.operator("bim.edit_extrusion_profile", text="Save Profile") + row.operator("bim.disable_editing_extrusion_profile", text="", icon="CANCEL") + row = layout.row(align=True) row.label(text="", icon="EVENT_SHIFT") row.label(text="Extend", icon="EVENT_E") @@ -91,3 +98,6 @@ class CadHotkey(bpy.types.Operator): def hotkey_S_T(self): bpy.ops.bim.cad_mitre() + + def hotkey_S_Q(self): + bpy.ops.bim.edit_extrusion_profile() diff --git a/src/blenderbim/blenderbim/bim/module/classification/data.py b/src/blenderbim/blenderbim/bim/module/classification/data.py index 6e12ecc83f..cb4a1c41db 100644 --- a/src/blenderbim/blenderbim/bim/module/classification/data.py +++ b/src/blenderbim/blenderbim/bim/module/classification/data.py @@ -130,9 +130,11 @@ class CostClassificationsData(ReferencesData): @classmethod def references(cls): results = [] - element = tool.Ifc.get().by_id(bpy.context.scene.BIMCostProperties.cost_items[ - bpy.context.scene.BIMCostProperties.active_cost_item_index - ].ifc_definition_id) + element = tool.Ifc.get().by_id( + bpy.context.scene.BIMCostProperties.cost_items[ + bpy.context.scene.BIMCostProperties.active_cost_item_index + ].ifc_definition_id + ) if element: for reference in ifcopenshell.util.classification.get_references(element): data = reference.get_info() diff --git a/src/blenderbim/blenderbim/bim/module/geometry/data.py b/src/blenderbim/blenderbim/bim/module/geometry/data.py index c9b94c5411..935cb68d28 100644 --- a/src/blenderbim/blenderbim/bim/module/geometry/data.py +++ b/src/blenderbim/blenderbim/bim/module/geometry/data.py @@ -79,19 +79,23 @@ class ConnectionsData: results = [] element = tool.Ifc.get_entity(bpy.context.active_object) for rel in getattr(element, "ConnectedTo", []): - results.append({ - "id": rel.id(), - "is_relating": True, - "Name": rel.RelatedElement.Name or "Unnamed", - "ConnectionType": rel.RelatingConnectionType, - }) + results.append( + { + "id": rel.id(), + "is_relating": True, + "Name": rel.RelatedElement.Name or "Unnamed", + "ConnectionType": rel.RelatingConnectionType, + } + ) for rel in getattr(element, "ConnectedFrom", []): - results.append({ - "id": rel.id(), - "is_relating": False, - "Name": rel.RelatingElement.Name or "Unnamed", - "ConnectionType": rel.RelatedConnectionType, - }) + results.append( + { + "id": rel.id(), + "is_relating": False, + "Name": rel.RelatingElement.Name or "Unnamed", + "ConnectionType": rel.RelatedConnectionType, + } + ) return results diff --git a/src/blenderbim/blenderbim/bim/module/geometry/helper.py b/src/blenderbim/blenderbim/bim/module/geometry/helper.py index d6c7ddd8ad..433825fe30 100644 --- a/src/blenderbim/blenderbim/bim/module/geometry/helper.py +++ b/src/blenderbim/blenderbim/bim/module/geometry/helper.py @@ -112,6 +112,142 @@ class Helper: return {"profile": profile, "extrusion": extrusion} + def auto_detect_arbitrary_profile_with_voids(self, obj, mesh): + arc_groups = [] + for i, group in enumerate(obj.vertex_groups): + if "IFCARCINDEX" in group.name: + arc_groups.append(i) + + bm = bmesh.new() + bm.from_mesh(mesh) + bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=1e-5) + + # https://docs.blender.org/api/blender_python_api_2_63_8/bmesh.html#CustomDataAccess + # This is how we access vertex groups via bmesh, apparently, it's not very intuitive + deform_layer = bm.verts.layers.deform.active + + # Sanity check + group_verts = {} + for vert in bm.verts: + if len(vert.link_edges) != 2: # Unclosed loop or forked loop + return (False, "UNCLOSED_LOOP") + total_groups = 0 + for group_index in arc_groups: + if group_index in vert[deform_layer]: + group_verts.setdefault(group_index, []).append(vert) + total_groups += 0 + if total_groups > 1: # A vert can only belong to one arc + return (False, "AMBIGUOUS_ARC") + for verts in group_verts.values(): + if len(verts) != 3: # Each arc needs 3 verts + return (False, "3POINT_ARC") + + loop_edges = set(bm.edges) + + # Create loops from edges + loops = [] + while loop_edges: + edge = loop_edges.pop() + loop = [edge] + has_found_connected_edge = True + while has_found_connected_edge: + has_found_connected_edge = False + for edge in loop_edges.copy(): + edge_verts = set(edge.verts) + if edge_verts & set(loop[0].verts): + loop.insert(0, edge) + loop_edges.remove(edge) + has_found_connected_edge = True + elif edge_verts & set(loop[-1].verts): + loop.append(edge) + loop_edges.remove(edge) + has_found_connected_edge = True + loops.append(loop) + + # Determine outer loop + max_area = 0 + outer_loop = None + inner_loops = [] + + for loop in loops: + loop_vertices = [] + total_edges = len(loop) + for i, edge in enumerate(loop): + if i + 1 == total_edges and edge.verts[0] in loop[i - 1].verts: + loop_vertices.append(edge.verts[0]) + elif i + 1 == total_edges and edge.verts[1] in loop[i - 1].verts: + loop_vertices.append(edge.verts[1]) + elif edge.verts[0] in loop[i + 1].verts: + loop_vertices.append(edge.verts[1]) + elif edge.verts[1] in loop[i + 1].verts: + loop_vertices.append(edge.verts[0]) + + loop_bm = bmesh.new() + for vert in loop_vertices: + loop_bm.verts.new(vert.co) + face = loop_bm.faces.new(loop_bm.verts) + + loop_vertex_indices = [] + active_arc_id = None + arc_stack = [] + last_index = len(loop_vertices) - 1 + + # It is possible to loop through loop_vertices halfway through an arc. + # If that is the case, we store it in incomplete_arc + incomplete_arc = [] + for i, v in enumerate(loop_vertices): + if len(arc_stack) == 3: + loop_vertex_indices.append(arc_stack) + loop_vertex_indices.append((arc_stack[-1], v.index)) + arc_stack = [] + active_arc_id = None + + is_arc = False + for group_index in arc_groups: + if group_index in v[deform_layer]: + if active_arc_id is not None and active_arc_id != group_index: + incomplete_arc = arc_stack + arc_stack = [] + active_arc_id = group_index + is_arc = True + break + + if i == last_index and not is_arc: + continue + + if is_arc: + arc_stack.append(v.index) + else: + if active_arc_id is not None: + incomplete_arc = arc_stack + active_arc_id = None + arc_stack = [] + loop_vertex_indices.append((v.index, loop_vertices[i + 1].index)) + + if active_arc_id is not None: + arc_stack.extend(incomplete_arc) + loop_vertex_indices.append(arc_stack) + + loop_vertex_indices.append((loop_vertex_indices[-1][-1], loop_vertex_indices[0][0])) + # loop_vertex_indices = [v.index for v in loop_vertices] + + face_area = face.calc_area() + if face_area > max_area: + max_area = face_area + outer_loop = loop_vertex_indices + inner_loops.append(loop_vertex_indices) + loop_bm.free() + + inner_loops.remove(outer_loop) + + points = [v.co for v in bm.verts] + + bm.to_mesh(mesh) + mesh.update() + bm.free() + + return {"points": points, "profile": outer_loop, "inner_curves": inner_loops} + # An arbitrary closed profile with voids is similar to one without voids. # We start the same way with any ngon (no tri), but instead of being the entire # profile, it is only one of the possible faces that make up the end of our diff --git a/src/blenderbim/blenderbim/bim/module/model/__init__.py b/src/blenderbim/blenderbim/bim/module/model/__init__.py index 75ba33ac8d..a92aadef5d 100644 --- a/src/blenderbim/blenderbim/bim/module/model/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/model/__init__.py @@ -40,8 +40,12 @@ classes = ( profile.ExtendProfile, profile.RecalculateProfile, profile.Rotate90, - slab.EnableEditingExtrusionProfile, + slab.DisableEditingExtrusionProfile, slab.EditExtrusionProfile, + slab.EditSketchExtrusionProfile, + slab.EnableEditingExtrusionProfile, + slab.EnableEditingSketchExtrusionProfile, + slab.SetArcIndex, prop.BIMModelProperties, prop.ConstrTypeInfo, ui.BIM_PT_authoring, diff --git a/src/blenderbim/blenderbim/bim/module/model/slab.py b/src/blenderbim/blenderbim/bim/module/model/slab.py index ca97356b6b..7c6779be8f 100644 --- a/src/blenderbim/blenderbim/bim/module/model/slab.py +++ b/src/blenderbim/blenderbim/bim/module/model/slab.py @@ -16,6 +16,10 @@ # You should have received a copy of the GNU General Public License # along with BlenderBIM Add-on. If not, see . +import os +import gpu +import blf +import bgl import bpy import bmesh import math @@ -28,12 +32,15 @@ import blenderbim.bim.handler import blenderbim.core.type import blenderbim.core.geometry import blenderbim.tool as tool +from bpy.types import SpaceView3D from blenderbim.bim.ifc import IfcStore from math import pi, degrees from mathutils import Vector, Matrix from ifcopenshell.api.pset.data import Data as PsetData from ifcopenshell.api.material.data import Data as MaterialData from blenderbim.bim.module.geometry.helper import Helper +from gpu.types import GPUShader, GPUBatch, GPUIndexBuf, GPUVertBuf, GPUVertFormat +from gpu_extras.batch import batch_for_shader def element_listener(element, obj): @@ -385,9 +392,9 @@ class DumbSlabPlaner: obj.location[2] -= delta_thickness -class EnableEditingExtrusionProfile(bpy.types.Operator): +class EnableEditingSketchExtrusionProfile(bpy.types.Operator): bl_idname = "bim.xxx" - bl_label = "Enable Editing Extrusion Profile" + bl_label = "Enable Editing Sketch Extrusion Profile" bl_options = {"REGISTER", "UNDO"} @classmethod @@ -446,7 +453,7 @@ class EnableEditingExtrusionProfile(bpy.types.Operator): break -class EditExtrusionProfile(bpy.types.Operator): +class EditSketchExtrusionProfile(bpy.types.Operator): bl_idname = "bim.xxe" bl_label = "Edit Extrusion Profile" bl_options = {"REGISTER", "UNDO"} @@ -464,7 +471,6 @@ class EditExtrusionProfile(bpy.types.Operator): sketch = context.scene.sketcher.active_sketch converter = cad_sketcher.convertors.BezierConverter(context.scene, sketch) converter.run() - print(converter.paths) profile = tool.Ifc.get().createIfcArbitraryClosedProfileDef("AREA") for path in converter.paths: @@ -513,3 +519,458 @@ class EditExtrusionProfile(bpy.types.Operator): item = item.FirstOperand else: break + + +class DisableEditingExtrusionProfile(bpy.types.Operator): + bl_idname = "bim.disable_editing_extrusion_profile" + bl_label = "Disable Editing Extrusion Profile" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + return context.selected_objects + + def execute(self, context): + DecorationsHandler.uninstall() + bpy.ops.object.mode_set(mode="OBJECT") + + obj = context.active_object + element = tool.Ifc.get_entity(obj) + body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW") + + blenderbim.core.geometry.switch_representation( + tool.Geometry, + obj=obj, + representation=body, + should_reload=True, + enable_dynamic_voids=False, + is_global=True, + should_sync_changes_first=False, + ) + return {"FINISHED"} + + +class EnableEditingExtrusionProfile(bpy.types.Operator): + bl_idname = "bim.enable_editing_extrusion_profile" + bl_label = "Enable Editing Extrusion Profile" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + return context.selected_objects + + def execute(self, context): + obj = context.active_object + element = tool.Ifc.get_entity(obj) + + body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW") + extrusion = self.get_extrusion(body) + profile = extrusion.SweptArea + position = Matrix(ifcopenshell.util.placement.get_axis2placement(extrusion.Position).tolist()) + + z_values = [v[2] for v in obj.bound_box] + + element = tool.Ifc.get_entity(obj) + + origin = obj.matrix_world @ Vector((0, 0, max(z_values))) + normal = obj.matrix_world.to_quaternion() + + self.vertices = [] + self.edges = [] + self.arcs = [] + + self.process_curve(obj, position, profile.OuterCurve) + + if profile.is_a("IfcArbitraryProfileDefWithVoids"): + for inner_curve in profile.InnerCurves: + self.process_curve(obj, position, inner_curve) + + mesh = bpy.data.meshes.new("Profile") + mesh.from_pydata(self.vertices, self.edges, []) + obj.data = mesh + + for arc in self.arcs: + group = obj.vertex_groups.new(name="IFCARCINDEX") + group.add(arc, 1, "REPLACE") + + bpy.ops.object.mode_set(mode="EDIT") + DecorationsHandler.install(bpy.context) + bpy.ops.wm.tool_set_by_id(name="bim.cad_tool") + return {"FINISHED"} + + def get_extrusion(self, representation): + item = representation.Items[0] + while True: + if item.is_a("IfcExtrudedAreaSolid"): + return item + elif item.is_a("IfcBooleanClippingResult"): + item = item.FirstOperand + else: + break + + def process_curve(self, obj, position, curve): + offset = len(self.vertices) + + if curve.is_a("IfcPolyline"): + total_points = len(curve.Points) + last_index = len(curve.Points) - 1 + for i, point in enumerate(curve.Points): + if i == last_index: + continue + global_point = position @ Vector(point.Coordinates).to_3d() + self.vertices.append(global_point) + elif curve.is_a("IfcIndexedPolyCurve"): + is_arc = False + if curve.Segments: + for segment in curve.Segments: + if len(segment[0]) == 3: # IfcArcIndex + is_arc = True + global_point = position @ Vector(curve.Points.CoordList[segment[0][0] - 1]).to_3d() + self.vertices.append(global_point) + global_point = position @ Vector(curve.Points.CoordList[segment[0][1] - 1]).to_3d() + self.vertices.append(global_point) + self.arcs.append([len(self.vertices) - 2, len(self.vertices) - 1]) + else: + global_point = position @ Vector(curve.Points.CoordList[segment[0][0] - 1]).to_3d() + self.vertices.append(global_point) + if is_arc: + self.arcs[-1].append(len(self.vertices) - 1) + is_arc = False + else: + for point in curve.PointsCoordList: + global_point = position @ Vector(point).to_3d() + self.vertices.append(global_point) + + self.edges.extend([(i, i + 1) for i in range(offset, len(self.vertices))]) + self.edges[-1] = (len(self.vertices) - 1, offset) + + +class EditExtrusionProfile(bpy.types.Operator): + bl_idname = "bim.edit_extrusion_profile" + bl_label = "Edit Extrusion Profile" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + DecorationsHandler.uninstall() + bpy.ops.object.mode_set(mode="OBJECT") + + obj = context.active_object + element = tool.Ifc.get_entity(obj) + + representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW") + extrusion = self.get_extrusion(representation) + position = Matrix(ifcopenshell.util.placement.get_axis2placement(extrusion.Position).tolist()) + + helper = Helper(tool.Ifc.get()) + indices = helper.auto_detect_arbitrary_profile_with_voids(obj, obj.data) + if isinstance(indices, tuple) and indices[0] is False: # Ugly + self.report({"ERROR"}, "INVALID PROFILE: " + indices[1]) + DecorationsHandler.install(context) + bpy.ops.object.mode_set(mode="EDIT") + return {"CANCELLED"} + + bm = bmesh.new() + bm.from_mesh(obj.data) + bm.verts.ensure_lookup_table() + bm.edges.ensure_lookup_table() + + if indices["inner_curves"]: + profile = tool.Ifc.get().createIfcArbitraryProfileDefWithVoids("AREA") + else: + profile = tool.Ifc.get().createIfcArbitraryClosedProfileDef("AREA") + + if tool.Ifc.get().schema != "IFC2X3": + self.points = self.create_points(position, indices["points"]) + + profile.OuterCurve = self.create_curve(position, indices["profile"]) + if indices["inner_curves"]: + results = [] + for inner_curve in indices["inner_curves"]: + results.append(self.create_curve(position, inner_curve)) + profile.InnerCurves = results + + bm.free() + + extrusion.SweptArea = profile + + blenderbim.core.geometry.switch_representation( + tool.Geometry, + obj=obj, + representation=representation, + should_reload=True, + enable_dynamic_voids=False, + is_global=True, + should_sync_changes_first=False, + ) + return {"FINISHED"} + + def create_points(self, position, indices): + position_i = position.inverted() + points = [] + for point in indices: + local_point = (position_i @ point).to_2d() + points.append(list(local_point)) + return tool.Ifc.get().createIfcCartesianPointList2D(points) + + def create_curve(self, position, edge_indices, points=None): + position_i = position.inverted() + if tool.Ifc.get().schema == "IFC2X3": + points = [] + for edge in edge_indices: + local_point = (position_i @ Vector(bm.verts[edge[0]].co)).to_2d() + points.append(tool.Ifc.get().createIfcCartesianPoint(local_point)) + points.append(points[0]) + return tool.Ifc.get().createIfcPolyline(points) + segments = [] + for segment in edge_indices: + if len(segment) == 2: + segments.append(tool.Ifc.get().createIfcLineIndex([i + 1 for i in segment])) + elif len(segment) == 3: + segments.append(tool.Ifc.get().createIfcArcIndex([i + 1 for i in segment])) + return tool.Ifc.get().createIfcIndexedPolyCurve(self.points, segments, False) + + def get_extrusion(self, representation): + item = representation.Items[0] + while True: + if item.is_a("IfcExtrudedAreaSolid"): + return item + elif item.is_a("IfcBooleanClippingResult"): + item = item.FirstOperand + else: + break + + +class SetArcIndex(bpy.types.Operator): + bl_idname = "bim.set_arc_index" + bl_label = "Set Arc Index" + + @classmethod + def poll(cls, context): + obj = context.active_object + return bool(obj) and obj.type == "MESH" + + def cancel_message(self, msg): + self.report({"WARNING"}, msg) + return {"CANCELLED"} + + def execute(self, context): + obj = context.active_object + bpy.ops.object.mode_set(mode="OBJECT") + selected_vertices = [v.index for v in obj.data.vertices if v.select] + for group in obj.vertex_groups: + group.remove(selected_vertices) + group = obj.vertex_groups.new(name="IFCARCINDEX") + group.add(selected_vertices, 1, "REPLACE") + bpy.ops.object.mode_set(mode="EDIT") + return {"FINISHED"} + + +class DecorationsHandler: + installed = None + + @classmethod + def install(cls, context): + if cls.installed: + cls.uninstall() + handler = cls() + cls.installed = SpaceView3D.draw_handler_add(handler, (context,), "WINDOW", "POST_VIEW") + + @classmethod + def uninstall(cls): + try: + SpaceView3D.draw_handler_remove(cls.installed, "WINDOW") + except ValueError: + pass + cls.installed = None + + def __call__(self, context): + obj = context.active_object + bgl.glLineWidth(2) + bgl.glPointSize(6) + bgl.glEnable(bgl.GL_BLEND) + bgl.glEnable(bgl.GL_LINE_SMOOTH) + if not obj.data.is_editmode: + return + + all_vertices = [] + error_vertices = [] + selected_vertices = [] + unselected_vertices = [] + special_vertices = [] + special_vertex_indices = {} + selected_edges = [] + unselected_edges = [] + special_edges = [] + + arc_groups = [] + for i, group in enumerate(obj.vertex_groups): + if "IFCARCINDEX" in group.name: + arc_groups.append(i) + + arcs = {} + + bm = bmesh.from_edit_mesh(obj.data) + + # https://docs.blender.org/api/blender_python_api_2_63_8/bmesh.html#CustomDataAccess + # This is how we access vertex groups via bmesh, apparently, it's not very intuitive + deform_layer = bm.verts.layers.deform.active + + for vertex in bm.verts: + co = tuple(obj.matrix_world @ vertex.co) + all_vertices.append(co) + if vertex.hide: + continue + + is_arc = False + for group_index in arc_groups: + if group_index in vertex[deform_layer]: + is_arc = True + break + if is_arc: + arcs.setdefault(group_index, []).append(vertex) + special_vertex_indices[vertex.index] = group_index + + if vertex.select: + selected_vertices.append(co) + else: + if len(vertex.link_edges) != 2: + error_vertices.append(co) + elif is_arc: + special_vertices.append(co) + else: + unselected_vertices.append(co) + + for edge in bm.edges: + edge_indices = [v.index for v in edge.verts] + if edge.hide: + continue + if edge.select: + selected_edges.append(edge_indices) + else: + i1, i2 = edge.verts[0].index, edge.verts[1].index + if i1 in special_vertex_indices and special_vertex_indices[i1] == special_vertex_indices.get(i2, None): + special_edges.append(edge_indices) + else: + unselected_edges.append(edge_indices) + indices = [[v.index for v in e.verts] for e in bm.edges] + + white = (1, 1, 1, 1) + green = (0.545, 0.863, 0, 1) + red = (1, 0.2, 0.322, 1) + blue = (0.157, 0.565, 1, 1) + grey = (0.2, 0.2, 0.2, 1) + + self.shader = gpu.shader.from_builtin("3D_UNIFORM_COLOR") + + batch = batch_for_shader(self.shader, "LINES", {"pos": all_vertices}, indices=unselected_edges) + self.shader.bind() + self.shader.uniform_float("color", white) + batch.draw(self.shader) + + batch = batch_for_shader(self.shader, "LINES", {"pos": all_vertices}, indices=selected_edges) + self.shader.uniform_float("color", green) + batch.draw(self.shader) + + batch = batch_for_shader(self.shader, "LINES", {"pos": all_vertices}, indices=special_edges) + self.shader.uniform_float("color", grey) + batch.draw(self.shader) + + batch = batch_for_shader(self.shader, "POINTS", {"pos": unselected_vertices}) + self.shader.uniform_float("color", white) + batch.draw(self.shader) + + batch = batch_for_shader(self.shader, "POINTS", {"pos": error_vertices}) + self.shader.uniform_float("color", red) + batch.draw(self.shader) + + batch = batch_for_shader(self.shader, "POINTS", {"pos": special_vertices}) + self.shader.uniform_float("color", blue) + batch.draw(self.shader) + + batch = batch_for_shader(self.shader, "POINTS", {"pos": selected_vertices}) + self.shader.uniform_float("color", green) + batch.draw(self.shader) + + arc_centroids = [] + arc_segments = [] + for arc in arcs.values(): + if len(arc) != 3: + continue + sorted_arc = [None, None, None] + for v1 in arc: + connections = 0 + for link_edge in v1.link_edges: + v2 = link_edge.other_vert(v1) + if v2 in arc: + connections += 1 + if connections == 2: # Midpoint + sorted_arc[1] = v1 + else: + sorted_arc[2 if sorted_arc[2] is None else 0] = v1 + points = [tuple(obj.matrix_world @ v.co) for v in sorted_arc] + centroid = tool.Cad.get_center_of_arc(points) + if centroid: + arc_centroids.append(tuple(centroid)) + arc_segments.append(self.generate_3PT_mode_1(pts=points, num_verts=17, make_edges=True)) + + batch = batch_for_shader(self.shader, "POINTS", {"pos": arc_centroids}) + self.shader.uniform_float("color", (0.2, 0.2, 0.2, 1)) + batch.draw(self.shader) + + for verts, edges in arc_segments: + batch = batch_for_shader(self.shader, "LINES", {"pos": verts}, indices=edges) + self.shader.uniform_float("color", (0.157, 0.565, 1, 1)) + batch.draw(self.shader) + + # https://github.com/nortikin/sverchok/blob/master/nodes/generator/basic_3pt_arc.py + # This generate_3PT_mode_1 function is taken from Sverchok, licensed under GPL v2-or-later. + # No functional modifications have been made. + def generate_3PT_mode_1(self, pts=None, num_verts=20, make_edges=False): + """ + Arc from [start - through - end] + - call this function only if you have 3 pts, + - do your error checking before passing to it. + """ + num_verts -= 1 + verts, edges = [], [] + V = Vector + + # construction + v1, v2, v3, v4 = V(pts[0]), V(pts[1]), V(pts[1]), V(pts[2]) + edge1_mid = v1.lerp(v2, 0.5) + edge2_mid = v3.lerp(v4, 0.5) + axis = mathutils.geometry.normal(v1, v2, v4) + mat_rot = mathutils.Matrix.Rotation(math.radians(90.0), 4, axis) + + # triangle edges + v1_ = ((v1 - edge1_mid) @ mat_rot) + edge1_mid + v2_ = ((v2 - edge1_mid) @ mat_rot) + edge1_mid + v3_ = ((v3 - edge2_mid) @ mat_rot) + edge2_mid + v4_ = ((v4 - edge2_mid) @ mat_rot) + edge2_mid + + r = mathutils.geometry.intersect_line_line(v1_, v2_, v3_, v4_) + if r: + # do arc + p1, _ = r + + # find arc angle. + a = (v1 - p1).angle((v4 - p1), 0) + s = (2 * math.pi) - a + + interior_angle = (v1 - v2).angle(v4 - v3, 0) + if interior_angle > 0.5 * math.pi: + s = math.pi + 2 * (0.5 * math.pi - interior_angle) + + for i in range(num_verts + 1): + mat_rot = mathutils.Matrix.Rotation(((s / num_verts) * i), 4, axis) + vec = ((v4 - p1) @ mat_rot) + p1 + verts.append(vec[:]) + else: + # do straight line + step_size = 1 / num_verts + verts = [v1_.lerp(v4_, i * step_size)[:] for i in range(num_verts + 1)] + + if make_edges: + edges = [(n, n + 1) for n in range(len(verts) - 1)] + + return verts, edges diff --git a/src/blenderbim/blenderbim/bim/module/model/workspace.py b/src/blenderbim/blenderbim/bim/module/model/workspace.py index 6a17f00671..6fa2f9d952 100644 --- a/src/blenderbim/blenderbim/bim/module/model/workspace.py +++ b/src/blenderbim/blenderbim/bim/module/model/workspace.py @@ -179,6 +179,15 @@ class BimTool(WorkSpaceTool): row.label(text="", icon="EVENT_S") row.operator("bim.hotkey", text="Split").hotkey = "S_S" + if ifc_class == "IfcSlabType": + if not context.active_object: + pass + elif context.active_object.mode == "OBJECT": + row = layout.row(align=True) + row.label(text="", icon="EVENT_SHIFT") + row.label(text="", icon="EVENT_E") + row.operator("bim.hotkey", text="Edit Profile").hotkey = "S_E" + if ifc_class in ("IfcColumnType", "IfcBeamType", "IfcMemberType"): row = layout.row(align=True) row.prop(data=props, property="cardinal_point", text="Axis") @@ -290,6 +299,11 @@ class Hotkey(bpy.types.Operator): return if self.props.ifc_class == "IfcWallType": bpy.ops.bim.join_wall(join_type="T") + elif self.props.ifc_class == "IfcSlabType": + if not bpy.context.active_object: + pass + elif bpy.context.active_object.mode == "OBJECT": + bpy.ops.bim.enable_editing_extrusion_profile() elif self.props.ifc_class in ["IfcColumnType", "IfcBeamType", "IfcMemberType"]: bpy.ops.bim.extend_profile(join_type="T") @@ -303,6 +317,24 @@ class Hotkey(bpy.types.Operator): elif self.props.ifc_class in ["IfcColumnType", "IfcBeamType", "IfcMemberType"]: bpy.ops.bim.recalculate_profile() + def hotkey_S_M(self): + if self.has_ifc_class and self.props.ifc_class == "IfcWallType": + bpy.ops.bim.merge_wall() + + def hotkey_S_R(self): + if not self.has_ifc_class: + return + if self.props.ifc_class == "IfcWallType": + bpy.ops.bim.rotate_90(axis="Z") + elif self.props.ifc_class == "IfcColumnType": + bpy.ops.bim.rotate_90(axis="Z") + elif self.props.ifc_class in ["IfcBeamType", "IfcMemberType"]: + bpy.ops.bim.rotate_90(axis="Y") + + def hotkey_S_S(self): + if self.has_ifc_class and self.props.ifc_class == "IfcWallType": + bpy.ops.bim.split_wall() + def hotkey_S_T(self): if not self.has_ifc_class: return @@ -324,24 +356,6 @@ class Hotkey(bpy.types.Operator): else: bpy.ops.bim.align_product(align_type="NEGATIVE") - def hotkey_S_R(self): - if not self.has_ifc_class: - return - if self.props.ifc_class == "IfcWallType": - bpy.ops.bim.rotate_90(axis="Z") - elif self.props.ifc_class == "IfcColumnType": - bpy.ops.bim.rotate_90(axis="Z") - elif self.props.ifc_class in ["IfcBeamType", "IfcMemberType"]: - bpy.ops.bim.rotate_90(axis="Y") - - def hotkey_S_S(self): - if self.has_ifc_class and self.props.ifc_class == "IfcWallType": - bpy.ops.bim.split_wall() - - def hotkey_S_M(self): - if self.has_ifc_class and self.props.ifc_class == "IfcWallType": - bpy.ops.bim.merge_wall() - def hotkey_S_Y(self): if self.props.ifc_class == "IfcWallType": bpy.ops.bim.join_wall(join_type="V") diff --git a/src/blenderbim/blenderbim/bim/module/resource/operator.py b/src/blenderbim/blenderbim/bim/module/resource/operator.py index 573b9c49c3..436b4e2906 100644 --- a/src/blenderbim/blenderbim/bim/module/resource/operator.py +++ b/src/blenderbim/blenderbim/bim/module/resource/operator.py @@ -21,6 +21,7 @@ from bpy_extras.io_utils import ImportHelper import blenderbim.core.resource as core import blenderbim.tool as tool + class LoadResources(bpy.types.Operator): bl_idname = "bim.load_resources" bl_label = "Load Resources" diff --git a/src/blenderbim/blenderbim/bim/ui.py b/src/blenderbim/blenderbim/bim/ui.py index 670a7b4e78..31e5ab9ea5 100644 --- a/src/blenderbim/blenderbim/bim/ui.py +++ b/src/blenderbim/blenderbim/bim/ui.py @@ -114,11 +114,7 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences): should_play_chaching_sound: BoolProperty( name="Should Make A Cha-Ching Sound When Project Costs Updates", default=False ) - lock_grids_on_import: BoolProperty( - name="Will lock grids upon import", default=True - ) - - + lock_grids_on_import: BoolProperty(name="Will lock grids upon import", default=True) def draw(self, context): layout = self.layout diff --git a/src/blenderbim/blenderbim/tool/cad.py b/src/blenderbim/blenderbim/tool/cad.py index adc49b5d3d..7b19d32f7f 100644 --- a/src/blenderbim/blenderbim/tool/cad.py +++ b/src/blenderbim/blenderbim/tool/cad.py @@ -356,8 +356,8 @@ class Cad: return bm @classmethod - def get_center_of_arc(cls, pts, obj): - mw = obj.matrix_world + def get_center_of_arc(cls, pts, obj=None): + mw = obj.matrix_world if obj else None V = Vector # construction @@ -376,7 +376,7 @@ class Cad: r = geometry.intersect_line_line(v1_, v2_, v3_, v4_) if r: p1, _ = r - cp = mw @ p1 + cp = mw @ p1 if mw else p1 return cp else: print("not on a circle")