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.
This commit is contained in:
Andrej730
2022-11-10 15:57:08 +06:00
parent b3f0b9b37b
commit 277317e6fd
5 changed files with 319 additions and 83 deletions
@@ -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)
@@ -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
@@ -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
@@ -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"}
@@ -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"