From 277317e6fd8aedf3a2c9af9a6ac7c127113b84f2 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 10 Nov 2022 15:57:08 +0600 Subject: [PATCH] Added "IFC Stair" to object modifiers 1) Added "IFC Stair" to object modifiers - it works in similar way as "IFC Array". It creates a stair from current object (if it's IfcStairFlight) and writes it's parameters to property set "BBIM_Stair" and also to IfcStairFlight parameters. Then you can edit it - modifying stair parameters and immediately see the changes, then you can save those changes or discard them. 2) "Clever Stair" operator (in Shift+A menu) works a bit different now - it creates new IfcStairFlight with IFC Stair modifier activated. --- .../blenderbim/bim/module/model/__init__.py | 9 + .../blenderbim/bim/module/model/data.py | 22 ++ .../blenderbim/bim/module/model/prop.py | 29 +- .../blenderbim/bim/module/model/stair.py | 283 +++++++++++++----- .../blenderbim/bim/module/model/ui.py | 59 +++- 5 files changed, 319 insertions(+), 83 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/model/__init__.py b/src/blenderbim/blenderbim/bim/module/model/__init__.py index d1d62126fb..5b7d60ffc7 100644 --- a/src/blenderbim/blenderbim/bim/module/model/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/model/__init__.py @@ -73,8 +73,10 @@ classes = ( prop.ConstrBrowserState, prop.BIMModelProperties, prop.BIMArrayProperties, + prop.BIMStairProperties, ui.BIM_PT_authoring, ui.BIM_PT_array, + ui.BIM_PT_stair, ui.DisplayConstrTypesUI, ui.LaunchTypeManager, ui.HelpConstrTypes, @@ -82,6 +84,11 @@ classes = ( grid.BIM_OT_add_object, stair.BIM_OT_add_object, stair.BIM_OT_add_clever_stair, + stair.AddStair, + stair.CancelEditingStair, + stair.FinishEditingStair, + stair.EnableEditingStair, + stair.RemoveStair, pie.OpenPieClass, pie.PieUpdateContainer, pie.PieAddOpening, @@ -97,6 +104,7 @@ def register(): bpy.utils.register_tool(workspace.BimTool, after={"builtin.scale_cage"}, separator=True, group=True) bpy.types.Scene.BIMModelProperties = bpy.props.PointerProperty(type=prop.BIMModelProperties) bpy.types.Object.BIMArrayProperties = bpy.props.PointerProperty(type=prop.BIMArrayProperties) + bpy.types.Object.BIMStairProperties = bpy.props.PointerProperty(type=prop.BIMStairProperties) bpy.types.VIEW3D_MT_mesh_add.append(grid.add_object_button) bpy.types.VIEW3D_MT_mesh_add.append(stair.add_object_button) bpy.types.VIEW3D_MT_add.append(ui.add_menu) @@ -114,6 +122,7 @@ def unregister(): bpy.utils.unregister_tool(workspace.BimTool) del bpy.types.Scene.BIMModelProperties del bpy.types.Object.BIMArrayProperties + del bpy.types.Object.BIMStairProperties bpy.app.handlers.load_post.remove(handler.load_post) bpy.types.VIEW3D_MT_mesh_add.remove(grid.add_object_button) bpy.types.VIEW3D_MT_mesh_add.remove(stair.add_object_button) diff --git a/src/blenderbim/blenderbim/bim/module/model/data.py b/src/blenderbim/blenderbim/bim/module/model/data.py index b9e39ad631..47b9b5039e 100644 --- a/src/blenderbim/blenderbim/bim/module/model/data.py +++ b/src/blenderbim/blenderbim/bim/module/model/data.py @@ -27,9 +27,11 @@ import blenderbim.tool as tool from blenderbim.bim.ifc import IfcStore from blenderbim.bim.module.model.root import ConstrTypeEntityNotFound + def refresh(): AuthoringData.is_loaded = False ArrayData.is_loaded = False + StairData.is_loaded = False class AuthoringData: @@ -352,3 +354,23 @@ class ArrayData: except: parameters["has_parent"] = False return parameters + + +class StairData: + data = {} + is_loaded = False + + @classmethod + def load(cls): + cls.is_loaded = True + cls.data = {"parameters": cls.parameters()} + + @classmethod + def parameters(cls): + element = tool.Ifc.get_entity(bpy.context.active_object) + if element: + psets = ifcopenshell.util.element.get_psets(element) + parameters = psets.get("BBIM_Stair", None) + if parameters: + parameters["data"] = json.loads(parameters.get("Data", "[]") or "[]") + return parameters diff --git a/src/blenderbim/blenderbim/bim/module/model/prop.py b/src/blenderbim/blenderbim/bim/module/model/prop.py index 7cb5340221..a2a356d60f 100644 --- a/src/blenderbim/blenderbim/bim/module/model/prop.py +++ b/src/blenderbim/blenderbim/bim/module/model/prop.py @@ -228,8 +228,8 @@ class BIMModelProperties(PropertyGroup): x: bpy.props.FloatProperty(name="X", default=0.5) y: bpy.props.FloatProperty(name="Y", default=0.5) z: bpy.props.FloatProperty(name="Z", default=0.5) - rl1: bpy.props.FloatProperty(name="RL", default=1) # Used for things like walls, doors, flooring, skirting, etc - rl2: bpy.props.FloatProperty(name="RL", default=1) # Used for things like windows, other hosted furniture + rl1: bpy.props.FloatProperty(name="RL", default=1) # Used for things like walls, doors, flooring, skirting, etc + rl2: bpy.props.FloatProperty(name="RL", default=1) # Used for things like windows, other hosted furniture x_angle: bpy.props.FloatProperty(name="X Angle", default=0) type_page: bpy.props.IntProperty(name="Type Page", default=1, update=update_type_page) type_template: bpy.props.EnumProperty( @@ -253,3 +253,28 @@ class BIMArrayProperties(PropertyGroup): x: bpy.props.FloatProperty(name="X", default=0) y: bpy.props.FloatProperty(name="Y", default=0) z: bpy.props.FloatProperty(name="Z", default=0) + + +class BIMStairProperties(PropertyGroup): + is_editing: bpy.props.IntProperty(default=-1) + width: bpy.props.FloatProperty(name="Width", default=1.2, soft_min=0.01) + height: bpy.props.FloatProperty(name="Height", default=1.0, soft_min=0.01) + number_of_treads: bpy.props.IntProperty(name="Number of Treads (Goings)", default=6, soft_min=1) + tread_depth: bpy.props.FloatProperty(name="Tread Depth", default=0.25, soft_min=0.01) + tread_run: bpy.props.FloatProperty(name="Tread Run", default=0.3, soft_min=0.01) + base_slab_depth: bpy.props.FloatProperty(name="Base slab depth", default=0.25, soft_min=0) + top_slab_depth: bpy.props.FloatProperty(name="Top slab depth", default=0.25, soft_min=0) + has_top_nib: bpy.props.BoolProperty(name="Has top nib", default=True) + + def get_props_kwargs(self): + kwargs = { + "width": self.width, + "height": self.height, + "number_of_treads": self.number_of_treads, + "tread_depth": self.tread_depth, + "tread_run": self.tread_run, + "base_slab_depth": self.base_slab_depth, + "top_slab_depth": self.top_slab_depth, + "has_top_nib": self.has_top_nib, + } + return kwargs diff --git a/src/blenderbim/blenderbim/bim/module/model/stair.py b/src/blenderbim/blenderbim/bim/module/model/stair.py index 0b97d43626..60f1fd2950 100644 --- a/src/blenderbim/blenderbim/bim/module/model/stair.py +++ b/src/blenderbim/blenderbim/bim/module/model/stair.py @@ -20,18 +20,22 @@ import bpy from bpy.types import Operator from bpy.props import FloatProperty, IntProperty, BoolProperty from bpy_extras.object_utils import AddObjectHelper, object_data_add -from mathutils import Vector import bmesh from bmesh.types import BMVert +import ifcopenshell +import blenderbim +import blenderbim.tool as tool +from blenderbim.bim.module.model.prop import BIMStairProperties + + from mathutils import Vector from pprint import pprint +import json def add_dumb_stair_object(self, context): - if self.number_of_treads <= 0: - self.number_of_treads = 1 verts = [ Vector((0, 0, 0)), Vector((0, self.tread_length, 0)), @@ -69,7 +73,7 @@ class BIM_OT_add_object(Operator, AddObjectHelper): width: FloatProperty(name="Width", default=1.1) height: FloatProperty(name="Height", default=1) tread_depth: FloatProperty(name="Tread Depth", default=0.2) - number_of_treads: IntProperty(name="Number of Treads (Goings)", default=6) + number_of_treads: IntProperty(name="Number of Treads (Goings)", default=6, soft_min=1) tread_length: FloatProperty(name="Tread Length (Going)", default=0.25) riser_height: FloatProperty(name="*Calculated* Riser Height") length: FloatProperty(name="*Calculated* Length") @@ -79,30 +83,25 @@ class BIM_OT_add_object(Operator, AddObjectHelper): return {"FINISHED"} -def create_stair( - operator, - context, - width=1.2, - height=1.0, - number_of_treads=6, - tread_depth=0.25, - tread_run=0.3, - # tread_rise = None, # 0.167 - base_slab_depth=0.25, - top_slab_depth=0.25, - has_top_nib=False, +def generate_stair_2d_profile( + number_of_treads, + height, + width, + tread_run, + tread_depth, + has_top_nib, + top_slab_depth, + base_slab_depth, stair_type="CONCRETE", - extrude=True, ): - vertices = [] edges = [] - # 2d profile generation - tread_rise = height / number_of_treads - length = tread_run * number_of_treads + number_of_risers = number_of_treads + 1 + tread_rise = height / number_of_risers + length = tread_run * number_of_risers - for i in range(number_of_treads): + for i in range(number_of_risers): vertices.extend([ Vector((tread_run*i, 0, tread_rise*i)), Vector((tread_run*i, 0, tread_rise*(i+1))) @@ -112,8 +111,8 @@ def create_stair( edges.append((cur_vertex - 1, cur_vertex)) edges.append((cur_vertex, cur_vertex + 1)) - vertices.append(Vector((tread_run * number_of_treads, 0, tread_rise * number_of_treads))) - edges.append((number_of_treads * 2, number_of_treads * 2 - 1)) + vertices.append(Vector((tread_run * number_of_risers, 0, tread_rise * number_of_risers))) + edges.append((number_of_risers * 2, number_of_risers * 2 - 1)) td_vector = Vector((vertices[2][2], 0, -vertices[2][0])).normalized() * tread_depth @@ -127,15 +126,15 @@ def create_stair( # top nib if has_top_nib: - vertices.append( vertices[number_of_treads * 2] + Vector((0, 0, -top_slab_depth)) ) - vertices.append( vertices[number_of_treads * 2] + Vector(((-top_slab_depth - b) / k, 0, -top_slab_depth)) ) + vertices.append( vertices[number_of_risers * 2] + Vector((0, 0, -top_slab_depth)) ) + vertices.append( vertices[number_of_risers * 2] + Vector(((-top_slab_depth - b) / k, 0, -top_slab_depth)) ) last_vertex_i = len(vertices) - 1 - edges.append( (number_of_treads * 2, last_vertex_i - 1) ) + edges.append( (number_of_risers * 2, last_vertex_i - 1) ) edges.append( (last_vertex_i - 1, last_vertex_i) ) else: - vertices.append(vertices[number_of_treads * 2] + depth_vector) + vertices.append(vertices[number_of_risers * 2] + depth_vector) last_vertex_i = len(vertices) - 1 - edges.append((number_of_treads * 2, last_vertex_i)) + edges.append((number_of_risers * 2, last_vertex_i)) top_nib_end = len(vertices) - 1 @@ -154,7 +153,17 @@ def create_stair( edges.append( (bottom_nib_end, top_nib_end) ) faces = [list(range(len(vertices)))] - # profile generation finished + + return (vertices, edges, faces) + + +def update_stair_modifier(context): + obj = context.active_object + props = obj.BIMStairProperties + + props_kwargs = props.get_props_kwargs() + props_kwargs["stair_type"] = "CONCRETE" + vertices, edges, faces = generate_stair_2d_profile(**props_kwargs) obj = context.object bm = bmesh.new() @@ -168,49 +177,19 @@ def create_stair( bmesh.ops.contextual_create(bm, geom=new_edges) - if extrude: - bm.faces.ensure_lookup_table() - faces = [bm.faces[0]] - extruded = bmesh.ops.extrude_face_region(bm, geom=faces) - extrusion_vector = Vector((0, 1, 0)) * width - translate_verts = [v for v in extruded["geom"] if isinstance(v, BMVert)] - bmesh.ops.translate(bm, vec=extrusion_vector, verts=translate_verts) - - # set origin - prev_cursor_location = bpy.context.scene.cursor.location.copy() - bpy.context.scene.cursor.location = vertices[0] - bpy.ops.object.origin_set(type="ORIGIN_CURSOR", center="MEDIAN") - bpy.context.scene.cursor.location = prev_cursor_location + bm.faces.ensure_lookup_table() + faces = [bm.faces[0]] + extruded = bmesh.ops.extrude_face_region(bm, geom=faces) + extrusion_vector = Vector((0, 1, 0)) * props.width + translate_verts = [v for v in extruded["geom"] if isinstance(v, BMVert)] + bmesh.ops.translate(bm, vec=extrusion_vector, verts=translate_verts) if context.object.mode == "EDIT": bmesh.update_edit_mesh(obj.data) else: bm.to_mesh(obj.data) bm.free() - - return (obj, tread_rise, length) - - -def add_object_stair(self, context): - if context.object is None: - self.report({"ERROR"}, "Select object that will be transformed to a stair.") - return - - mesh, tread_rise, length = create_stair( - operator=self, - context=context, - stair_type="CONCRETE", - width=self.width, - height=self.height, - number_of_treads=self.number_of_treads, - tread_depth=self.tread_depth, - tread_run=self.tread_run, - base_slab_depth=self.base_slab_depth, - top_slab_depth=self.top_slab_depth, - has_top_nib=self.has_top_nib, - ) - self.tread_rise = tread_rise - self.length = length + obj.data.update() class BIM_OT_add_clever_stair(Operator): @@ -218,20 +197,164 @@ class BIM_OT_add_clever_stair(Operator): bl_label = "Clever Stair" bl_options = {"REGISTER", "UNDO"} - width: FloatProperty(name="Width", default=1.2, soft_min=0.01) - height: FloatProperty(name="Height", default=1.0, soft_min=0.01) - number_of_treads: IntProperty(name="Number of Treads (Goings)", default=6, soft_min=1) - tread_depth: FloatProperty(name="Tread Depth", default=0.25, soft_min=0.01) - tread_run: FloatProperty(name="Tread Run", default=0.3, soft_min=0.01) - base_slab_depth: FloatProperty(name="Base slab depth", default=0.25, soft_min=0) - top_slab_depth: FloatProperty(name="Top slab depth", default=0.25, soft_min=0) - has_top_nib: BoolProperty(name="Has top nib", default=True) - - tread_rise: FloatProperty(name="*Calculated* Tread Rise") - length: FloatProperty(name="*Calculated* Length") - def execute(self, context): - add_object_stair(self, context) + ifcfile = tool.Ifc.get() + if not ifcfile: + self.report({"ERROR"}, "You need to start IFC project first to create a stair.") + return {"CANCELLED"} + + if context.object is not None: + spawn_location = context.object.location.copy() + context.object.select_set(False) + else: + spawn_location = bpy.context.scene.cursor.location.copy() + + mesh = bpy.data.meshes.new("StairFlight") + obj = bpy.data.objects.new("StairFlight", mesh) + bpy.context.scene.collection.objects.link(obj) + obj.location = spawn_location + body_context = ifcopenshell.util.representation.get_context(ifcfile, "Model", "Body", "MODEL_VIEW") + element = blenderbim.core.root.assign_class( + tool.Ifc, + tool.Collector, + tool.Root, + obj=obj, + ifc_class="IfcStairFlight", + should_add_representation=True, + context=body_context, + ) + bpy.context.view_layer.objects.active = None + bpy.context.view_layer.objects.active = obj + obj.select_set(True) + bpy.ops.bim.add_stair() + return {"FINISHED"} + + +# UI operators +class AddStair(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.add_stair" + bl_label = "Add Stair" + bl_options = {"REGISTER"} + + def _execute(self, context): + obj = context.active_object + element = tool.Ifc.get_entity(obj) + + if not element.is_a("IfcStairFlight"): + self.report({"ERROR"}, "Object has to be IfcStairFlight type to add a stair.") + return {"CANCELLED"} + + props = obj.BIMStairProperties + + stair_data = props.get_props_kwargs() + psets = ifcopenshell.util.element.get_psets(element) + pset = psets.get("BBIM_Stair", None) + + if pset: + pset = tool.Ifc.get().by_id(pset["id"]) + else: + pset = ifcopenshell.api.run("pset.add_pset", tool.Ifc.get(), product=element, name="BBIM_Stair") + + ifcopenshell.api.run( + "pset.edit_pset", + tool.Ifc.get(), + pset=pset, + properties={"Data": json.dumps(stair_data)}, + ) + update_stair_modifier(context) + + # update IfcStairFlight properties + element.NumberOfRisers = props.number_of_treads + 1 + element.NumberOfTreads = props.number_of_treads + element.RiserHeight = props.height / element.NumberOfRisers + element.TreadLength = props.tread_depth + return {"FINISHED"} + + +class CancelEditingStair(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.cancel_editing_stair" + bl_label = "Cancel editing Stair" + bl_options = {"REGISTER"} + + def _execute(self, context): + obj = context.active_object + element = tool.Ifc.get_entity(obj) + psets = ifcopenshell.util.element.get_psets(element) + data = json.loads(psets["BBIM_Stair"]["Data"]) + props = obj.BIMStairProperties + # restore previous settings since editing was canceled + for prop_name in data: + setattr(props, prop_name, data[prop_name]) + update_stair_modifier(context) + + props.is_editing = -1 + + return {"FINISHED"} + + +class FinishEditingStair(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.finish_editing_stair" + bl_label = "Finish editing stair" + bl_options = {"REGISTER"} + + def _execute(self, context): + obj = context.active_object + element = tool.Ifc.get_entity(obj) + props = obj.BIMStairProperties + + psets = ifcopenshell.util.element.get_psets(element) + pset = psets["BBIM_Stair"] + data = props.get_props_kwargs() + + props.is_editing = -1 + + update_stair_modifier(context) + + pset = tool.Ifc.get().by_id(pset["id"]) + data = json.dumps(data) + ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties={"Data": data}) + + # update IfcStairFlight properties + if element.is_a("IfcStairFlight"): + element.NumberOfRisers = props.number_of_treads + 1 + element.NumberOfTreads = props.number_of_treads + element.RiserHeight = props.height / element.NumberOfRisers + element.TreadLength = props.tread_depth + return {"FINISHED"} + + +class EnableEditingStair(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.enable_editing_stair" + bl_label = "Enable Editing Stair" + bl_options = {"REGISTER"} + + def _execute(self, context): + obj = context.active_object + props = obj.BIMStairProperties + element = tool.Ifc.get_entity(obj) + pset = ifcopenshell.util.element.get_psets(element) + data = json.loads(pset["BBIM_Stair"]["Data"]) + # required since we could load pset from .ifc and BIMStairProperties won't be set + for prop_name in data: + setattr(props, prop_name, data[prop_name]) + + props.is_editing = 1 + + +class RemoveStair(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.remove_stair" + bl_label = "Remove Stair" + bl_options = {"REGISTER"} + + def _execute(self, context): + obj = context.active_object + element = tool.Ifc.get_entity(obj) + obj.BIMStairProperties.is_editing = -1 + + pset = ifcopenshell.util.element.get_psets(element) + pset = tool.Ifc.get().by_id(pset["BBIM_Stair"]["id"]) + ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset) + return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/model/ui.py b/src/blenderbim/blenderbim/bim/module/model/ui.py index d62bf9ee4f..139f703476 100644 --- a/src/blenderbim/blenderbim/bim/module/model/ui.py +++ b/src/blenderbim/blenderbim/bim/module/model/ui.py @@ -19,8 +19,10 @@ import bpy import blenderbim.tool as tool from bpy.types import Panel, Operator, Menu -from blenderbim.bim.module.model.data import AuthoringData, ArrayData +from blenderbim.bim.module.model.data import AuthoringData, ArrayData, StairData from blenderbim.bim.module.model.prop import store_cursor_position +from blenderbim.bim.module.model.stair import update_stair_modifier + from blenderbim.bim.helper import prop_with_search @@ -291,6 +293,7 @@ class BIM_PT_array(bpy.types.Panel): row.prop(props, "x") row.prop(props, "y") row.prop(props, "z") + else: row = box.row(align=True) row.label(text=f"{array['count']} Items", icon="MOD_ARRAY") @@ -306,6 +309,60 @@ class BIM_PT_array(bpy.types.Panel): row.operator("bim.add_array", icon="ADD", text="") +class BIM_PT_stair(bpy.types.Panel): + bl_label = "IFC Stair" + bl_idname = "BIM_PT_stair" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "modifier" + + @classmethod + def poll(cls, context): + # always display modifier if it's IFC object + return tool.Ifc.get() and tool.Ifc.get_entity(context.active_object) + + def draw(self, context): + if not StairData.is_loaded: + StairData.load() + + props = context.active_object.BIMStairProperties + + if StairData.data["parameters"]: + row = self.layout.row(align=True) + row.label(text="Stair", icon="IPO_CONSTANT") + + stair_data = StairData.data["parameters"]["data"] + box = self.layout.box() + if props.is_editing != -1: + row = box.row(align=True) + row.operator("bim.finish_editing_stair", icon="CHECKMARK", text="Finish editing") + row.operator("bim.cancel_editing_stair", icon="CANCEL", text="") + row = box.row(align=True) + for prop_name in props.get_props_kwargs(): + box.prop(props, prop_name) + update_stair_modifier(context) + else: + row = box.row(align=True) + row.label(text=f"Parameters:", icon="MOD_ARRAY") + row.operator("bim.enable_editing_stair", icon="GREASEPENCIL", text="") + row.operator("bim.remove_stair", icon="X", text="") + row = box.row(align=True) + for prop in props.get_props_kwargs(): + prop_value = stair_data[prop] + prop_value = round(prop_value, 5) if type(prop_value) is float else prop_value + box.label(text=f"{props.bl_rna.properties[prop].name}: {prop_value}") + + # calculated properties + number_of_rises = props.number_of_treads + 1 + box.label(text=f"Number of risers: {number_of_rises}") + box.label(text=f"Tread rise: {round(props.height / number_of_rises, 5)}") + box.label(text=f"Length: {round(props.tread_run * number_of_rises, 5)}") + else: + row = self.layout.row() + row.label(text="No Stair Found") + row.operator("bim.add_stair", icon="ADD", text="") + + class BIM_MT_model(Menu): bl_idname = "BIM_MT_model" bl_label = "IFC Objects"