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"]
@@ -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)),
}