Ifc Railing modifier

Added Ifc Railing modifier (similarly to stair, window, door modifiers...).
You can start working with it either creating some edge based path and adding this modifier or by creating Railing for Shift+A menu (it will create some simple path to start with).

In railing modifier you can switch two edit modes - editing main railing properties and editing path. Editing path will show only the railing path, you can edit it and save it. You cannot edit the railing pass if you're not in this mode.
This commit is contained in:
Andrej730
2023-03-03 11:23:28 +05:00
parent d1217e61c4
commit b057312ebf
7 changed files with 560 additions and 1 deletions
@@ -36,6 +36,7 @@ from . import (
profile,
sverchok_modifier,
door,
railing
)
classes = (
@@ -99,12 +100,14 @@ classes = (
prop.BIMSverchokProperties,
prop.BIMWindowProperties,
prop.BIMDoorProperties,
prop.BIMRailingProperties,
ui.BIM_PT_authoring,
ui.BIM_PT_array,
ui.BIM_PT_stair,
ui.BIM_PT_sverchok,
ui.BIM_PT_window,
ui.BIM_PT_door,
ui.BIM_PT_railing,
ui.LaunchTypeManager,
ui.HelpConstrTypes,
ui.BIM_MT_model,
@@ -138,6 +141,15 @@ classes = (
door.FinishEditingDoor,
door.EnableEditingDoor,
door.RemoveDoor,
railing.BIM_OT_add_railing,
railing.AddRailing,
railing.CancelEditingRailing,
railing.FinishEditingRailing,
railing.EnableEditingRailing,
railing.CancelEditingRailingPath,
railing.FinishEditingRailingPath,
railing.EnableEditingRailingPath,
railing.RemoveRailing,
)
addon_keymaps = []
@@ -152,10 +164,12 @@ def register():
bpy.types.Object.BIMSverchokProperties = bpy.props.PointerProperty(type=prop.BIMSverchokProperties)
bpy.types.Object.BIMWindowProperties = bpy.props.PointerProperty(type=prop.BIMWindowProperties)
bpy.types.Object.BIMDoorProperties = bpy.props.PointerProperty(type=prop.BIMDoorProperties)
bpy.types.Object.BIMRailingProperties = bpy.props.PointerProperty(type=prop.BIMRailingProperties)
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_mesh_add.append(window.add_object_button)
bpy.types.VIEW3D_MT_mesh_add.append(door.add_object_button)
bpy.types.VIEW3D_MT_mesh_add.append(railing.add_object_button)
bpy.types.VIEW3D_MT_add.append(ui.add_menu)
bpy.app.handlers.load_post.append(handler.load_post)
wm = bpy.context.window_manager
@@ -175,11 +189,13 @@ def unregister():
del bpy.types.Object.BIMSverchokProperties
del bpy.types.Object.BIMWindowProperties
del bpy.types.Object.BIMDoorProperties
del bpy.types.Object.BIMRailingProperties
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)
bpy.types.VIEW3D_MT_mesh_add.remove(window.add_object_button)
bpy.types.VIEW3D_MT_mesh_add.remove(door.add_object_button)
bpy.types.VIEW3D_MT_mesh_add.remove(railing.add_object_button)
bpy.types.VIEW3D_MT_add.remove(ui.add_menu)
wm = bpy.context.window_manager
kc = wm.keyconfigs.addon
@@ -35,6 +35,7 @@ def refresh():
SverchokData.is_loaded = False
WindowData.is_loaded = False
DoorData.is_loaded = False
RailingData.is_loaded = False
class AuthoringData:
@@ -462,3 +463,23 @@ class DoorData:
if parameters:
parameters["data_dict"] = json.loads(parameters.get("Data", "[]") or "[]")
return parameters
class RailingData:
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_Railing", None)
if parameters:
parameters["data_dict"] = json.loads(parameters.get("Data", "[]") or "[]")
return parameters
@@ -542,3 +542,26 @@ class BIMDoorProperties(PropertyGroup):
kwargs["frame_depth"] = self.frame_depth
return kwargs
class BIMRailingProperties(PropertyGroup):
railing_types = (
("FRAMELESS_PANEL", "FRAMELESS_PANEL", ""),
)
railing_added_previously: bpy.props.BoolProperty(default=False)
is_editing: bpy.props.IntProperty(default=-1)
is_editing_path: bpy.props.BoolProperty(default=False)
railing_type: bpy.props.EnumProperty(name="Railing Type", items=railing_types, default="FRAMELESS_PANEL")
height: bpy.props.FloatProperty(name="Height", default=1.0)
thickness: bpy.props.FloatProperty(name="Thickness", default=0.050)
spacing: bpy.props.FloatProperty(name="Spacing", default=0.050)
def get_general_kwargs(self):
return {
"railing_type": self.railing_type,
"height": self.height,
"thickness": self.thickness,
"spacing": self.spacing,
}
@@ -0,0 +1,405 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2023 @Andrej730
#
# This file is part of BlenderBIM Add-on.
#
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# BlenderBIM Add-on is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
from bpy.types import Operator
import bmesh
import ifcopenshell
from ifcopenshell.util.shape_builder import V
import blenderbim
import blenderbim.tool as tool
import blenderbim.core.geometry as core
from blenderbim.bim.helper import convert_property_group_from_si
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.model.door import bm_sort_out_geom
from blenderbim.bim.module.model.data import RailingData, refresh
from mathutils import Vector, Matrix
from pprint import pprint
import json
# reference:
# https://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcRailing.htm
# https://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcRailingType.htm
def bm_split_edge_at_offset(edge, offset):
v0, v1 = edge.verts
offset = offset / 2
edge_len = (v0.co-v1.co).xy.length
split_output_0 = bmesh.utils.edge_split(edge, v0, offset/edge_len)
split_output_1 = bmesh.utils.edge_split(edge, v1, offset/(edge_len-offset))
new_geometry = bm_sort_out_geom(split_output_0 + split_output_1)
return new_geometry
def update_railing_modifier_ifc_data(context):
obj = context.active_object
props = obj.BIMRailingProperties
element = tool.Ifc.get_entity(obj)
ifc_file = tool.Ifc.get()
# type attributes
element.PredefinedType = 'USERDEFINED'
# occurences attributes
occurences = tool.Ifc.get_all_element_occurences(element)
for occurence in occurences:
occurence.ObjectType = props.railing_type
# update pset
pset_common = tool.Pset.get_element_pset(element, "Pset_RailingCommon")
if not pset_common:
pset_common = ifcopenshell.api.run("pset.add_pset", ifc_file, product=element, name="Pset_RailingCommon")
ifcopenshell.api.run(
"pset.edit_pset",
ifc_file,
pset=pset_common,
properties={
"Height": props.height,
},
)
def update_bbim_railing_pset(element, railing_data):
pset = tool.Pset.get_element_pset(element, "BBIM_Railing")
if not pset:
pset = ifcopenshell.api.run("pset.add_pset", tool.Ifc.get(), product=element, name="BBIM_Railing")
railing_data = json.dumps(railing_data, default=list)
ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties={"Data": railing_data})
def update_railing_modifier_bmesh(context):
obj = context.object
props = obj.BIMRailingProperties
if not RailingData.is_loaded:
RailingData.load()
path_data = RailingData.data["parameters"]['data_dict']['path_data']
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
# need to make sure we support edit mode
# since users will probably be in edit mode when they'll be changing railing path
bm = tool.Blender.get_bmesh_for_mesh(obj.data, clean=True)
# generating railing path
bm.verts.index_update()
bm.edges.index_update()
new_verts = [bm.verts.new(Vector(v) * si_conversion) for v in path_data['verts']]
new_edges = [bm.edges.new( (new_verts[e[0]], new_verts[e[1]]) ) for e in path_data['edges']]
bm.verts.index_update()
bm.edges.index_update()
if props.is_editing_path:
tool.Blender.apply_bmesh(obj.data, bm)
return
# generating the entire railing
height = props.height * si_conversion
thickness = props.thickness * si_conversion
spacing = props.spacing * si_conversion
# spacing
# split each edge in 3 segments by 0.5 * spacing by x-y plane
main_edges = bm.edges[:]
for main_edge in main_edges:
bm_split_edge_at_offset(main_edge, spacing)
# thickness
# keep track of translated verts so we won't translate the same
# vert twice
edge_dissolving_verts = []
for main_edge in main_edges:
v0, v1 = main_edge.verts
edge_dissolving_verts.extend([v0, v1])
edge_dir = ( (v1.co-v0.co) * V(1,1,0) ).normalized()
ortho_vector = edge_dir.cross( V(0,0,1) )
extruded_geom = bmesh.ops.extrude_edge_only(bm, edges=[main_edge])['geom']
extruded_verts = bm_sort_out_geom(extruded_geom)['verts']
bmesh.ops.translate(bm, vec=ortho_vector * (-thickness/2), verts=extruded_verts)
extruded_geom = bmesh.ops.extrude_edge_only(bm, edges=[main_edge])['geom']
extruded_verts = bm_sort_out_geom(extruded_geom)['verts']
bmesh.ops.translate(bm, vec=ortho_vector * (thickness/2), verts=extruded_verts)
# dissolve middle edge
bmesh.ops.dissolve_edges(bm, edges=[main_edge])
# height
extruded_geom = bmesh.ops.extrude_face_region(bm, geom=bm.faces)['geom']
extruded_verts = bm_sort_out_geom(extruded_geom)['verts']
extruded_edges = bm_sort_out_geom(extruded_geom)['edges']
extrusion_vector = Vector((0, 0, 1)) * height
bmesh.ops.translate(bm, vec=extrusion_vector, verts=extruded_verts)
# dissolve middle edges
edges_to_dissolve = []
verts_to_dissolve = []
for v in edge_dissolving_verts:
for e in v.link_edges:
other_vert = e.other_vert(v)
if other_vert in extruded_verts:
edges_to_dissolve.append(e)
verts_to_dissolve.append(other_vert)
bmesh.ops.dissolve_edges(bm, edges=edges_to_dissolve)
bmesh.ops.dissolve_verts(bm, verts=verts_to_dissolve)
# to remove unnecessary verts in 0 spacing case
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001)
tool.Blender.apply_bmesh(obj.data, bm)
def get_path_data(obj):
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
if obj.mode == 'EDIT':
# otherwise mesh may not contain all changes
# added in edit mode
obj.update_from_editmode()
mesh = obj.data
path_data = dict()
path_data['edges'] = [e.vertices[:] for e in mesh.edges]
path_data['verts'] = [v.co / si_conversion for v in mesh.vertices]
if not path_data['edges'] or not path_data['verts']:
return None
return path_data
class BIM_OT_add_railing(Operator):
bl_idname = "mesh.add_railing"
bl_label = "Railing"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
ifc_file = tool.Ifc.get()
if not ifc_file:
self.report({"ERROR"}, "You need to start IFC project first to create a railing.")
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("IfcRailing")
obj = bpy.data.objects.new("IfcRailing", mesh)
obj.location = spawn_location
body_context = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
element = blenderbim.core.root.assign_class(
tool.Ifc, tool.Collector, tool.Root, obj=obj, ifc_class="IfcRailing", should_add_representation=False
)
element.PredefinedType = "USERDEFINED"
bm = bmesh.new()
bm.verts.index_update()
bm.edges.index_update()
new_verts = [bm.verts.new(v) for v in [V(-1, 0, 0), V(0, 0, 0), V(1, 0, 0)]]
new_edges = [bm.edges.new( (new_verts[e[0]], new_verts[e[1]]) ) for e in [(0,1), (1,2)]]
bm.verts.index_update()
bm.edges.index_update()
tool.Blender.apply_bmesh(mesh, bm)
bpy.ops.object.select_all(action="DESELECT")
bpy.context.view_layer.objects.active = None
bpy.context.view_layer.objects.active = obj
obj.select_set(True)
bpy.ops.bim.add_railing()
return {"FINISHED"}
# UI operators
class AddRailing(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_railing"
bl_label = "Add Railing"
bl_options = {"REGISTER"}
def _execute(self, context):
obj = context.active_object
element = tool.Ifc.get_entity(obj)
props = obj.BIMRailingProperties
if element.is_a() not in ("IfcRailing", "IfcRailingType"):
self.report({"ERROR"}, "Object has to be IfcRailing/IfcRailingType type to add a railing.")
return {"CANCELLED"}
# need to make sure all default props will have correct units
if not props.railing_added_previously:
skip_props = ("is_editing", "railing_type", "railing_added_previously", "path_data")
convert_property_group_from_si(props, skip_props=skip_props)
railing_data = props.get_general_kwargs()
railing_data['path_data'] = get_path_data(obj)
update_bbim_railing_pset(element, railing_data)
update_railing_modifier_ifc_data(context)
update_railing_modifier_bmesh(context)
return {"FINISHED"}
class EnableEditingRailing(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.enable_editing_railing"
bl_label = "Enable Editing Railing"
bl_options = {"REGISTER"}
def _execute(self, context):
obj = context.active_object
props = obj.BIMRailingProperties
element = tool.Ifc.get_entity(obj)
data = json.loads(ifcopenshell.util.element.get_pset(element, "BBIM_Railing", "Data"))
data['path_data'] = json.dumps(data['path_data'])
# required since we could load pset from .ifc and BIMRailingProperties won't be set
for prop_name in data:
setattr(props, prop_name, data[prop_name])
# need to make sure all props that weren't used before
# will have correct units
skip_props = ("is_editing", "railing_type", "railing_added_previously")
skip_props += tuple(data.keys())
convert_property_group_from_si(props, skip_props=skip_props)
props.is_editing = 1
return {"FINISHED"}
class CancelEditingRailing(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.cancel_editing_railing"
bl_label = "Cancel editing Railing"
bl_options = {"REGISTER"}
def _execute(self, context):
obj = context.active_object
element = tool.Ifc.get_entity(obj)
data = json.loads(ifcopenshell.util.element.get_pset(element, "BBIM_Railing", "Data"))
props = obj.BIMRailingProperties
# restore previous settings since editing was canceled
for prop_name in data:
setattr(props, prop_name, data[prop_name])
body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
blenderbim.core.geometry.switch_representation(
tool.Ifc,
tool.Geometry,
obj=obj,
representation=body,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
props.is_editing = -1
return {"FINISHED"}
class FinishEditingRailing(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.finish_editing_railing"
bl_label = "Finish editing railing"
bl_options = {"REGISTER"}
def _execute(self, context):
obj = context.active_object
element = tool.Ifc.get_entity(obj)
props = obj.BIMRailingProperties
if not RailingData.is_loaded:
RailingData.load()
path_data = RailingData.data["parameters"]['data_dict']['path_data']
railing_data = props.get_general_kwargs()
railing_data['path_data'] = path_data
props.is_editing = -1
update_bbim_railing_pset(element, railing_data)
update_railing_modifier_ifc_data(context)
return {"FINISHED"}
class EnableEditingRailingPath(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.enable_editing_railing_path"
bl_label = "Enable Editing Railing Path"
bl_options = {"REGISTER"}
def _execute(self, context):
obj = context.active_object
props = obj.BIMRailingProperties
element = tool.Ifc.get_entity(obj)
props.is_editing_path = True
update_railing_modifier_bmesh(context)
return {"FINISHED"}
class CancelEditingRailingPath(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.cancel_editing_railing_path"
bl_label = "Cancel Editing Railing Path"
bl_options = {"REGISTER"}
def _execute(self, context):
obj = context.active_object
element = tool.Ifc.get_entity(obj)
props = obj.BIMRailingProperties
update_railing_modifier_bmesh(context)
props.is_editing_path = False
return {"FINISHED"}
class FinishEditingRailingPath(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.finish_editing_railing_path"
bl_label = "Finish Editing Railing Path"
bl_options = {"REGISTER"}
def _execute(self, context):
obj = context.active_object
element = tool.Ifc.get_entity(obj)
props = obj.BIMRailingProperties
railing_data = props.get_general_kwargs()
path_data = get_path_data(obj)
railing_data['path_data'] = path_data
props.is_editing_path = False
update_bbim_railing_pset(element, railing_data)
refresh() # need RailingData to be updated before update_railing_modifier_bmesh
update_railing_modifier_bmesh(context)
return {"FINISHED"}
class RemoveRailing(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.remove_railing"
bl_label = "Remove Railing"
bl_options = {"REGISTER"}
def _execute(self, context):
obj = context.active_object
props = obj.BIMRailingProperties
element = tool.Ifc.get_entity(obj)
obj.BIMRailingProperties.is_editing = -1
pset = tool.Pset.get_element_pset(element, "BBIM_Railing")
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset)
props.railing_added_previously = True
return {"FINISHED"}
def add_object_button(self, context):
self.layout.operator(BIM_OT_add_railing.bl_idname, icon="PLUGIN")
@@ -19,11 +19,12 @@
import bpy
import blenderbim.tool as tool
from bpy.types import Panel, Operator, Menu
from blenderbim.bim.module.model.data import AuthoringData, ArrayData, StairData, SverchokData, WindowData, DoorData
from blenderbim.bim.module.model.data import AuthoringData, ArrayData, StairData, SverchokData, WindowData, DoorData, RailingData
from blenderbim.bim.module.model.prop import get_ifc_class
from blenderbim.bim.module.model.stair import update_stair_modifier
from blenderbim.bim.module.model.window import update_window_modifier_bmesh
from blenderbim.bim.module.model.door import update_door_modifier_bmesh
from blenderbim.bim.module.model.railing import update_railing_modifier_bmesh
from blenderbim.bim.helper import prop_with_search
@@ -529,6 +530,65 @@ class BIM_PT_door(bpy.types.Panel):
row.operator("bim.add_door", icon="ADD", text="")
class BIM_PT_railing(bpy.types.Panel):
bl_label = "IFC Railing"
bl_idname = "BIM_PT_railing"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "modifier"
bl_options = {"DEFAULT_CLOSED"}
@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 RailingData.is_loaded:
RailingData.load()
props = context.active_object.BIMRailingProperties
if RailingData.data["parameters"]:
row = self.layout.row(align=True)
row.label(text="Railing parameters", icon="OUTLINER_OB_LATTICE")
railing_data = RailingData.data["parameters"]["data_dict"]
if props.is_editing != -1:
row = self.layout.row(align=True)
row.operator("bim.finish_editing_railing", icon="CHECKMARK", text="Finish editing")
row.operator("bim.cancel_editing_railing", icon="CANCEL", text="")
general_props = props.get_general_kwargs()
for prop in general_props:
self.layout.prop(props, prop)
update_railing_modifier_bmesh(context)
elif props.is_editing_path:
row.operator("bim.finish_editing_railing_path", icon="CHECKMARK", text="Finish editing path")
row.operator("bim.cancel_editing_railing_path", icon="CANCEL", text="")
else:
row.operator("bim.enable_editing_railing", icon="GREASEPENCIL", text="")
row.operator("bim.enable_editing_railing_path", icon="ANIM", text="")
row.operator("bim.remove_railing", icon="X", text="")
box = self.layout.box()
general_props = props.get_general_kwargs()
for prop in general_props:
prop_value = railing_data[prop]
prop_value = round(prop_value, 5) if type(prop_value) is float else prop_value
row = box.row(align=True)
row.label(text=f"{props.bl_rna.properties[prop].name}")
row.label(text=str(prop_value))
else:
row = self.layout.row()
row.label(text="No Railing Found")
row.operator("bim.add_railing", icon="ADD", text="")
class BIM_MT_model(Menu):
bl_idname = "BIM_MT_model"
bl_label = "IFC Objects"
+3
View File
@@ -76,6 +76,9 @@ def get_name(cls, ifc_class, name): pass
def get_obj_ifc_definition_id(cls, obj, obj_type): pass
def get_selected_objects(cls): pass
def set_active_object(cls, obj): pass
def apply_bmesh(cls, mesh, bm): pass
def get_bmesh_for_mesh(cls, mesh, clean=False): pass
@interface
class Boundary: pass
+31
View File
@@ -92,3 +92,34 @@ class Blender:
return tool.Ifc.get_entity(bpy.context.active_object).is_a(ifc_class)
return False
return False
@classmethod
def apply_bmesh(cls, mesh, bm):
import bmesh
if bpy.context.object and bpy.context.object.mode == "EDIT":
# better to be safe because otherwise mesh won't be updated
# and you won't get any errors
if not bm.is_wrapped or hash(bmesh.from_edit_mesh(mesh)) != hash(bm):
raise Exception(f'{bm} is not edit mesh for {mesh}. '
'For applying bmesh in edit mode bmesh should be acquired with `bmesh.from_edit_mesh(me)`.')
bmesh.update_edit_mesh(mesh)
else:
bm.to_mesh(mesh)
bm.free()
mesh.update()
@classmethod
def get_bmesh_for_mesh(cls, mesh, clean=False):
import bmesh
if bpy.context.object.mode == "EDIT":
bm = bmesh.from_edit_mesh(mesh)
if clean:
bm.clear()
else:
bm = bmesh.new()
if not clean:
bm.from_mesh(mesh)
return bm