diff --git a/src/blenderbim/blenderbim/bim/module/cad/workspace.py b/src/blenderbim/blenderbim/bim/module/cad/workspace.py index e8c91b3e9f..e5d7ef85e9 100644 --- a/src/blenderbim/blenderbim/bim/module/cad/workspace.py +++ b/src/blenderbim/blenderbim/bim/module/cad/workspace.py @@ -18,6 +18,7 @@ import os import bpy +import blenderbim.tool as tool import blenderbim.bim.module.type.prop as type_prop from bpy.types import WorkSpaceTool from blenderbim.bim.module.model.data import AuthoringData @@ -51,9 +52,14 @@ class CadTool(WorkSpaceTool): 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.align_view_to_profile", text="", icon="AXIS_FRONT") - row.operator("bim.disable_editing_extrusion_profile", text="", icon="CANCEL") + if obj.BIMObjectProperties.ifc_definition_id: + row.operator("bim.edit_extrusion_profile", text="Save Profile") + row.operator("bim.align_view_to_profile", text="", icon="AXIS_FRONT") + row.operator("bim.disable_editing_extrusion_profile", text="", icon="CANCEL") + else: + row.operator("bim.edit_arbitrary_profile", text="Save Profile") + row.operator("bim.align_view_to_profile", text="", icon="AXIS_FRONT") + row.operator("bim.disable_editing_arbitrary_profile", text="", icon="CANCEL") row = layout.row(align=True) row.label(text="", icon="EVENT_SHIFT") @@ -180,7 +186,10 @@ class CadHotkey(bpy.types.Operator): bpy.ops.bim.cad_offset(distance=self.props.distance) def hotkey_S_Q(self): - bpy.ops.bim.edit_extrusion_profile() + if tool.Ifc.get_entity(bpy.context.active_object): + bpy.ops.bim.edit_extrusion_profile() + else: + bpy.ops.bim.edit_arbitrary_profile() def hotkey_S_R(self): if self.is_profile(): diff --git a/src/blenderbim/blenderbim/bim/module/model/slab.py b/src/blenderbim/blenderbim/bim/module/model/slab.py index a9fd989b82..f454957b25 100644 --- a/src/blenderbim/blenderbim/bim/module/model/slab.py +++ b/src/blenderbim/blenderbim/bim/module/model/slab.py @@ -173,10 +173,9 @@ class DumbSlabGenerator: matrix_world[2][3] = self.collection_obj.location[2] - self.depth else: matrix_world[2][3] -= self.depth - obj.matrix_world = matrix_world + obj.matrix_world = Matrix.Rotation(self.x_angle, 4, 'X') @ matrix_world bpy.context.view_layer.update() self.collection.objects.link(obj) - obj.matrix_world = Matrix.Rotation(self.x_angle, 4, 'X') element = blenderbim.core.root.assign_class( tool.Ifc, @@ -618,122 +617,13 @@ class EnableEditingExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator): else: position = Matrix() - self.vertices = [] - self.edges = [] - self.arcs = [] - self.circles = [] - - profile = extrusion.SweptArea - if profile.is_a("IfcArbitraryClosedProfileDef"): - 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) - elif profile.is_a() == "IfcRectangleProfileDef": - self.process_rectangle(obj, position, profile) - - mesh = bpy.data.meshes.new("Profile") - mesh.from_pydata(self.vertices, self.edges, []) - mesh.BIMMeshProperties.is_profile = True - obj.data = mesh - - for arc in self.arcs: - group = obj.vertex_groups.new(name="IFCARCINDEX") - group.add(arc, 1, "REPLACE") - - for circle in self.circles: - group = obj.vertex_groups.new(name="IFCCIRCLE") - group.add(circle, 1, "REPLACE") + tool.Model.import_profile(extrusion.SweptArea, obj=obj, position=position) bpy.ops.object.mode_set(mode="EDIT") DecorationsHandler.install(context) bpy.ops.wm.tool_set_by_id(name="bim.cad_tool") return {"FINISHED"} - 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(self.convert_unit_to_si(point.Coordinates)).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) # Close the loop - 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 - local_point = self.convert_unit_to_si(curve.Points.CoordList[segment[0][0] - 1]) - global_point = position @ Vector(local_point).to_3d() - self.vertices.append(global_point) - local_point = self.convert_unit_to_si(curve.Points.CoordList[segment[0][1] - 1]) - global_point = position @ Vector(local_point).to_3d() - self.vertices.append(global_point) - self.arcs.append([len(self.vertices) - 2, len(self.vertices) - 1]) - else: - local_point = self.convert_unit_to_si(curve.Points.CoordList[segment[0][0] - 1]) - global_point = position @ Vector(local_point).to_3d() - self.vertices.append(global_point) - if is_arc: - self.arcs[-1].append(len(self.vertices) - 1) - is_arc = False - else: - for local_point in curve.Points.CoordList: - global_point = position @ Vector(self.convert_unit_to_si(local_point)).to_3d() - self.vertices.append(global_point) - # Curves without segments are self closing - del self.vertices[-1] - - self.edges.extend([(i, i + 1) for i in range(offset, len(self.vertices))]) - self.edges[-1] = (len(self.vertices) - 1, offset) # Close the loop - elif curve.is_a("IfcCircle"): - center = self.convert_unit_to_si( - Matrix(ifcopenshell.util.placement.get_axis2placement(curve.Position).tolist()).col[3].to_3d() - ) - radius = self.convert_unit_to_si(curve.Radius) - self.vertices.extend( - [ - position @ Vector((center[0], center[1] - curve.Radius, 0.0)), - position @ Vector((center[0], center[1] + curve.Radius, 0.0)), - ] - ) - self.circles.append([offset, offset + 1]) - self.edges.append((offset, offset + 1)) - - def process_rectangle(self, obj, position, profile): - if profile.Position: - p_position = Matrix(ifcopenshell.util.placement.get_axis2placement(profile.Position).tolist()) - p_position[0][3] *= self.unit_scale - p_position[1][3] *= self.unit_scale - p_position[2][3] *= self.unit_scale - else: - p_position = Matrix() - - x = self.convert_unit_to_si(profile.XDim) - y = self.convert_unit_to_si(profile.YDim) - - self.vertices.extend( - [ - position @ p_position @ Vector((-x / 2, -y / 2, 0.0)), - position @ p_position @ Vector((x / 2, -y / 2, 0.0)), - position @ p_position @ Vector((x / 2, y / 2, 0.0)), - position @ p_position @ Vector((-x / 2, y / 2, 0.0)), - ] - ) - self.edges.extend([(i, i + 1) for i in range(0, len(self.vertices))]) - self.edges[-1] = (len(self.vertices) - 1, 0) # Close the loop - - def convert_unit_to_si(self, value): - if isinstance(value, (tuple, list)): - return [v * self.unit_scale for v in value] - return value * self.unit_scale - class EditExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.edit_extrusion_profile" @@ -758,40 +648,16 @@ class EditExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator): else: position = Matrix() - 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 + profile = tool.Model.export_profile(obj, position=position) + if not profile: def msg(self, context): self.layout.label(text="INVALID PROFILE: " + indices[1]) bpy.context.window_manager.popup_menu(msg, title="Error", icon="ERROR") DecorationsHandler.install(context) bpy.ops.object.mode_set(mode="EDIT") - return {"FINISHED"} - - self.bm = bmesh.new() - self.bm.from_mesh(obj.data) - self.bm.verts.ensure_lookup_table() - self.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 - - self.bm.free() + return old_profile = extrusion.SweptArea extrusion.SweptArea = profile @@ -831,45 +697,6 @@ class EditExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator): ) 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(self.convert_si_to_unit(list(local_point))) - return tool.Ifc.get().createIfcCartesianPointList2D(points) - - def create_curve(self, position, edge_indices, points=None): - position_i = position.inverted() - if len(edge_indices) == 2: - diameter = edge_indices[0] - p1 = self.bm.verts[diameter[0]].co - p2 = self.bm.verts[diameter[1]].co - center = self.convert_si_to_unit(list(position_i @ p1.lerp(p2, 0.5))) - radius = self.convert_si_to_unit((p1 - p2).length / 2) - return tool.Ifc.get().createIfcCircle( - tool.Ifc.get().createIfcAxis2Placement2D(tool.Ifc.get().createIfcCartesianPoint(center[0:2])), radius - ) - if tool.Ifc.get().schema == "IFC2X3": - points = [] - for edge in edge_indices: - local_point = (position_i @ Vector(self.bm.verts[edge[0]].co)).to_2d() - points.append(tool.Ifc.get().createIfcCartesianPoint(self.convert_si_to_unit(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 convert_si_to_unit(self, value): - if isinstance(value, (tuple, list)): - return [v / self.unit_scale for v in value] - return value / self.unit_scale - class ResetVertex(bpy.types.Operator): bl_idname = "bim.reset_vertex" diff --git a/src/blenderbim/blenderbim/bim/module/model/workspace.py b/src/blenderbim/blenderbim/bim/module/model/workspace.py index 96d3dfc2d8..9dd7ab6c64 100644 --- a/src/blenderbim/blenderbim/bim/module/model/workspace.py +++ b/src/blenderbim/blenderbim/bim/module/model/workspace.py @@ -413,7 +413,9 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator): bpy.ops.bim.align_product(align_type="CENTERLINE") def hotkey_S_E(self): - if self.active_class in ("IfcWall", "IfcWallStandardCase"): + if not bpy.context.selected_objects: + return + elif self.active_class in ("IfcWall", "IfcWallStandardCase"): bpy.ops.bim.join_wall(join_type="T") elif self.active_class in ("IfcSlab", "IfcSlabStandardCase", "IfcRamp", "IfcRoof"): if not bpy.context.active_object: diff --git a/src/blenderbim/blenderbim/bim/module/profile/__init__.py b/src/blenderbim/blenderbim/bim/module/profile/__init__.py index 86636ba5ff..101e9a3cde 100644 --- a/src/blenderbim/blenderbim/bim/module/profile/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/profile/__init__.py @@ -21,9 +21,12 @@ from . import ui, prop, operator classes = ( operator.AddProfileDef, + operator.DisableEditingArbitraryProfile, operator.DisableEditingProfile, operator.DisableProfileEditingUI, + operator.EditArbitraryProfile, operator.EditProfile, + operator.EnableEditingArbitraryProfile, operator.EnableEditingProfile, operator.LoadProfiles, operator.RemoveProfileDef, diff --git a/src/blenderbim/blenderbim/bim/module/profile/data.py b/src/blenderbim/blenderbim/bim/module/profile/data.py index d98747f658..e0da6336c5 100644 --- a/src/blenderbim/blenderbim/bim/module/profile/data.py +++ b/src/blenderbim/blenderbim/bim/module/profile/data.py @@ -31,7 +31,12 @@ class ProfileData: @classmethod def load(cls): - cls.data = {"total_profiles": cls.total_profiles(), "profile_classes": cls.profile_classes()} + cls.data = { + "total_profiles": cls.total_profiles(), + "profile_classes": cls.profile_classes(), + "is_arbitrary_profile": cls.is_arbitrary_profile(), + "is_editing_arbitrary_profile": cls.is_editing_arbitrary_profile(), + } cls.is_loaded = True @classmethod @@ -52,3 +57,16 @@ class ProfileData: (c, c, ifcopenshell.util.doc.get_entity_doc(schema_identifier, c)["description"] or "") for c in sorted(classes) ] + + @classmethod + def is_arbitrary_profile(cls): + props = bpy.context.scene.BIMProfileProperties + if props.active_profile_id: + profile = tool.Ifc.get().by_id(props.active_profile_id) + if profile.is_a("IfcArbitraryClosedProfileDef"): + return True + + @classmethod + def is_editing_arbitrary_profile(cls): + obj = bpy.context.active_object + return obj and obj.data and hasattr(obj.data, "BIMMeshProperties") and obj.data.BIMMeshProperties.is_profile diff --git a/src/blenderbim/blenderbim/bim/module/profile/operator.py b/src/blenderbim/blenderbim/bim/module/profile/operator.py index 453a788ec8..b72927661c 100644 --- a/src/blenderbim/blenderbim/bim/module/profile/operator.py +++ b/src/blenderbim/blenderbim/bim/module/profile/operator.py @@ -20,6 +20,7 @@ import bpy import ifcopenshell.api import blenderbim.bim.helper import blenderbim.tool as tool +from blenderbim.bim.module.model.slab import DecorationsHandler class LoadProfiles(bpy.types.Operator): @@ -35,6 +36,7 @@ class LoadProfiles(bpy.types.Operator): new = props.profiles.add() new.ifc_definition_id = profile.id() new.name = profile.ProfileName or "Unnamed" + new.ifc_class = profile.is_a() props.is_editing = True bpy.ops.bim.disable_editing_profile() @@ -60,21 +62,19 @@ class RemoveProfileDef(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): ifcopenshell.api.run("profile.remove_profile", tool.Ifc.get(), profile=tool.Ifc.get().by_id(self.profile)) bpy.ops.bim.load_profiles() - return {"FINISHED"} -class EnableEditingProfile(bpy.types.Operator): +class EnableEditingProfile(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.enable_editing_profile" bl_label = "Enable Editing Profile" bl_options = {"REGISTER", "UNDO"} profile: bpy.props.IntProperty() - def execute(self, context): + def _execute(self, context): props = context.scene.BIMProfileProperties props.profile_attributes.clear() blenderbim.bim.helper.import_attributes2(tool.Ifc.get().by_id(self.profile), props.profile_attributes) props.active_profile_id = self.profile - return {"FINISHED"} class DisableEditingProfile(bpy.types.Operator): @@ -98,7 +98,6 @@ class EditProfile(bpy.types.Operator, tool.Ifc.Operator): profile = tool.Ifc.get().by_id(props.active_profile_id) ifcopenshell.api.run("profile.edit_profile", tool.Ifc.get(), profile=profile, attributes=attributes) bpy.ops.bim.load_profiles() - return {"FINISHED"} class AddProfileDef(bpy.types.Operator, tool.Ifc.Operator): @@ -110,9 +109,69 @@ class AddProfileDef(bpy.types.Operator, tool.Ifc.Operator): props = context.scene.BIMProfileProperties profile_class = props.profile_classes if profile_class == "IfcArbitraryClosedProfileDef": - points = [(0, 0), (0.1, 0), (0.1, 0.1), (0, 0.1)] - ifcopenshell.api.run("profile.add_arbitrary_profile", tool.Ifc.get(), profile=points) + points = [(0, 0), (0.1, 0), (0.1, 0.1), (0, 0.1), (0, 0)] + profile = ifcopenshell.api.run("profile.add_arbitrary_profile", tool.Ifc.get(), profile=points) else: - ifcopenshell.api.run("profile.add_parameterized_profile", tool.Ifc.get(), ifc_class=profile_class) + profile = ifcopenshell.api.run("profile.add_parameterized_profile", tool.Ifc.get(), ifc_class=profile_class) + profile.ProfileName = "New Profile" bpy.ops.bim.load_profiles() - return {"FINISHED"} + + +class EnableEditingArbitraryProfile(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.enable_editing_arbitrary_profile" + bl_label = "Enable Editing Arbitrary Profile" + bl_options = {"REGISTER", "UNDO"} + + def _execute(self, context): + props = context.scene.BIMProfileProperties + profile = tool.Ifc.get().by_id(props.active_profile_id) + obj = tool.Model.import_profile(profile) + bpy.context.scene.collection.objects.link(obj) + bpy.context.view_layer.objects.active = obj + bpy.ops.object.mode_set(mode="EDIT") + DecorationsHandler.install(context) + bpy.ops.wm.tool_set_by_id(name="bim.cad_tool") + + +class DisableEditingArbitraryProfile(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.disable_editing_arbitrary_profile" + bl_label = "Disable Editing Arbitrary Profile" + bl_options = {"REGISTER", "UNDO"} + + def _execute(self, context): + obj = context.active_object + DecorationsHandler.uninstall() + bpy.ops.object.mode_set(mode="OBJECT") + bpy.data.objects.remove(obj) + + +class EditArbitraryProfile(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.edit_arbitrary_profile" + bl_label = "Edit Arbitrary Profile" + bl_options = {"REGISTER", "UNDO"} + + def _execute(self, context): + props = context.scene.BIMProfileProperties + old_profile = tool.Ifc.get().by_id(props.active_profile_id) + + obj = context.active_object + + DecorationsHandler.uninstall() + bpy.ops.object.mode_set(mode="OBJECT") + + profile = tool.Model.export_profile(obj) + if not profile: + def msg(self, context): + self.layout.label(text="INVALID PROFILE: " + indices[1]) + + bpy.context.window_manager.popup_menu(msg, title="Error", icon="ERROR") + DecorationsHandler.install(context) + bpy.ops.object.mode_set(mode="EDIT") + return + + bpy.data.objects.remove(obj) + profile.ProfileType = old_profile.ProfileType + profile.ProfileName = old_profile.ProfileName + ifcopenshell.util.element.remove_deep2(tool.Ifc.get(), old_profile) + bpy.ops.bim.load_profiles() + props.active_profile_id = profile.id() diff --git a/src/blenderbim/blenderbim/bim/module/profile/prop.py b/src/blenderbim/blenderbim/bim/module/profile/prop.py index 7bfea0a221..3b7b0a91ef 100644 --- a/src/blenderbim/blenderbim/bim/module/profile/prop.py +++ b/src/blenderbim/blenderbim/bim/module/profile/prop.py @@ -44,6 +44,7 @@ def get_profile_classes(self, context): class Profile(PropertyGroup): name: StringProperty(name="Name") + ifc_class: StringProperty(name="IFC Class") ifc_definition_id: IntProperty(name="IFC Definition ID") diff --git a/src/blenderbim/blenderbim/bim/module/profile/ui.py b/src/blenderbim/blenderbim/bim/module/profile/ui.py index 38c6365144..36dbd35c83 100644 --- a/src/blenderbim/blenderbim/bim/module/profile/ui.py +++ b/src/blenderbim/blenderbim/bim/module/profile/ui.py @@ -67,6 +67,14 @@ class BIM_PT_profiles(Panel): self.draw_editable_ui(context) def draw_editable_ui(self, context): + if ProfileData.data["is_arbitrary_profile"]: + if ProfileData.data["is_editing_arbitrary_profile"]: + row = self.layout.row(align=True) + row.operator("bim.edit_arbitrary_profile", text="Save Profile", icon="CHECKMARK") + row.operator("bim.disable_editing_arbitrary_profile", text="", icon="CANCEL") + else: + row = self.layout.row() + row.operator("bim.enable_editing_arbitrary_profile", text="Edit Profile", icon="GREASEPENCIL") blenderbim.bim.helper.draw_attributes(self.props.profile_attributes, self.layout) @@ -76,6 +84,7 @@ class BIM_UL_profiles(UIList): if item: row = layout.row(align=True) row.label(text=item.name or "Unnamed") + row.label(text=item.ifc_class) if props.active_profile_id == item.ifc_definition_id: row.operator("bim.edit_profile", text="", icon="CHECKMARK") diff --git a/src/blenderbim/docs/devs/installation.rst b/src/blenderbim/docs/devs/installation.rst index 37894e9f9e..c32529b36a 100644 --- a/src/blenderbim/docs/devs/installation.rst +++ b/src/blenderbim/docs/devs/installation.rst @@ -42,8 +42,8 @@ IfcOpenShell (using an IfcOpenBot build) for convenience. Instructions on how to compile IfcOpenShell is out of scope of this document. You can create your own package by using the Makefile as shown below. You can -choose between a ``PLATFORM`` of ``linux``, ``macos``, and ``win``. You can -choose between a ``PYVERSION`` of ``py39``, ``py37``, or ``py310``. +choose between a ``PLATFORM`` of ``linux``, ``macos``, ``macosm1``, and ``win``. +You can choose between a ``PYVERSION`` of ``py39``, ``py37``, or ``py310``. :: $ cd src/blenderbim