mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-09 17:31:45 +00:00
Split railing representation into pure-compute + IFC wrapper
add_railing_representation now factors into two parts: * compute_wall_mounted_handrail_geometry returns a pure-geometry WallMountedHandrailGeometry dataclass (handrail polyline + support list + terminal caps), no IFC mutation. * add_railing_representation wraps that dataclass into an IfcShapeRepresentation as before. Downstream consumers that want the same math without round-tripping through an IFC file (Blender gizmo previews, viewport drafts) now drive compute_X directly. Future add_X_representation work in the geometry API is encouraged to follow the same shape — a sibling compute_X function + thin IFC wrapper. The railing_type parameter is dropped from the signature — only WALL_MOUNTED_HANDRAIL was ever supported, so the kwarg was dead. The Bonsai railing-modifier caller is updated in the same commit to stop passing it; without that update Bonsai's finish_editing_railing_path raises TypeError on the first edit. RailingSupport and WallMountedHandrailGeometry use @dataclass(slots=True) — they're constructed N-per-cap during arc sampling, so the per-instance overhead matters. Public symbols (RailingSupport, TERMINAL_TYPE, WallMountedHandrailGeometry, compute_wall_mounted_handrail_geometry, add_railing_representation) re-exported from ifcopenshell.api.geometry. New test/api/geometry/test_add_railing_representation.py covers the compute/wrap contract. Generated with the assistance of an AI coding tool.
This commit is contained in:
@@ -33,7 +33,20 @@ from .add_door_representation import add_door_representation
|
||||
from .add_footprint_representation import add_footprint_representation
|
||||
from .add_mesh_representation import add_mesh_representation
|
||||
from .add_profile_representation import add_profile_representation
|
||||
from .add_railing_representation import add_railing_representation
|
||||
|
||||
# add_railing_representation is the pilot for a "pure-compute + IFC-wrap" split:
|
||||
# compute_wall_mounted_handrail_geometry returns a dataclass with the raw geometry,
|
||||
# add_railing_representation wraps it into an IfcShapeRepresentation. The split lets
|
||||
# downstream consumers (Blender gizmo previews, etc.) drive the same math without
|
||||
# round-tripping through an IFC file. Future add_X_representation work is encouraged
|
||||
# to follow the same shape — sibling compute_X_geometry function + thin IFC wrapper.
|
||||
from .add_railing_representation import (
|
||||
RailingSupport,
|
||||
TERMINAL_TYPE,
|
||||
WallMountedHandrailGeometry,
|
||||
add_railing_representation,
|
||||
compute_wall_mounted_handrail_geometry,
|
||||
)
|
||||
|
||||
try:
|
||||
from .add_representation import add_representation
|
||||
@@ -72,8 +85,12 @@ __all__ = [
|
||||
"add_door_representation",
|
||||
"add_footprint_representation",
|
||||
"add_mesh_representation",
|
||||
"RailingSupport",
|
||||
"TERMINAL_TYPE",
|
||||
"WallMountedHandrailGeometry",
|
||||
"add_profile_representation",
|
||||
"add_railing_representation",
|
||||
"compute_wall_mounted_handrail_geometry",
|
||||
"add_representation",
|
||||
"add_shape_aspect",
|
||||
"add_slab_representation",
|
||||
|
||||
@@ -16,18 +16,21 @@
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from math import cos, pi, radians, sin, tan
|
||||
from typing import Any, Literal, Optional
|
||||
from typing import Callable, Literal, Optional
|
||||
|
||||
import numpy as np
|
||||
from typing_extensions import assert_never
|
||||
|
||||
import ifcopenshell.util.unit
|
||||
from ifcopenshell.util.shape_builder import (
|
||||
NP_XY,
|
||||
NP_YX,
|
||||
NP_Z,
|
||||
PRECISION,
|
||||
SequenceOfVectors,
|
||||
ShapeBuilder,
|
||||
V,
|
||||
is_x,
|
||||
np_angle,
|
||||
np_angle_signed,
|
||||
np_intersect_line_line,
|
||||
@@ -36,12 +39,7 @@ from ifcopenshell.util.shape_builder import (
|
||||
np_normalized,
|
||||
np_to_3d,
|
||||
)
|
||||
|
||||
|
||||
def mm(x: float) -> float:
|
||||
"""mm to meters shortcut for readability"""
|
||||
return x / 1000
|
||||
|
||||
from ifcopenshell.util.unit import mm_to_m as mm
|
||||
|
||||
TERMINAL_TYPE = Literal[
|
||||
"180",
|
||||
@@ -49,15 +47,524 @@ TERMINAL_TYPE = Literal[
|
||||
"TO_WALL",
|
||||
"TO_FLOOR",
|
||||
"TO_END_POST_AND_FLOOR",
|
||||
"NONE",
|
||||
]
|
||||
|
||||
# Geometric design constants for the WALL_MOUNTED_HANDRAIL railing type (millimetres).
|
||||
TERMINAL_RADIUS_MM = 150
|
||||
HANDRAIL_FILLET_RADIUS_MM = 100
|
||||
SUPPORT_ARC_RADIUS_MM = 10
|
||||
SUPPORT_DISK_DEPTH_MM = 20
|
||||
|
||||
# Default parameter values for ``add_railing_representation`` (millimetres).
|
||||
DEFAULT_SUPPORT_SPACING_MM = 1000
|
||||
DEFAULT_RAILING_DIAMETER_MM = 50
|
||||
DEFAULT_CLEAR_WIDTH_MM = 40
|
||||
DEFAULT_HEIGHT_MM = 1000
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class RailingSupport:
|
||||
"""Pure-geometry description of a single wall-mount support.
|
||||
|
||||
A support consists of:
|
||||
|
||||
- A 3-point polyline (base at the handrail, mid-arc, floor end)
|
||||
swept into a cylinder of radius ``arc_radius``.
|
||||
- A short disk extrusion (wall-attachment plate) at the floor end.
|
||||
|
||||
All values are in IFC project units.
|
||||
"""
|
||||
|
||||
arc_polyline: np.ndarray # shape (3, 3)
|
||||
arc_radius: float
|
||||
disk_position: np.ndarray # shape (3,) — equal to arc_polyline[-1]
|
||||
disk_radius: float
|
||||
disk_depth: float
|
||||
disk_z_rotation: float # rotation around Z applied to the disk's "Y" extrude axis
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class WallMountedHandrailGeometry:
|
||||
"""Pure-geometry description of a wall-mounted handrail.
|
||||
|
||||
Decoupled from any IFC entity creation. The shared data structure is
|
||||
consumed by the IFC-representation wrapper and by viewport-only previews
|
||||
in authoring add-ons that need to update mesh state without mutating the
|
||||
IFC file.
|
||||
|
||||
All values are in IFC project units.
|
||||
"""
|
||||
|
||||
handrail_polyline: np.ndarray # shape (N, 3)
|
||||
handrail_arc_point_indices: list[int]
|
||||
handrail_radius: float
|
||||
supports: list[RailingSupport] = field(default_factory=list)
|
||||
|
||||
|
||||
_Z_DOWN = V(0, 0, -1)
|
||||
_ARC_MIDDLE_POINT_COS = sin(radians(45))
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class _RailingDims:
|
||||
"""Derived dimensions for a wall-mounted-handrail compute pass.
|
||||
|
||||
All values are in IFC project units.
|
||||
"""
|
||||
|
||||
railing_radius: float
|
||||
height_below_handrail: float
|
||||
terminal_radius: float
|
||||
fillet_radius: float
|
||||
support_spacing: float
|
||||
support_length: float
|
||||
support_arc_radius: float
|
||||
support_disk_radius: float
|
||||
support_disk_depth: float
|
||||
clear_width: float
|
||||
cap_type: TERMINAL_TYPE
|
||||
|
||||
|
||||
def _collinear(d0: np.ndarray, d1: np.ndarray) -> bool:
|
||||
# Cross-product magnitude is linear near zero, so the test stays
|
||||
# numerically stable for near-parallel unit vectors. The natural
|
||||
# arccos(dot) formulation is not stable here: sub-ulp overshoot of
|
||||
# dot past 1.0 returns NaN, which would silently break the fillet
|
||||
# on straight subdivided edges. Anti-parallel vectors also collapse
|
||||
# |d0 × d1| to 0 — and that "no usable turn" outcome is what the
|
||||
# fillet caller wants, so we treat it as collinear too.
|
||||
return bool(np.linalg.norm(np.cross(d0, d1)) < PRECISION)
|
||||
|
||||
|
||||
def _get_fillet_points(v0: np.ndarray, v1: np.ndarray, v2: np.ndarray, radius: float) -> list[np.ndarray]:
|
||||
"""Fillet arc points between edges v0v1 and v1v2.
|
||||
|
||||
Raises ``ZeroDivisionError`` / ``FloatingPointError`` (and may return
|
||||
NaN/inf points) on numerically degenerate input — callers that may
|
||||
receive degenerate input must guard.
|
||||
"""
|
||||
dir1 = np_normalized(v0 - v1)
|
||||
dir2 = np_normalized(v2 - v1)
|
||||
edge_angle = np_angle(dir1, dir2)
|
||||
slide_distance = radius / tan(edge_angle / 2)
|
||||
|
||||
fillet_v1co = v1 + (dir1 * slide_distance)
|
||||
fillet_v2co = v1 + (dir2 * slide_distance)
|
||||
|
||||
normal = np_normal([v0, v1, v2])
|
||||
center = np_intersect_line_line(
|
||||
fillet_v1co,
|
||||
fillet_v1co + np.cross(normal, dir1),
|
||||
fillet_v2co,
|
||||
fillet_v2co + np.cross(normal, dir2),
|
||||
)[0]
|
||||
|
||||
dir_ = np_normalized(np_lerp(fillet_v1co, fillet_v2co, 0.5) - center)
|
||||
midpointco = center + dir_ * radius
|
||||
return [fillet_v1co, midpointco, fillet_v2co]
|
||||
|
||||
|
||||
def _make_support(point: np.ndarray, railing_direction: np.ndarray, dims: _RailingDims) -> RailingSupport:
|
||||
"""Build a pure-geometry support description from a point + railing direction."""
|
||||
ortho_dir = railing_direction[NP_YX] * (1, -1)
|
||||
ortho_dir = np_normalized(np_to_3d(ortho_dir))
|
||||
arc_center = point + ortho_dir * dims.support_length
|
||||
support_points = V(
|
||||
[
|
||||
point,
|
||||
arc_center - ortho_dir * dims.support_length * cos(pi / 4) + _Z_DOWN * dims.support_length * sin(pi / 4),
|
||||
arc_center + _Z_DOWN * dims.support_length,
|
||||
]
|
||||
)
|
||||
angle = np_angle_signed((0, 1), ortho_dir[NP_XY])
|
||||
return RailingSupport(
|
||||
arc_polyline=support_points,
|
||||
arc_radius=dims.support_arc_radius,
|
||||
disk_position=support_points[-1],
|
||||
disk_radius=dims.support_disk_radius,
|
||||
disk_depth=dims.support_disk_depth,
|
||||
disk_z_rotation=angle,
|
||||
)
|
||||
|
||||
|
||||
def _add_arcs_on_turning_points(
|
||||
base_points: np.ndarray, dims: _RailingDims, looped_path: bool
|
||||
) -> tuple[np.ndarray, list[np.ndarray]]:
|
||||
"""Add 3-point fillet arcs on turning points of the railing path.
|
||||
|
||||
Returns ``(polyline_with_arcs, arc_midpoints)``.
|
||||
"""
|
||||
arc_points: list[np.ndarray] = []
|
||||
if len(base_points) < 3:
|
||||
return base_points, arc_points
|
||||
|
||||
# looking for turning points by checking non-collinear edges
|
||||
output_points: list[np.ndarray] = list(base_points[:1])
|
||||
prev_dir = np_normalized(base_points[1] - base_points[0])
|
||||
i = 1
|
||||
while i < len(base_points) - 1:
|
||||
cur_dir = np_normalized(base_points[i + 1] - base_points[i])
|
||||
|
||||
# Treat NaN cur_dir (zero-length edge → np_normalized of zero) as
|
||||
# collinear: a coincident path vertex carries no turn information,
|
||||
# so the safest fallback is "stay on the previous direction".
|
||||
cur_dir_is_nan = bool(np.any(np.isnan(cur_dir)))
|
||||
|
||||
if cur_dir_is_nan or _collinear(cur_dir, prev_dir):
|
||||
output_points.append(base_points[i])
|
||||
else:
|
||||
# User-supplied railing paths can produce numerically degenerate
|
||||
# turns (anti-parallel directions, nearly-collinear triangle,
|
||||
# zero-length edges from coincident vertices). Falling back to a
|
||||
# sharp turn at the original vertex keeps the rest of the
|
||||
# polyline real-valued instead of poisoning it with NaN.
|
||||
fillet_points: Optional[list[np.ndarray]]
|
||||
try:
|
||||
fillet_points = _get_fillet_points(
|
||||
base_points[i - 1], base_points[i], base_points[i + 1], dims.fillet_radius
|
||||
)
|
||||
except (ZeroDivisionError, FloatingPointError):
|
||||
fillet_points = None
|
||||
else:
|
||||
if any(np.any(np.isnan(fp)) or np.any(np.isinf(fp)) for fp in fillet_points):
|
||||
fillet_points = None
|
||||
|
||||
if fillet_points is None:
|
||||
output_points.append(base_points[i])
|
||||
else:
|
||||
output_points.extend(fillet_points)
|
||||
arc_points.append(fillet_points[1])
|
||||
|
||||
# Only advance prev_dir when cur_dir is well-defined — keeping a
|
||||
# NaN prev_dir would cascade through every subsequent collinearity
|
||||
# check.
|
||||
if not cur_dir_is_nan:
|
||||
prev_dir = cur_dir
|
||||
i = i + 1
|
||||
|
||||
if looped_path:
|
||||
output_points[0] = output_points[-1]
|
||||
else:
|
||||
output_points.append(base_points[-1])
|
||||
return V(output_points), arc_points
|
||||
|
||||
|
||||
def _collect_supports(coords: np.ndarray, manual_supports: bool, dims: _RailingDims) -> list[RailingSupport]:
|
||||
"""Build the list of supports for the railing path."""
|
||||
supports: list[RailingSupport] = []
|
||||
# simplified_coords is a list of points that form non-collinear edges
|
||||
simplified_coords: list[np.ndarray] = [coords[0]]
|
||||
prev_dir = np_normalized(coords[1] - coords[0])
|
||||
|
||||
# iterating over each edge of the railing path
|
||||
for i in range(1, len(coords) - 1):
|
||||
cur_dir = np_normalized(coords[i + 1] - coords[i])
|
||||
|
||||
if not _collinear(cur_dir, prev_dir):
|
||||
simplified_coords.append(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.append(_make_support(coords[i], cur_dir, dims))
|
||||
|
||||
simplified_coords.append(coords[-1])
|
||||
|
||||
if manual_supports:
|
||||
return supports
|
||||
|
||||
# create automatic supports based on the support spacing
|
||||
for i in range(len(simplified_coords) - 1):
|
||||
v0, v1 = simplified_coords[i : i + 2]
|
||||
edge = v1 - v0
|
||||
length: float = np.linalg.norm(edge)
|
||||
edge_dir = np_normalized(edge)
|
||||
n_supports, support_offset = divmod(length, dims.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 * dims.support_spacing * edge_dir
|
||||
supports.append(_make_support(support_position, edge, dims))
|
||||
|
||||
return supports
|
||||
|
||||
|
||||
# Per-cap-type builders. Each takes the cap-frame inputs (precomputed by the
|
||||
# dispatcher) and returns ``(cap_coords, new_arc_points)``. The shared
|
||||
# orientation flip and final ``np.vstack`` live in the dispatcher so the
|
||||
# builders stay focused on the geometric shape of their cap.
|
||||
_CapBuilder = Callable[
|
||||
[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray, "_RailingDims"],
|
||||
tuple[list[np.ndarray], list[np.ndarray]],
|
||||
]
|
||||
|
||||
|
||||
def _cap_180(
|
||||
railing_coords_for_cap: np.ndarray,
|
||||
start_point: np.ndarray,
|
||||
cap_dir: np.ndarray,
|
||||
ortho_dir: np.ndarray,
|
||||
local_z_down: np.ndarray,
|
||||
dims: "_RailingDims",
|
||||
) -> tuple[list[np.ndarray], list[np.ndarray]]:
|
||||
arc_point = start_point + cap_dir * dims.terminal_radius + dims.terminal_radius * local_z_down
|
||||
cap_coords = [arc_point, start_point + dims.terminal_radius * 2 * local_z_down]
|
||||
return cap_coords, [arc_point]
|
||||
|
||||
|
||||
def _cap_to_end_post(
|
||||
railing_coords_for_cap: np.ndarray,
|
||||
start_point: np.ndarray,
|
||||
cap_dir: np.ndarray,
|
||||
ortho_dir: np.ndarray,
|
||||
local_z_down: np.ndarray,
|
||||
dims: "_RailingDims",
|
||||
) -> tuple[list[np.ndarray], list[np.ndarray]]:
|
||||
arc_point = start_point + cap_dir * dims.terminal_radius + dims.terminal_radius * local_z_down
|
||||
end_point = railing_coords_for_cap[-2].copy()
|
||||
end_point[NP_Z] -= dims.terminal_radius * 2
|
||||
cap_coords = [arc_point, start_point + dims.terminal_radius * 2 * local_z_down, end_point]
|
||||
return cap_coords, [arc_point]
|
||||
|
||||
|
||||
def _cap_to_wall(
|
||||
railing_coords_for_cap: np.ndarray,
|
||||
start_point: np.ndarray,
|
||||
cap_dir: np.ndarray,
|
||||
ortho_dir: np.ndarray,
|
||||
local_z_down: np.ndarray,
|
||||
dims: "_RailingDims",
|
||||
) -> tuple[list[np.ndarray], list[np.ndarray]]:
|
||||
arc_point = (
|
||||
start_point
|
||||
+ cap_dir * dims.clear_width * _ARC_MIDDLE_POINT_COS
|
||||
+ ortho_dir * dims.clear_width * (1 - _ARC_MIDDLE_POINT_COS)
|
||||
)
|
||||
cap_coords = [arc_point, start_point + ortho_dir * dims.clear_width + cap_dir * dims.clear_width]
|
||||
return cap_coords, [arc_point]
|
||||
|
||||
|
||||
def _cap_to_floor(
|
||||
railing_coords_for_cap: np.ndarray,
|
||||
start_point: np.ndarray,
|
||||
cap_dir: np.ndarray,
|
||||
ortho_dir: np.ndarray,
|
||||
local_z_down: np.ndarray,
|
||||
dims: "_RailingDims",
|
||||
) -> tuple[list[np.ndarray], list[np.ndarray]]:
|
||||
arc_point = (
|
||||
start_point
|
||||
+ cap_dir * dims.terminal_radius * _ARC_MIDDLE_POINT_COS
|
||||
+ _Z_DOWN * dims.terminal_radius * (1 - _ARC_MIDDLE_POINT_COS)
|
||||
)
|
||||
arc_end = start_point + cap_dir * dims.terminal_radius + dims.terminal_radius * _Z_DOWN
|
||||
cap_coords = [
|
||||
arc_point,
|
||||
arc_end,
|
||||
arc_end + _Z_DOWN * (dims.height_below_handrail - dims.terminal_radius),
|
||||
]
|
||||
return cap_coords, [arc_point]
|
||||
|
||||
|
||||
def _cap_to_end_post_and_floor(
|
||||
railing_coords_for_cap: np.ndarray,
|
||||
start_point: np.ndarray,
|
||||
cap_dir: np.ndarray,
|
||||
ortho_dir: np.ndarray,
|
||||
local_z_down: np.ndarray,
|
||||
dims: "_RailingDims",
|
||||
) -> tuple[list[np.ndarray], list[np.ndarray]]:
|
||||
first_arc_end = start_point + cap_dir * dims.terminal_radius + dims.terminal_radius * local_z_down
|
||||
first_arc_coords = _get_fillet_points(
|
||||
start_point, start_point + cap_dir * dims.terminal_radius, first_arc_end, dims.terminal_radius
|
||||
)
|
||||
end_point = railing_coords_for_cap[-2].copy()
|
||||
end_point[NP_Z] -= dims.height_below_handrail
|
||||
second_arc_coords = _get_fillet_points(
|
||||
first_arc_end, first_arc_end + local_z_down * dims.terminal_radius, end_point, dims.terminal_radius
|
||||
)
|
||||
cap_coords = [start_point] + first_arc_coords + second_arc_coords + [end_point]
|
||||
return cap_coords, [first_arc_coords[1], second_arc_coords[1]]
|
||||
|
||||
|
||||
# Dispatch table for handrail terminal caps. "NONE" stays out of this table:
|
||||
# every other cap type appends real geometry to the polyline, so a "NONE" slot
|
||||
# would need an awkward empty-vstack contract — the dispatcher early-returns
|
||||
# unchanged instead.
|
||||
_CAP_BUILDERS: dict[TERMINAL_TYPE, _CapBuilder] = {
|
||||
"180": _cap_180,
|
||||
"TO_END_POST": _cap_to_end_post,
|
||||
"TO_WALL": _cap_to_wall,
|
||||
"TO_FLOOR": _cap_to_floor,
|
||||
"TO_END_POST_AND_FLOOR": _cap_to_end_post_and_floor,
|
||||
}
|
||||
|
||||
|
||||
def _add_cap(
|
||||
railing_coords: np.ndarray,
|
||||
arc_points_list: list[np.ndarray],
|
||||
start: bool,
|
||||
dims: _RailingDims,
|
||||
) -> tuple[np.ndarray, list[np.ndarray]]:
|
||||
"""Add a handrail terminal cap at one end of the railing.
|
||||
|
||||
Returns the inputs unchanged when ``dims.cap_type == "NONE"``.
|
||||
"""
|
||||
if dims.cap_type == "NONE":
|
||||
return railing_coords, arc_points_list
|
||||
|
||||
railing_coords_for_cap = railing_coords[::-1] if start else railing_coords
|
||||
arc_points_list = arc_points_list[::-1] if start else arc_points_list
|
||||
|
||||
start_point: np.ndarray = railing_coords_for_cap[-1]
|
||||
cap_dir = np_normalized(railing_coords_for_cap[-1] - railing_coords_for_cap[-2])
|
||||
ortho_dir = np_normalized(np_to_3d(cap_dir[NP_YX] * (1, -1)))
|
||||
local_z_down = np.cross(cap_dir, ortho_dir)
|
||||
if start:
|
||||
ortho_dir = -ortho_dir
|
||||
|
||||
cap_coords, new_arc_points = _CAP_BUILDERS[dims.cap_type](
|
||||
railing_coords_for_cap, start_point, cap_dir, ortho_dir, local_z_down, dims
|
||||
)
|
||||
arc_points_list.extend(new_arc_points)
|
||||
railing_coords = np.vstack((railing_coords_for_cap, cap_coords))
|
||||
|
||||
if start:
|
||||
railing_coords = railing_coords[::-1]
|
||||
arc_points_list = arc_points_list[::-1]
|
||||
return railing_coords, arc_points_list
|
||||
|
||||
|
||||
def _get_arc_indices(points: np.ndarray, arc_pts: list[np.ndarray]) -> list[int]:
|
||||
points_ = points.copy()
|
||||
arc_indices = []
|
||||
i_base = 0
|
||||
for arc_point in arc_pts:
|
||||
for i, point in enumerate(points_):
|
||||
if np.allclose(arc_point, point):
|
||||
current_index = i + i_base
|
||||
arc_indices.append(current_index)
|
||||
i_base = current_index + 1
|
||||
break
|
||||
else:
|
||||
raise Exception(
|
||||
f"Arc point '{arc_point}' is not present in points:\n{points_}\nFull points data:\n{points}"
|
||||
)
|
||||
points_ = points_[i + 1 :]
|
||||
return arc_indices
|
||||
|
||||
|
||||
def compute_wall_mounted_handrail_geometry(
|
||||
*,
|
||||
railing_path: SequenceOfVectors,
|
||||
support_spacing: float,
|
||||
railing_diameter: float,
|
||||
clear_width: float,
|
||||
height: float,
|
||||
use_manual_supports: bool = False,
|
||||
terminal_type: TERMINAL_TYPE = "180",
|
||||
looped_path: bool = False,
|
||||
unit_scale: float = 1.0,
|
||||
) -> WallMountedHandrailGeometry:
|
||||
"""Compute pure geometric data for a wall-mounted handrail.
|
||||
|
||||
The result can be wrapped into an ``IfcShapeRepresentation`` by the
|
||||
railing-representation API, or converted directly to a Blender bmesh
|
||||
(or any other viewport mesh) for a live preview that does not mutate
|
||||
the IFC file.
|
||||
|
||||
Geometric inputs (``railing_path``, ``support_spacing``,
|
||||
``railing_diameter``, ``clear_width``, ``height``) are expected in IFC
|
||||
project units. ``unit_scale`` is used only to convert hard-coded
|
||||
millimetre constants (fillet radius, support rod radius, etc.) into
|
||||
project units.
|
||||
|
||||
Constraints:
|
||||
|
||||
- ``railing_path`` must contain at least 2 points.
|
||||
- ``railing_diameter`` must be > 0.
|
||||
- ``height`` must be ≥ ``railing_diameter / 2`` (otherwise the
|
||||
``TO_FLOOR`` / ``TO_END_POST_AND_FLOOR`` caps extrude upward
|
||||
instead of down).
|
||||
- ``clear_width`` must be > 0 (otherwise the support wraps backward
|
||||
into the wall).
|
||||
|
||||
:param railing_path: Sequence of 3D points along the top of the
|
||||
handrail (not the centre).
|
||||
:param support_spacing: Distance between automatic supports.
|
||||
:param railing_diameter: Handrail tube diameter.
|
||||
:param clear_width: Clear gap between the wall and the handrail tube.
|
||||
:param height: Total railing height (top of handrail to floor).
|
||||
:param use_manual_supports: If true, one support is placed on every
|
||||
non-collinear vertex of ``railing_path``; if false, supports are
|
||||
distributed automatically by ``support_spacing``.
|
||||
:param terminal_type: Style of the terminal end cap, or ``"NONE"`` for
|
||||
no cap. Ignored when ``looped_path=True`` (no open ends to cap).
|
||||
:param looped_path: If true, the railing closes on its first point.
|
||||
:param unit_scale: Output of
|
||||
:func:`ifcopenshell.util.unit.calculate_unit_scale`. Defaults to
|
||||
1.0 (i.e. inputs are already in metres).
|
||||
"""
|
||||
railing_radius = railing_diameter / 2
|
||||
# for calculations purposes we use height without railing radius
|
||||
height_below_handrail = height - railing_radius
|
||||
railing_coords: np.ndarray = np.subtract(railing_path, _Z_DOWN * railing_radius)
|
||||
|
||||
dims = _RailingDims(
|
||||
railing_radius=railing_radius,
|
||||
height_below_handrail=height_below_handrail,
|
||||
terminal_radius=mm(TERMINAL_RADIUS_MM) / unit_scale,
|
||||
fillet_radius=mm(HANDRAIL_FILLET_RADIUS_MM) / unit_scale,
|
||||
support_spacing=support_spacing,
|
||||
support_length=clear_width + railing_radius,
|
||||
support_arc_radius=mm(SUPPORT_ARC_RADIUS_MM) / unit_scale,
|
||||
support_disk_radius=railing_radius,
|
||||
support_disk_depth=mm(SUPPORT_DISK_DEPTH_MM) / unit_scale,
|
||||
clear_width=clear_width,
|
||||
cap_type=terminal_type,
|
||||
)
|
||||
|
||||
# need to add first two points to the path
|
||||
# to create the turning arcs and supports on the last segment of the loop
|
||||
if looped_path:
|
||||
railing_coords = np.vstack((railing_coords, railing_coords[:2]))
|
||||
|
||||
supports = _collect_supports(railing_coords, use_manual_supports, dims)
|
||||
railing_coords, arc_points = _add_arcs_on_turning_points(railing_coords, dims, looped_path)
|
||||
|
||||
if not looped_path:
|
||||
railing_coords, arc_points = _add_cap(railing_coords, arc_points, start=True, dims=dims)
|
||||
railing_coords, arc_points = _add_cap(railing_coords, arc_points, start=False, dims=dims)
|
||||
|
||||
return WallMountedHandrailGeometry(
|
||||
handrail_polyline=railing_coords,
|
||||
handrail_arc_point_indices=_get_arc_indices(railing_coords, arc_points),
|
||||
handrail_radius=railing_radius,
|
||||
supports=supports,
|
||||
)
|
||||
|
||||
|
||||
def _resolve_default_mm(value: Optional[float], default_mm: float, unit_scale: float) -> float:
|
||||
"""Resolve an optional millimetre-defaulted parameter into project units.
|
||||
|
||||
Callers pass ``value`` as the user-supplied override (or ``None``) and
|
||||
``default_mm`` as the integer millimetre default; the result is in project
|
||||
units (``mm/1000 / unit_scale``).
|
||||
"""
|
||||
if value is not None:
|
||||
return value
|
||||
return mm(default_mm) / unit_scale
|
||||
|
||||
|
||||
def add_railing_representation(
|
||||
file: ifcopenshell.file,
|
||||
*, # keywords only as this API implementation is probably not final
|
||||
# IfcGeometricRepresentationContext
|
||||
context: ifcopenshell.entity_instance,
|
||||
railing_type: Literal["WALL_MOUNTED_HANDRAIL"] = "WALL_MOUNTED_HANDRAIL",
|
||||
railing_path: SequenceOfVectors,
|
||||
use_manual_supports: bool = False,
|
||||
support_spacing: Optional[float] = None,
|
||||
@@ -72,7 +579,6 @@ def add_railing_representation(
|
||||
Units are expected to be in IFC project units.
|
||||
|
||||
:param context: IfcGeometricRepresentationContext for the representation.
|
||||
:param railing_type: Type of the railing. Defaults to "WALL_MOUNTED_HANDRAIL".
|
||||
:param railing_path: A list of points coordinates for the railing path,
|
||||
coordinates are expected to be at the top of the railing, not at the center.
|
||||
If not provided, default path [(0, 0, 1), (1, 0, 1), (2, 0, 1)] (in meters) will be used
|
||||
@@ -81,7 +587,7 @@ def add_railing_representation(
|
||||
:param support_spacing: Distance between supports if automatic supports are used. Defaults to 1m.
|
||||
:param railing_diameter: Railing diameter. Defaults to 50mm.
|
||||
:param clear_width: Clear width between the railing and the wall. Defaults to 40mm.
|
||||
:param terminal_type: type of the cap. Defaults to "180".
|
||||
:param terminal_type: type of the cap, or "NONE" for no cap. Defaults to "180".
|
||||
:param height: defaults to 1m
|
||||
:param looped_path: Whether to end the railing on the first point of `railing_path`. Defaults to False.
|
||||
:param unit_scale: The unit scale as calculated by
|
||||
@@ -89,317 +595,51 @@ def add_railing_representation(
|
||||
will be automatically calculated for you.
|
||||
:return: IfcShapeRepresentation for a railing.
|
||||
"""
|
||||
usecase = Usecase()
|
||||
usecase.file = file
|
||||
# define unit_scale first as it's going to be used setting default arguments
|
||||
settings: dict[str, Any] = {
|
||||
"unit_scale": ifcopenshell.util.unit.calculate_unit_scale(file) if unit_scale is None else unit_scale,
|
||||
}
|
||||
settings.update(
|
||||
{
|
||||
"context": context,
|
||||
"railing_type": railing_path,
|
||||
"railing_path": (
|
||||
railing_path
|
||||
if railing_path is not None
|
||||
else usecase.path_si_to_units(V([(0, 0, 1), (1, 0, 1), (2, 0, 1)]))
|
||||
),
|
||||
"use_manual_supports": use_manual_supports,
|
||||
"support_spacing": support_spacing if support_spacing is not None else usecase.convert_si_to_unit(mm(1000)),
|
||||
"railing_diameter": (
|
||||
railing_diameter if railing_diameter is not None else usecase.convert_si_to_unit(mm(50))
|
||||
),
|
||||
"clear_width": clear_width if clear_width is not None else usecase.convert_si_to_unit(mm(40)),
|
||||
"terminal_type": terminal_type,
|
||||
"height": height if height is not None else usecase.convert_si_to_unit(mm(1000)),
|
||||
"looped_path": looped_path,
|
||||
}
|
||||
if unit_scale is None:
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(file)
|
||||
|
||||
if railing_path is None:
|
||||
railing_path = V([(0, 0, 1), (1, 0, 1), (2, 0, 1)]) / unit_scale
|
||||
support_spacing = _resolve_default_mm(support_spacing, DEFAULT_SUPPORT_SPACING_MM, unit_scale)
|
||||
railing_diameter = _resolve_default_mm(railing_diameter, DEFAULT_RAILING_DIAMETER_MM, unit_scale)
|
||||
clear_width = _resolve_default_mm(clear_width, DEFAULT_CLEAR_WIDTH_MM, unit_scale)
|
||||
height = _resolve_default_mm(height, DEFAULT_HEIGHT_MM, unit_scale)
|
||||
|
||||
geometry = compute_wall_mounted_handrail_geometry(
|
||||
railing_path=railing_path,
|
||||
use_manual_supports=use_manual_supports,
|
||||
support_spacing=support_spacing,
|
||||
railing_diameter=railing_diameter,
|
||||
clear_width=clear_width,
|
||||
terminal_type=terminal_type,
|
||||
height=height,
|
||||
looped_path=looped_path,
|
||||
unit_scale=unit_scale,
|
||||
)
|
||||
usecase.settings = settings
|
||||
|
||||
if railing_type != "WALL_MOUNTED_HANDRAIL":
|
||||
raise Exception('Only "WALL_MOUNTED_HANDRAIL" railing_type is supported at the moment.')
|
||||
return usecase.execute()
|
||||
builder = ShapeBuilder(file)
|
||||
items_3d: list[ifcopenshell.entity_instance] = []
|
||||
|
||||
for support in geometry.supports:
|
||||
support_polyline = builder.polyline(support.arc_polyline, closed=False, arc_points=(1,))
|
||||
items_3d.append(builder.create_swept_disk_solid(support_polyline, support.arc_radius))
|
||||
|
||||
class Usecase:
|
||||
file: ifcopenshell.file
|
||||
settings: dict[str, Any]
|
||||
|
||||
def execute(self):
|
||||
arc_points: list[np.ndarray] = []
|
||||
items_3d: list[ifcopenshell.entity_instance] = []
|
||||
builder = ShapeBuilder(self.file)
|
||||
z_down = V(0, 0, -1)
|
||||
|
||||
# measurements
|
||||
# from settings
|
||||
use_manual_supports: bool = self.settings["use_manual_supports"]
|
||||
railing_radius: float = self.settings["railing_diameter"] / 2
|
||||
support_spacing: float = self.settings["support_spacing"]
|
||||
clear_width: float = self.settings["clear_width"]
|
||||
# for calculations purposes we use height without railing radius
|
||||
height: float = self.settings["height"] - railing_radius
|
||||
cap_type: TERMINAL_TYPE = self.settings["terminal_type"]
|
||||
ifc_context: ifcopenshell.entity_instance = self.settings["context"]
|
||||
railing_coords: SequenceOfVectors = self.settings["railing_path"]
|
||||
looped_path: bool = self.settings["looped_path"]
|
||||
railing_coords: np.ndarray
|
||||
railing_coords = np.subtract(railing_coords, z_down * railing_radius)
|
||||
|
||||
# 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
|
||||
def collinear(d0: np.ndarray, d1: np.ndarray) -> bool:
|
||||
return is_x(np_angle(d0, d1), 0)
|
||||
|
||||
np_Z = 2
|
||||
np_XY = slice(2)
|
||||
np_YX = [1, 0]
|
||||
|
||||
def add_support_on_point(
|
||||
point: np.ndarray, railing_direction: np.ndarray
|
||||
) -> tuple[ifcopenshell.entity_instance, ...]:
|
||||
"""create a support arc and a disk based on the position and direction of the railing"""
|
||||
ortho_dir = railing_direction[np_YX] * (1, -1)
|
||||
ortho_dir = np_normalized(np_to_3d(ortho_dir))
|
||||
arc_center = point + ortho_dir * support_length
|
||||
support_points: list[np.ndarray] = [
|
||||
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)
|
||||
|
||||
angle = np_angle_signed((0, 1), ortho_dir[np_XY])
|
||||
y_extrusion_kwargs = builder.rotate_extrusion_kwargs_by_z(builder.extrude_kwargs("Y"), angle)
|
||||
support_disk = builder.extrude(
|
||||
support_disk_circle, support_disk_depth, position=support_points[-1], **y_extrusion_kwargs
|
||||
disk_circle = builder.circle(radius=support.disk_radius)
|
||||
y_extrusion_kwargs = builder.rotate_extrusion_kwargs_by_z(builder.extrude_kwargs("Y"), support.disk_z_rotation)
|
||||
items_3d.append(
|
||||
builder.extrude(
|
||||
disk_circle,
|
||||
support.disk_depth,
|
||||
position=support.disk_position,
|
||||
**y_extrusion_kwargs,
|
||||
)
|
||||
return (solid, support_disk)
|
||||
|
||||
def get_fillet_points(v0: np.ndarray, v1: np.ndarray, v2: np.ndarray, radius: float) -> list[np.ndarray]:
|
||||
"""get fillet points between edges v0v1 and v1v2"""
|
||||
dir1 = np_normalized(v0 - v1)
|
||||
dir2 = np_normalized(v2 - v1)
|
||||
edge_angle = np_angle(dir1, dir2)
|
||||
slide_distance = radius / tan(edge_angle / 2)
|
||||
|
||||
fillet_v1co = v1 + (dir1 * slide_distance)
|
||||
fillet_v2co = v1 + (dir2 * slide_distance)
|
||||
|
||||
normal = np_normal([v0, v1, v2])
|
||||
center = np_intersect_line_line(
|
||||
fillet_v1co,
|
||||
fillet_v1co + np.cross(normal, dir1),
|
||||
fillet_v2co,
|
||||
fillet_v2co + np.cross(normal, dir2),
|
||||
)[0]
|
||||
|
||||
dir_ = np_normalized(np_lerp(fillet_v1co, fillet_v2co, 0.5) - center)
|
||||
midpointco = center + dir_ * radius
|
||||
return [fillet_v1co, midpointco, fillet_v2co]
|
||||
|
||||
def add_arcs_on_turnings_points(base_points: np.ndarray) -> np.ndarray:
|
||||
"""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: list[np.ndarray] = list(base_points[:1])
|
||||
prev_dir = np_normalized(base_points[1] - base_points[0])
|
||||
i = 1
|
||||
while i < len(base_points) - 1:
|
||||
cur_dir = np_normalized(base_points[i + 1] - base_points[i])
|
||||
|
||||
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
|
||||
|
||||
if looped_path:
|
||||
output_points[0] = output_points[-1]
|
||||
else:
|
||||
output_points.append(base_points[-1])
|
||||
return V(output_points)
|
||||
|
||||
def create_supports_items(
|
||||
railing_coords: np.ndarray, manual_supports: bool = False
|
||||
) -> list[ifcopenshell.entity_instance]:
|
||||
"""create supports items based on the railing coordinates"""
|
||||
supports_items: list[ifcopenshell.entity_instance] = []
|
||||
|
||||
# simplified_coords is a list of points that form non-collinear edges
|
||||
simplified_coords: list[np.ndarray] = [railing_coords[0]]
|
||||
prev_dir = np_normalized(railing_coords[1] - railing_coords[0])
|
||||
|
||||
# iterating over each edge of the railing path
|
||||
for i in range(1, len(railing_coords) - 1):
|
||||
cur_dir = np_normalized(railing_coords[i + 1] - railing_coords[i])
|
||||
|
||||
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: float = np.linalg.norm(edge)
|
||||
edge_dir = np_normalized(edge)
|
||||
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: np.ndarray, arc_points: list[np.ndarray], start: bool = False):
|
||||
"""add handrail terminal cap"""
|
||||
railing_coords_for_cap = railing_coords[::-1] if start else railing_coords
|
||||
arc_points = arc_points[::-1] if start else arc_points
|
||||
|
||||
start_point: np.ndarray = railing_coords_for_cap[-1]
|
||||
cap_dir = railing_coords_for_cap[-1] - railing_coords_for_cap[-2]
|
||||
cap_dir = np_normalized(cap_dir)
|
||||
ortho_dir = np_to_3d(cap_dir[np_YX] * (1, -1))
|
||||
ortho_dir = np_normalized(ortho_dir)
|
||||
local_z_down = np.cross(cap_dir, ortho_dir)
|
||||
if start:
|
||||
ortho_dir = -ortho_dir
|
||||
|
||||
arc_middle_point_cos = sin(radians(45))
|
||||
|
||||
if cap_type in ("180", "TO_END_POST"):
|
||||
arc_point = start_point + cap_dir * terminal_radius + terminal_radius * local_z_down
|
||||
arc_points.append(arc_point)
|
||||
cap_coords = [arc_point, start_point + terminal_radius * 2 * local_z_down]
|
||||
|
||||
if cap_type == "TO_END_POST":
|
||||
end_point = railing_coords_for_cap[-2].copy()
|
||||
end_point[np_Z] -= terminal_radius * 2
|
||||
cap_coords.append(end_point)
|
||||
|
||||
elif cap_type == "TO_WALL":
|
||||
arc_point = (
|
||||
start_point
|
||||
+ cap_dir * clear_width * arc_middle_point_cos
|
||||
+ ortho_dir * clear_width * (1 - arc_middle_point_cos)
|
||||
)
|
||||
arc_points.append(arc_point)
|
||||
cap_coords = [arc_point, start_point + ortho_dir * clear_width + cap_dir * clear_width]
|
||||
|
||||
elif cap_type == "TO_FLOOR":
|
||||
arc_point = (
|
||||
start_point
|
||||
+ cap_dir * terminal_radius * arc_middle_point_cos
|
||||
+ z_down * terminal_radius * (1 - arc_middle_point_cos)
|
||||
)
|
||||
arc_points.append(arc_point)
|
||||
arc_end = start_point + cap_dir * terminal_radius + terminal_radius * z_down
|
||||
cap_coords = [
|
||||
arc_point,
|
||||
arc_end,
|
||||
arc_end + z_down * (height - terminal_radius),
|
||||
]
|
||||
|
||||
elif cap_type == "TO_END_POST_AND_FLOOR":
|
||||
first_arc_end = start_point + cap_dir * terminal_radius + terminal_radius * local_z_down
|
||||
first_arc_coords = get_fillet_points(
|
||||
start_point, start_point + cap_dir * terminal_radius, first_arc_end, terminal_radius
|
||||
)
|
||||
arc_points.append(first_arc_coords[1])
|
||||
|
||||
end_point = railing_coords_for_cap[-2].copy()
|
||||
end_point[np_Z] -= height
|
||||
second_arc_coords = get_fillet_points(
|
||||
first_arc_end, first_arc_end + local_z_down * terminal_radius, end_point, terminal_radius
|
||||
)
|
||||
arc_points.append(second_arc_coords[1])
|
||||
cap_coords = [start_point] + first_arc_coords + second_arc_coords + [end_point]
|
||||
else:
|
||||
assert_never(cap_type)
|
||||
|
||||
railing_coords = np.vstack((railing_coords_for_cap, cap_coords))
|
||||
|
||||
if start:
|
||||
railing_coords = railing_coords[::-1]
|
||||
arc_points = arc_points[::-1]
|
||||
return railing_coords, arc_points
|
||||
|
||||
# need to add first two points to the path
|
||||
# to create the turning arcs and supports on the last segment of the loop
|
||||
if looped_path:
|
||||
railing_coords = np.vstack((railing_coords, railing_coords[:2]))
|
||||
|
||||
items_3d.extend(create_supports_items(railing_coords, manual_supports=use_manual_supports))
|
||||
railing_coords = add_arcs_on_turnings_points(railing_coords)
|
||||
|
||||
if not looped_path and cap_type != "NONE":
|
||||
railing_coords, arc_points = add_cap(railing_coords, arc_points, start=True)
|
||||
railing_coords, arc_points = add_cap(railing_coords, arc_points, start=False)
|
||||
|
||||
def get_arc_indices(points: np.ndarray, arc_points: list[np.ndarray]) -> list[int]:
|
||||
points_ = points.copy()
|
||||
arc_indices = []
|
||||
i_base = 0
|
||||
for arc_point in arc_points:
|
||||
for i, point in enumerate(points_):
|
||||
if np.allclose(arc_point, point):
|
||||
current_index = i + i_base
|
||||
arc_indices.append(current_index)
|
||||
i_base = current_index + 1
|
||||
break
|
||||
else:
|
||||
raise Exception(
|
||||
f"Arc point '{arc_point}' is not present in points:\n{points_}\nFull points data:\n{points}"
|
||||
)
|
||||
points_ = points_[i + 1 :]
|
||||
return arc_indices
|
||||
|
||||
railing_path = builder.polyline(
|
||||
railing_coords,
|
||||
closed=False,
|
||||
arc_points=get_arc_indices(railing_coords, 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: float) -> float:
|
||||
return value / self.settings["unit_scale"]
|
||||
railing_path_entity = builder.polyline(
|
||||
geometry.handrail_polyline,
|
||||
closed=False,
|
||||
arc_points=geometry.handrail_arc_point_indices,
|
||||
)
|
||||
items_3d.append(builder.create_swept_disk_solid(railing_path_entity, geometry.handrail_radius))
|
||||
|
||||
def path_si_to_units(self, path: np.ndarray) -> np.ndarray:
|
||||
"""converts list of vectors from SI to ifc project units"""
|
||||
return path / self.settings["unit_scale"]
|
||||
return builder.get_representation(context, items=items_3d)
|
||||
|
||||
Reference in New Issue
Block a user