WALL_MOUNTED_HANDRAIL type for Railing modifier

Added WALL_MOUNTED_HANDRAIL type for railing modifier. It's still a bit work in progress - still need to add support for different railing termination types.

Here's the short demonstration - https://user-images.githubusercontent.com/9417531/234582621-6c6f948b-9cdc-4be6-b380-17ddbf5803bf.mp4
This commit is contained in:
Andrej730
2023-04-24 10:57:56 +05:00
parent af1d14a0c1
commit 1dcf74eb60
7 changed files with 510 additions and 100 deletions
@@ -148,6 +148,7 @@ classes = (
railing.AddRailing,
railing.CancelEditingRailing,
railing.FinishEditingRailing,
railing.FlipRailingPathOrder,
railing.EnableEditingRailing,
railing.CancelEditingRailingPath,
railing.FinishEditingRailingPath,
@@ -494,7 +494,18 @@ class BIMDoorProperties(PropertyGroup):
class BIMRailingProperties(PropertyGroup):
railing_types = (("FRAMELESS_PANEL", "FRAMELESS_PANEL", ""),)
railing_types = (
("FRAMELESS_PANEL", "FRAMELESS_PANEL", ""),
("WALL_MOUNTED_HANDRAIL", "WALL_MOUNTED_HANDRAIL", ""),
)
cap_types = (
("none", "none", ""),
("180", "180", ""),
("to_wall", "to_wall", ""),
("to_floor", "to_floor", ""),
("to_end_post", "to_end_post", ""),
("to_end_post_and_floor", "to_end_post_and_floor", ""),
)
railing_added_previously: bpy.props.BoolProperty(default=False)
is_editing: bpy.props.IntProperty(default=-1)
@@ -505,13 +516,42 @@ class BIMRailingProperties(PropertyGroup):
thickness: bpy.props.FloatProperty(name="Thickness", default=0.050)
spacing: bpy.props.FloatProperty(name="Spacing", default=0.050)
# wall mounted handrail specific properties
use_manual_supports: bpy.props.BoolProperty(
name="Use Manual Supports",
default=False,
description="If enabled, supports are added on every vertex on the edges of the railing path.\n"
"If disabled, supports are added automatically based on the support spacing",
)
support_spacing: bpy.props.FloatProperty(
name="Support Spacing", default=1.0, description="Distance between supports if automatic supports are used"
)
railing_diameter: bpy.props.FloatProperty(name="Railing Diameter", default=0.050)
clear_width: bpy.props.FloatProperty(
name="Clear Width", default=0.040, description="Clear width between the railing and the wall"
)
terminal_type: bpy.props.EnumProperty(name="Terminal Type", items=cap_types, default="180")
def get_general_kwargs(self):
return {
base_kwargs = {
"railing_type": self.railing_type,
"height": self.height,
"thickness": self.thickness,
"spacing": self.spacing,
}
additional_kwargs = {}
if self.railing_type == "FRAMELESS_PANEL":
additional_kwargs = {
"thickness": self.thickness,
"spacing": self.spacing,
}
elif self.railing_type == "WALL_MOUNTED_HANDRAIL":
additional_kwargs = {
"railing_diameter": self.railing_diameter,
"clear_width": self.clear_width,
"use_manual_supports": self.use_manual_supports,
"support_spacing": self.support_spacing,
"terminal_type": self.terminal_type,
}
return base_kwargs | additional_kwargs
class BIMRoofProperties(PropertyGroup):
@@ -18,14 +18,11 @@
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
@@ -41,6 +38,15 @@ import json
# https://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcRailingType.htm
NON_SI_RAILING_PROPS = (
"is_editing",
"railing_type",
"railing_added_previously",
"use_manual_supports",
"terminal_type",
)
def bm_split_edge_at_offset(edge, offset):
v0, v1 = edge.verts
@@ -82,7 +88,31 @@ def update_railing_modifier_ifc_data(context):
"Height": props.height,
},
)
tool.Ifc.edit(obj)
if props.railing_type == "WALL_MOUNTED_HANDRAIL":
body = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
railing_path = [Vector(v) for v in RailingData.data["parameters"]["data_dict"]["path_data"]["verts"]]
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,
"railing_diameter": props.railing_diameter,
"clear_width": props.clear_width,
"terminal_type": props.terminal_type,
"height": props.height,
}
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):
@@ -118,77 +148,114 @@ def update_railing_modifier_bmesh(context):
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
if props.railing_type != "FRAMELESS_PANEL":
return
# 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)
def generate_frameless_panel_railing():
# generating FRAMELESS_PANEL railing
height = props.height * si_conversion
thickness = props.thickness * si_conversion
spacing = props.spacing * si_conversion
# 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])
# 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)
edge_dir = ((v1.co - v0.co) * V(1, 1, 0)).normalized()
ortho_vector = edge_dir.cross(V(0, 0, 1))
# 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])
extruded_geom = bmesh.ops.extrude_edge_only(bm, edges=[main_edge])["geom"]
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"]
bmesh.ops.translate(bm, vec=ortho_vector * (-thickness / 2), verts=extruded_verts)
extrusion_vector = Vector((0, 0, 1)) * height
bmesh.ops.translate(bm, vec=extrusion_vector, 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 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)
# dissolve middle edge
bmesh.ops.dissolve_edges(bm, edges=[main_edge])
# to remove unnecessary verts in 0 spacing case
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001)
# 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)
tool.Blender.apply_bmesh(obj.data, bm)
# 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)
generate_frameless_panel_railing()
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"]:
bm = tool.Blender.get_bmesh_for_mesh(obj.data)
end_points = [v for v in bm.verts if len(v.link_edges) == 1]
if not end_points:
return None
# 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])
else:
start_point = min(end_points, key=lambda v: v.index)
# walking through the path
# to make sure all verts and 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:
link_edges = v.link_edges
edge = other_edge(link_edges, edge)
v = edge.other_vert(v)
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
@@ -248,8 +315,7 @@ class AddRailing(bpy.types.Operator, tool.Ifc.Operator):
# 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")
convert_property_group_from_si(props, skip_props=skip_props)
convert_property_group_from_si(props, skip_props=NON_SI_RAILING_PROPS)
railing_data = props.get_general_kwargs()
path_data = get_path_data(obj)
@@ -289,8 +355,7 @@ class EnableEditingRailing(bpy.types.Operator, tool.Ifc.Operator):
# 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())
skip_props = NON_SI_RAILING_PROPS + tuple(data.keys())
convert_property_group_from_si(props, skip_props=skip_props)
props.is_editing = 1
@@ -349,6 +414,37 @@ class FinishEditingRailing(bpy.types.Operator, tool.Ifc.Operator):
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
if not RailingData.is_loaded:
RailingData.load()
path_data = RailingData.data["parameters"]["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()
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"
@@ -543,6 +543,9 @@ class BIM_PT_railing(bpy.types.Panel):
else:
row.operator("bim.enable_editing_railing", icon="GREASEPENCIL", text="")
row.operator("bim.enable_editing_railing_path", icon="ANIM", text="")
# TODO: good for preview but probably should move to .is_editing == -1
# since it's writing to ifc
row.operator("bim.flip_railing_path_order", icon="ARROW_LEFTRIGHT", text="")
row.operator("bim.remove_railing", icon="X", text="")
box = self.layout.box()
@@ -611,7 +614,7 @@ class BIM_PT_roof(bpy.types.Panel):
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}")
if prop == 'angle':
if prop == "angle":
prop_value = round(degrees(prop_value), 2)
row.label(text=str(prop_value))
else:
@@ -202,6 +202,15 @@ class BimToolUI:
add_layout_hotkey_operator(cls.layout, "Regen", "S_G", bpy.ops.bim.recalculate_profile.__doc__)
row.operator("bim.extend_profile", icon="X", text="").join_type = ""
elif (
(RailingData.is_loaded or not RailingData.load())
and RailingData.data["parameters"]
and not context.active_object.BIMRailingProperties.is_editing_path
):
# NOTE: should be above "active_representation_type" = "SweptSolid" check
# because it could be a SweptSolid too
add_layout_hotkey_operator(cls.layout, "Edit Railing Path", "S_E", "")
elif AuthoringData.data["active_representation_type"] == "SweptSolid":
add_layout_hotkey_operator(cls.layout, "Edit Profile", "S_E", "")
@@ -232,13 +241,6 @@ class BimToolUI:
):
add_layout_hotkey_operator(cls.layout, "Extend", "S_E", "")
elif (
(RailingData.is_loaded or not RailingData.load())
and RailingData.data["parameters"]
and not context.active_object.BIMRailingProperties.is_editing_path
):
add_layout_hotkey_operator(cls.layout, "Edit Railing Path", "S_E", "")
elif (
(RoofData.is_loaded or not RoofData.load())
and RoofData.data["parameters"]
@@ -407,6 +409,16 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
if not bpy.context.selected_objects:
return
# NOTE: placing it before the other operations because railing can also be SweptSolid
# and it might conflict with one of the conditions below
if (
(RailingData.is_loaded or not RailingData.load())
and RailingData.data["parameters"]
and not bpy.context.active_object.BIMRailingProperties.is_editing_path
):
bpy.ops.bim.enable_editing_railing_path()
return
selected_usages = {}
for obj in bpy.context.selected_objects:
element = tool.Ifc.get_entity(obj)
@@ -461,15 +473,6 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
[o.select_set(False) for o in selected_usages.get("LAYER2", [])]
bpy.ops.bim.extend_profile(join_type="T")
elif (
(RailingData.is_loaded or not RailingData.load())
and RailingData.data["parameters"]
and not bpy.context.active_object.BIMRailingProperties.is_editing_path
):
# undo the unselection done above because railing has no usage type 🙃
bpy.context.object.select_set(True)
bpy.ops.bim.enable_editing_railing_path()
elif (
(RoofData.is_loaded or not RoofData.load())
and RoofData.data["parameters"]