Files
IfcOpenShell/src/blenderbim/blenderbim/bim/module/model/railing.py
T

540 lines
19 KiB
Python

# 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
import bmesh
import ifcopenshell
from ifcopenshell.util.shape_builder import V
import blenderbim
import blenderbim.tool as tool
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 blenderbim.bim.module.model.decorator import ProfileDecorator
from mathutils import Vector
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):
"""should be called after new geometry settled
since it's going to update ifc representation
"""
obj = context.active_object
props = obj.BIMRailingProperties
element = tool.Ifc.get_entity(obj)
ifc_file = tool.Ifc.get()
# type attributes
element.PredefinedType = "USERDEFINED"
# occurrences attributes
occurrences = tool.Ifc.get_all_element_occurrences(element)
for occurrence in occurrences:
occurrence.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,
},
)
if props.railing_type == "WALL_MOUNTED_HANDRAIL":
body = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
pset_data = tool.Model.get_modeling_bbim_pset_data(bpy.context.active_object, "BBIM_Railing")
path_data = pset_data["data_dict"]["path_data"]
railing_path = [Vector(v) for v in path_data["verts"]]
looped_path = path_data["edges"][-1][-1] == path_data["edges"][0][0]
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
representation_data = {
"railing_type": props.railing_type,
"context": body,
"railing_path": railing_path,
"use_manual_supports": props.use_manual_supports,
"support_spacing": props.support_spacing / si_conversion,
"railing_diameter": props.railing_diameter / si_conversion,
"clear_width": props.clear_width / si_conversion,
"terminal_type": props.terminal_type,
"height": props.height / si_conversion,
"looped_path": looped_path,
}
model_representation = ifcopenshell.api.run(
"geometry.add_railing_representation", ifc_file, **representation_data
)
tool.Model.replace_object_ifc_representation(body, obj, model_representation)
# hacky way to ensure tha ifc representation won't get tessellated at project save
IfcStore.edited_objs.discard(obj)
elif props.railing_type == "FRAMELESS_PANEL":
tool.Ifc.edit(obj)
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 = tool.Ifc.get().createIfcText(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):
"""before using should make sure that Data contains up-to-date information.
If BBIM Pset just changed should call refresh() before updating bmesh
"""
obj = context.active_object
props = obj.BIMRailingProperties
# NOTE: using Data since bmesh update will hapen very often
if not RailingData.is_loaded:
RailingData.load()
path_data = RailingData.data["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
if props.railing_type != "FRAMELESS_PANEL":
return
def generate_frameless_panel_railing():
# generating FRAMELESS_PANEL railing
height = props.height
thickness = props.thickness
spacing = props.spacing
# 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"]
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)
bmesh.ops.recalc_face_normals(bm, faces=bm.faces[:])
tool.Blender.apply_bmesh(obj.data, bm)
generate_frameless_panel_railing()
def get_path_data(obj):
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
bm = tool.Blender.get_bmesh_for_mesh(obj.data)
if not bm.verts or not bm.edges:
return
end_points = [v for v in bm.verts if len(v.link_edges) == 1]
looped = not end_points
# TODO: check with previous data
# if we have some previous data then we try to match
# start or end of the path with the previous path
previous_data = False
if previous_data:
previous_start = previous_data[0]
previous_end = previous_data[-1]
potential_start = min([(v, (v.co - previous_start).length) for v in end_points], key=lambda v_data: v_data[1])
potential_end = min([(v, (v.co - previous_end).length) for v in end_points], key=lambda v_data: v_data[1])
if potential_start[1] < potential_end[1]:
start_point = potential_start[0]
else:
start_point = next(v for v in end_points if v != potential_start[0])
elif not looped:
start_point = min(end_points, key=lambda v: v.index)
elif looped:
start_point = bm.verts[:][0]
# walking through the path
# to make sure all verts are in consequent order
edge = start_point.link_edges[0]
v = edge.other_vert(start_point)
points = [start_point.co, v.co]
segments = [(0, 1)]
i = 2
other_edge = lambda edges, edge: next(e for e in edges if e != edge)
while len(link_edges := v.link_edges) != 1:
prev_v = v
edge = other_edge(link_edges, edge)
v = edge.other_vert(prev_v)
if looped and v == start_point:
segments.append((i - 1, 0))
break
# skip path verts if they just go vertical to avoid errors
if (v.co.xy - prev_v.co.xy).length <= 0.0001:
continue
points.append(v.co)
segments.append((i - 1, i))
i += 1
path_data = {"edges": segments, "verts": [p / si_conversion for p in points]}
return path_data
class BIM_OT_add_railing(bpy.types.Operator, tool.Ifc.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.active_object is not None:
spawn_location = context.active_object.location.copy()
context.active_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
collection = context.view_layer.active_layer_collection.collection
collection.objects.link(obj)
body_context = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
blenderbim.core.root.assign_class(
tool.Ifc,
tool.Collector,
tool.Root,
obj=obj,
ifc_class="IfcRailing",
should_add_representation=True,
context=body_context,
)
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
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
railing_data = props.get_general_kwargs(convert_to_project_units=True)
path_data = get_path_data(obj)
# NOTE: will occur only on meshes without edges or verts
if not path_data:
path_data = {
"edges": [[0, 1], [1, 2]],
"verts": [
Vector([-1.0, 0.0, 0.0]) / si_conversion,
Vector([0.0, 0.0, 0.0]) / si_conversion,
Vector([1.0, 0.0, 0.0]) / si_conversion,
],
}
railing_data["path_data"] = path_data
update_bbim_railing_pset(element, railing_data)
refresh()
update_railing_modifier_bmesh(context)
update_railing_modifier_ifc_data(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
data = tool.Model.get_modeling_bbim_pset_data(obj, "BBIM_Railing")["data_dict"]
data["path_data"] = json.dumps(data["path_data"])
# required since we could load pset from .ifc and BIMRailingProperties won't be set
props.set_props_kwargs_from_ifc_data(data)
props.is_editing = True
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
data = tool.Model.get_modeling_bbim_pset_data(obj, "BBIM_Railing")["data_dict"]
props = obj.BIMRailingProperties
# restore previous settings since editing was canceled
props.set_props_kwargs_from_ifc_data(data)
update_railing_modifier_bmesh(context)
props.is_editing = False
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
pset_data = tool.Model.get_modeling_bbim_pset_data(bpy.context.active_object, "BBIM_Railing")
path_data = pset_data["data_dict"]["path_data"]
railing_data = props.get_general_kwargs(convert_to_project_units=True)
railing_data["path_data"] = path_data
props.is_editing = False
update_bbim_railing_pset(element, railing_data)
update_railing_modifier_ifc_data(context)
return {"FINISHED"}
class FlipRailingPathOrder(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.flip_railing_path_order"
bl_label = "Flip Railing Path Order"
bl_description = "Can be useful to maintain railing supports direction"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
obj = context.active_object
element = tool.Ifc.get_entity(obj)
props = obj.BIMRailingProperties
pset_data = tool.Model.get_modeling_bbim_pset_data(bpy.context.active_object, "BBIM_Railing")
path_data = pset_data["data_dict"]["path_data"]
# flip the vertex order and edges
path_data["verts"] = path_data["verts"][::-1]
last_vert_i = len(path_data["verts"]) - 1
edges = []
for edge in path_data["edges"][::-1]:
edge = [abs(vi - last_vert_i) for vi in edge[::-1]]
edges.append(edge)
railing_data = props.get_general_kwargs(convert_to_project_units=True)
railing_data["path_data"] = path_data
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
data = tool.Model.get_modeling_bbim_pset_data(obj, "BBIM_Railing")["data_dict"]
# required since we could load pset from .ifc and BIMRoofProperties won't be set
props.set_props_kwargs_from_ifc_data(data)
props.is_editing_path = True
update_railing_modifier_bmesh(context)
if bpy.context.active_object.mode != "EDIT":
bpy.ops.object.mode_set(mode="EDIT")
tool.Blender.set_viewport_tool("bim.cad_tool")
ProfileDecorator.install(context, exit_edit_mode_callback=lambda: cancel_editing_railing_path(context))
return {"FINISHED"}
def cancel_editing_railing_path(context):
obj = context.active_object
props = obj.BIMRailingProperties
ProfileDecorator.uninstall()
props.is_editing_path = False
if bpy.context.active_object.mode == "EDIT":
bpy.ops.object.mode_set(mode="OBJECT")
if props.railing_type == "FRAMELESS_PANEL":
update_railing_modifier_bmesh(context)
else:
element = tool.Ifc.get_entity(obj)
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,
)
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):
return cancel_editing_railing_path(context)
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(convert_to_project_units=True)
path_data = get_path_data(obj)
railing_data["path_data"] = path_data
ProfileDecorator.uninstall()
props.is_editing_path = False
update_bbim_railing_pset(element, railing_data)
# RailingData has to be updated before run update_railing_modifier_bmesh
# since we know that BBIM_Railing could have changed
refresh()
update_railing_modifier_bmesh(context)
if bpy.context.active_object.mode == "EDIT":
bpy.ops.object.mode_set(mode="OBJECT")
update_railing_modifier_ifc_data(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
element = tool.Ifc.get_entity(obj)
obj.BIMRailingProperties.is_editing = False
pset = tool.Pset.get_element_pset(element, "BBIM_Railing")
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=element, pset=pset)
return {"FINISHED"}