mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-10 01:41:57 +00:00
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:
@@ -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"]
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
# IfcOpenShell - IFC toolkit and geometry engine
|
||||
# Copyright (C) 2023 @Andrej730
|
||||
#
|
||||
# This file is part of IfcOpenShell.
|
||||
#
|
||||
# IfcOpenShell is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# IfcOpenShell 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 Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import ifcopenshell.util.unit
|
||||
from ifcopenshell.util.shape_builder import ShapeBuilder, V
|
||||
from itertools import chain
|
||||
from mathutils import Vector
|
||||
import collections
|
||||
import mathutils
|
||||
from pprint import pprint
|
||||
from math import pi, cos, sin, tan
|
||||
|
||||
|
||||
def mm(x):
|
||||
"""mm to meters shortcut for readability"""
|
||||
return x / 1000
|
||||
|
||||
|
||||
class Usecase:
|
||||
def __init__(self, file, **settings):
|
||||
"""
|
||||
units in settings expected to be in ifc project units
|
||||
|
||||
`railing_path` is a list of point coordinates for the railing path,
|
||||
coordinates are expected to be at the top of the railing, not at the center
|
||||
|
||||
`railing_path` is expected to be a list of Vector objects
|
||||
"""
|
||||
self.file = file
|
||||
self.settings = {"unit_scale": ifcopenshell.util.unit.calculate_unit_scale(self.file)}
|
||||
self.settings.update(
|
||||
{
|
||||
"context": None, # IfcGeometricRepresentationContext
|
||||
"railing_type": "WALL_MOUNTED_HANDRAIL",
|
||||
"railing_path": self.path_si_to_units([V(0, 0, 1), V(1, 0, 1), V(2, 0, 1)]),
|
||||
"use_manual_supports": False,
|
||||
"support_spacing": self.convert_si_to_unit(mm(1000)),
|
||||
"railing_diameter": self.convert_si_to_unit(mm(50)),
|
||||
"clear_width": self.convert_si_to_unit(mm(40)),
|
||||
"terminal_type": "180",
|
||||
"height": self.convert_si_to_unit(mm(1000)),
|
||||
}
|
||||
)
|
||||
|
||||
for key, value in settings.items():
|
||||
self.settings[key] = value
|
||||
|
||||
if self.settings["railing_type"] != "WALL_MOUNTED_HANDRAIL":
|
||||
raise Exception('Only "WALL_MOUNTED_HANDRAIL" railing_type is supported at the moment.')
|
||||
|
||||
def execute(self):
|
||||
arc_points = []
|
||||
items_3d = []
|
||||
builder = ShapeBuilder(self.file)
|
||||
z_down = V(0, 0, -1)
|
||||
|
||||
# measurements
|
||||
# from settings
|
||||
use_manual_supports = self.settings["use_manual_supports"]
|
||||
railing_radius = self.settings["railing_diameter"] / 2
|
||||
support_spacing = self.settings["support_spacing"]
|
||||
clear_width = self.settings["clear_width"]
|
||||
height = self.settings["height"]
|
||||
cap_type = self.settings["terminal_type"]
|
||||
ifc_context = self.settings["context"]
|
||||
railing_coords = self.settings["railing_path"]
|
||||
railing_coords = [p - z_down * railing_radius for p in railing_coords]
|
||||
|
||||
# constant
|
||||
terminal_radius = self.convert_si_to_unit(mm(150))
|
||||
railing_fillet_radius = self.convert_si_to_unit(mm(100))
|
||||
support_length = clear_width + railing_radius
|
||||
support_radius = self.convert_si_to_unit(mm(10))
|
||||
support_disk_radius = railing_radius
|
||||
support_disk_depth = self.convert_si_to_unit(mm(20))
|
||||
|
||||
# util functions
|
||||
float_is_zero = lambda f: 0.0001 >= f >= -0.0001
|
||||
collinear = lambda d0, d1: float_is_zero(d0.angle(d1))
|
||||
|
||||
def add_support_on_point(point, railing_direction):
|
||||
"""create a support arc and a disk based on the position and direction of the railing"""
|
||||
ortho_dir = (railing_direction.yx * V(1, -1)).to_3d().normalized()
|
||||
arc_center = point + ortho_dir * support_length
|
||||
support_points = [
|
||||
point,
|
||||
arc_center - ortho_dir * support_length * cos(pi / 4) + z_down * support_length * sin(pi / 4),
|
||||
arc_center + z_down * support_length,
|
||||
]
|
||||
polyline = builder.polyline(support_points, closed=False, arc_points=[1])
|
||||
solid = builder.create_swept_disk_solid(polyline, support_radius)
|
||||
|
||||
support_disk_circle = builder.circle(radius=support_disk_radius)
|
||||
support_disk = builder.extrude(
|
||||
support_disk_circle, support_disk_depth, position=support_points[-1], **builder.extrude_by_y_kwargs()
|
||||
)
|
||||
return [solid, support_disk]
|
||||
|
||||
def get_fillet_points(v0, v1, v2, radius):
|
||||
"""get fillet points between edges v0v1 and v1v2"""
|
||||
dir1 = (v0 - v1).normalized()
|
||||
dir2 = (v2 - v1).normalized()
|
||||
edge_angle = dir1.angle(dir2)
|
||||
slide_distance = radius / tan(edge_angle / 2)
|
||||
|
||||
fillet_v1co = v1 + (dir1 * slide_distance)
|
||||
fillet_v2co = v1 + (dir2 * slide_distance)
|
||||
|
||||
normal = mathutils.geometry.normal([v0, v1, v2])
|
||||
center = mathutils.geometry.intersect_line_line(
|
||||
fillet_v1co, fillet_v1co + normal.cross(dir1), fillet_v2co, fillet_v2co + normal.cross(dir2)
|
||||
)[0]
|
||||
|
||||
midpointco = center + ((fillet_v1co.lerp(fillet_v2co, 0.5) - center).normalized() * radius)
|
||||
return fillet_v1co, midpointco, fillet_v2co
|
||||
|
||||
def add_arcs_on_turnings_points(base_points):
|
||||
"""add 3 point fillet arcs on turning points of the railing path"""
|
||||
if len(base_points) < 3:
|
||||
return base_points
|
||||
|
||||
# looking for turning points by checking non-collinear edges
|
||||
output_points = base_points[:1]
|
||||
prev_dir = (base_points[1] - base_points[0]).normalized()
|
||||
i = 1
|
||||
while i < len(base_points) - 1:
|
||||
cur_dir = (base_points[i + 1] - base_points[i]).normalized()
|
||||
|
||||
if collinear(cur_dir, prev_dir):
|
||||
output_points.append(base_points[i])
|
||||
else:
|
||||
fillet_points = get_fillet_points(
|
||||
base_points[i - 1], base_points[i], base_points[i + 1], railing_fillet_radius
|
||||
)
|
||||
output_points.extend(fillet_points)
|
||||
arc_points.append(fillet_points[1])
|
||||
|
||||
prev_dir = cur_dir
|
||||
i = i + 1
|
||||
|
||||
output_points.append(base_points[-1])
|
||||
return output_points
|
||||
|
||||
def create_supports_items(railing_coords, manual_supports=False):
|
||||
"""create supports items based on the railing coordinates"""
|
||||
supports_items = []
|
||||
|
||||
# simplified_coords is a list of points that form non-collinear edges
|
||||
simplified_coords = [railing_coords[0]]
|
||||
prev_dir = (railing_coords[1] - railing_coords[0]).normalized()
|
||||
|
||||
# iterating over each edge of the railing path
|
||||
for i in range(1, len(railing_coords) - 1):
|
||||
cur_dir = (railing_coords[i + 1] - railing_coords[i]).normalized()
|
||||
|
||||
if not collinear(cur_dir, prev_dir):
|
||||
simplified_coords.append(railing_coords[i])
|
||||
prev_dir = cur_dir
|
||||
|
||||
# for manual supports each vertex on the railing path edge
|
||||
# will be a point for a support
|
||||
elif manual_supports:
|
||||
supports_items.extend(add_support_on_point(point=railing_coords[i], railing_direction=cur_dir))
|
||||
|
||||
simplified_coords.append(railing_coords[-1])
|
||||
|
||||
if manual_supports:
|
||||
return supports_items
|
||||
|
||||
# create automatic supports based on the support spacing
|
||||
for i in range(0, len(simplified_coords) - 1):
|
||||
v0, v1 = simplified_coords[i : i + 2]
|
||||
edge = v1 - v0
|
||||
length = edge.length
|
||||
edge_dir = edge.normalized()
|
||||
n_supports, support_offset = divmod(length, support_spacing)
|
||||
n_supports = int(n_supports) + 1
|
||||
support_offset /= 2
|
||||
|
||||
start_position = v0 + support_offset * edge_dir
|
||||
for support_i in range(n_supports):
|
||||
support_position = start_position + support_i * support_spacing * edge_dir
|
||||
supports_items.extend(add_support_on_point(point=support_position, railing_direction=edge))
|
||||
|
||||
return supports_items
|
||||
|
||||
def add_cap(railing_coords, arc_points, start=False):
|
||||
"""add handrail terminal cap"""
|
||||
# TODO: implement more cap types
|
||||
railing_coords_for_cap = railing_coords[::-1] if start else railing_coords
|
||||
|
||||
start = railing_coords_for_cap[-1]
|
||||
cap_dir = (railing_coords_for_cap[-1] - railing_coords_for_cap[-2]).xy.to_3d().normalized()
|
||||
arc_point = start + cap_dir * terminal_radius + terminal_radius * z_down
|
||||
arc_points.append(arc_point)
|
||||
cap_coords = [arc_point, start + terminal_radius * 2 * z_down]
|
||||
|
||||
railing_coords = railing_coords_for_cap + cap_coords
|
||||
|
||||
if start:
|
||||
railing_coords = railing_coords[::-1]
|
||||
return railing_coords, arc_points
|
||||
|
||||
items_3d.extend(create_supports_items(railing_coords, manual_supports=use_manual_supports))
|
||||
railing_coords = add_arcs_on_turnings_points(railing_coords)
|
||||
|
||||
railing_coords, arc_points = add_cap(railing_coords, arc_points, start=True)
|
||||
railing_coords, arc_points = add_cap(railing_coords, arc_points, start=False)
|
||||
|
||||
railing_path = builder.polyline(
|
||||
railing_coords, closed=False, arc_points=[railing_coords.index(p) for p in arc_points]
|
||||
)
|
||||
railing_solid = builder.create_swept_disk_solid(railing_path, railing_radius)
|
||||
items_3d.append(railing_solid)
|
||||
representation = builder.get_representation(ifc_context, items=items_3d)
|
||||
return representation
|
||||
|
||||
def convert_si_to_unit(self, value):
|
||||
return value / self.settings["unit_scale"]
|
||||
|
||||
def path_si_to_units(self, path):
|
||||
"""converts list of vectors from SI to ifc project units"""
|
||||
return [self.convert_si_to_unit(v) for v in path]
|
||||
@@ -35,10 +35,21 @@ class ShapeBuilder:
|
||||
def __init__(self, ifc_file):
|
||||
self.file = ifc_file
|
||||
|
||||
def polyline(self, points, closed=False, position_offset=None):
|
||||
def polyline(self, points, closed=False, position_offset=None, arc_points=[]):
|
||||
# > points - list of points formatted like ( (x0, y0), (x1, y1) )
|
||||
# < IfcIndexedPolyCurve
|
||||
segments = [(i, i + 1) for i in range(1, len(points))]
|
||||
segments = []
|
||||
|
||||
cur_i = 0
|
||||
while cur_i < len(points) - 1:
|
||||
cur_i_ifc = cur_i + 1
|
||||
if cur_i + 1 in arc_points:
|
||||
segments.append((cur_i_ifc, cur_i_ifc + 1, cur_i_ifc + 2))
|
||||
cur_i += 2
|
||||
else:
|
||||
segments.append((cur_i_ifc, cur_i_ifc + 1))
|
||||
cur_i += 1
|
||||
|
||||
if closed:
|
||||
segments.append((len(points), 1))
|
||||
if position_offset:
|
||||
@@ -50,7 +61,12 @@ class ShapeBuilder:
|
||||
elif dimensions == 3:
|
||||
ifc_points = self.file.createIfcCartesianPointList3D(points)
|
||||
|
||||
ifc_segments = [self.file.createIfcLineIndex(segment) for segment in segments]
|
||||
ifc_segments = []
|
||||
for segment in segments:
|
||||
if len(segment) == 2:
|
||||
ifc_segments.append(self.file.createIfcLineIndex(segment))
|
||||
elif len(segment) == 3:
|
||||
ifc_segments.append(self.file.createIfcArcIndex(segment))
|
||||
ifc_curve = self.file.createIfcIndexedPolyCurve(Points=ifc_points, Segments=ifc_segments)
|
||||
return ifc_curve
|
||||
|
||||
@@ -187,28 +203,20 @@ class ShapeBuilder:
|
||||
# < returns IfcArbitraryClosedProfileDef or IfcArbitraryProfileDefWithVoids
|
||||
|
||||
if outer_curve.Dim != 2:
|
||||
# TODO: replace with exception
|
||||
print(
|
||||
f"WARNING. Outer curve for IfcArbitraryClosedProfileDef/IfcIfcArbitraryProfileDefWithVoid should be 2D to be valid, currently it has {outer_curve.Dim} dimensions.\n"
|
||||
raise Exception(
|
||||
f"Outer curve for IfcArbitraryClosedProfileDef/IfcIfcArbitraryProfileDefWithVoid should be 2D to be valid, currently it has {outer_curve.Dim} dimensions.\n"
|
||||
"Ref: https://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcArbitraryClosedProfileDef.htm#8.15.3.1.4-Formal-propositions"
|
||||
)
|
||||
import traceback
|
||||
|
||||
traceback.print_stack()
|
||||
|
||||
if inner_curves:
|
||||
if not isinstance(inner_curves, collections.abc.Iterable):
|
||||
inner_curves = [inner_curves]
|
||||
# TODO: replace with exception
|
||||
if any(curve.Dim != 2 for curve in inner_curves):
|
||||
print(
|
||||
raise Exception(
|
||||
"WARNING. InnerCurve for IfcIfcArbitraryProfileDefWithVoid sould be 2D to be valid, "
|
||||
"currently on one of the inner curves is using different amount of dimensions.\n"
|
||||
"Ref: https://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcArbitraryClosedProfileDef.htm#8.15.3.1.4-Formal-propositions"
|
||||
)
|
||||
import traceback
|
||||
|
||||
traceback.print_stack()
|
||||
|
||||
profile = self.file.createIfcArbitraryProfileDefWithVoids(
|
||||
ProfileName=name, ProfileType=profile_type, OuterCurve=outer_curve, InnerCurves=inner_curves
|
||||
@@ -515,6 +523,17 @@ class ShapeBuilder:
|
||||
)
|
||||
return extruded_area
|
||||
|
||||
def create_swept_disk_solid(self, path_curve, radius):
|
||||
"""Create IfcSweptDiskSolid from `path_curve` (must be 3D) and `radius`"""
|
||||
if path_curve.Dim != 3:
|
||||
raise Exception(
|
||||
f"Path curve for IfcSweptDiskSolid should be 3D to be valid, currently it has {path_curve.Dim} dimensions.\n"
|
||||
"Ref: https://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcSweptDiskSolid.htm#8.8.3.42.4-Formal-propositions"
|
||||
)
|
||||
|
||||
disk_solid = self.file.createIfcSweptDiskSolid(Directrix=path_curve, Radius=radius)
|
||||
return disk_solid
|
||||
|
||||
def get_representation(self, context, items, representation_type=None):
|
||||
# > items - could be a list or single curve/IfcExtrudedAreaSolid
|
||||
# < IfcShapeRepresentation
|
||||
@@ -522,7 +541,7 @@ class ShapeBuilder:
|
||||
items = [items]
|
||||
|
||||
if not representation_type:
|
||||
if items[0].is_a("IfcExtrudedAreaSolid"):
|
||||
if items[0].is_a() in ("IfcExtrudedAreaSolid", "IfcSweptDiskSolid"):
|
||||
representation_type = "SweptSolid"
|
||||
elif items[0].is_a("IfcCurve") and items[0].Dim == 3:
|
||||
representation_type = "Curve3D"
|
||||
@@ -539,3 +558,13 @@ class ShapeBuilder:
|
||||
|
||||
def deep_copy(self, element):
|
||||
return ifcopenshell.util.element.copy_deep(self.file, element)
|
||||
|
||||
# UTILITIES
|
||||
def extrude_by_y_kwargs(self):
|
||||
"""shortcut for `ShapeBuilder.extrude` to extrude by y axis.
|
||||
it assumes you have 2d profile in xz plane and trying to extrude it by y axis"""
|
||||
return {
|
||||
"position_x_axis": Vector((1, 0, 0)),
|
||||
"position_z_axis": Vector((0, -1, 0)),
|
||||
"extrusion_vector": Vector((0, 0, -1)),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user