mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-20 06:58:56 +00:00
More forgotten files
This commit is contained in:
@@ -0,0 +1,270 @@
|
|||||||
|
# ==============================================================================
|
||||||
|
# Saikei Civil - Civil Engineering Tools for Blender
|
||||||
|
# Copyright (c) 2025 Michael Yoder / Desert Springs Civil Engineering PLLC
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
# You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
# Primary Author: Michael Yoder
|
||||||
|
# Company: Desert Springs Civil Engineering PLLC
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
"""Core alignment business logic - Pure Python, NO bpy imports.
|
||||||
|
|
||||||
|
This module contains all alignment-related calculations and logic that
|
||||||
|
can be tested outside of Blender. Functions receive tool classes as
|
||||||
|
parameters following Bonsai's dependency injection pattern.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
import math
|
||||||
|
from typing import TYPE_CHECKING, List, Tuple, Optional
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
import ifcopenshell
|
||||||
|
from .. import tool
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Data Classes for Pure Python PI Handling
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PIPoint:
|
||||||
|
"""Pure Python representation of a PI (Point of Intersection).
|
||||||
|
|
||||||
|
This mirrors the Blender PropertyGroup but without bpy dependencies,
|
||||||
|
allowing for testing and core logic operations.
|
||||||
|
"""
|
||||||
|
|
||||||
|
x: float
|
||||||
|
y: float
|
||||||
|
pi_type: str = "TANGENT" # ENDPOINT, TANGENT, or CURVE
|
||||||
|
radius: float = 0.0
|
||||||
|
length_to_next: float = 0.0
|
||||||
|
direction_to_next: float = 0.0
|
||||||
|
station: float = 0.0
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PIGeometryResult:
|
||||||
|
"""Result of PI geometry calculation."""
|
||||||
|
|
||||||
|
stations: List[float]
|
||||||
|
lengths: List[float]
|
||||||
|
directions: List[float]
|
||||||
|
total_length: float
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Pure Python Calculation Functions
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_pi_geometry(pis: List[Tuple[float, float]], start_station: float = 0.0) -> PIGeometryResult:
|
||||||
|
"""Calculate lengths, stations, and directions for a list of PI points.
|
||||||
|
|
||||||
|
This is a pure Python function with no Blender dependencies.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
pis: List of (x, y) coordinate tuples for each PI
|
||||||
|
start_station: Starting station value
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
PIGeometryResult containing calculated values
|
||||||
|
"""
|
||||||
|
if len(pis) < 2:
|
||||||
|
return PIGeometryResult(
|
||||||
|
stations=[start_station] if pis else [],
|
||||||
|
lengths=[0.0] if pis else [],
|
||||||
|
directions=[0.0] if pis else [],
|
||||||
|
total_length=0.0,
|
||||||
|
)
|
||||||
|
|
||||||
|
stations = []
|
||||||
|
lengths = []
|
||||||
|
directions = []
|
||||||
|
cumulative_length = start_station
|
||||||
|
|
||||||
|
for i, pi in enumerate(pis):
|
||||||
|
stations.append(cumulative_length)
|
||||||
|
|
||||||
|
if i < len(pis) - 1:
|
||||||
|
next_pi = pis[i + 1]
|
||||||
|
dx = next_pi[0] - pi[0]
|
||||||
|
dy = next_pi[1] - pi[1]
|
||||||
|
length = math.sqrt(dx * dx + dy * dy)
|
||||||
|
direction = math.atan2(dy, dx)
|
||||||
|
lengths.append(length)
|
||||||
|
directions.append(direction)
|
||||||
|
cumulative_length += length
|
||||||
|
else:
|
||||||
|
lengths.append(0.0)
|
||||||
|
directions.append(0.0)
|
||||||
|
|
||||||
|
total_length = cumulative_length - start_station
|
||||||
|
|
||||||
|
return PIGeometryResult(stations=stations, lengths=lengths, directions=directions, total_length=total_length)
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_deflection_angle(incoming_direction: float, outgoing_direction: float) -> float:
|
||||||
|
"""Calculate the deflection angle between two tangent directions.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
incoming_direction: Direction angle of incoming tangent (radians)
|
||||||
|
outgoing_direction: Direction angle of outgoing tangent (radians)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Deflection angle in radians (always positive)
|
||||||
|
"""
|
||||||
|
delta = outgoing_direction - incoming_direction
|
||||||
|
# Normalize to -pi to pi
|
||||||
|
while delta > math.pi:
|
||||||
|
delta -= 2 * math.pi
|
||||||
|
while delta < -math.pi:
|
||||||
|
delta += 2 * math.pi
|
||||||
|
return abs(delta)
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_tangent_length(radius: float, deflection_angle: float) -> float:
|
||||||
|
"""Calculate tangent length for a circular curve.
|
||||||
|
|
||||||
|
T = R * tan(Δ/2)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
radius: Curve radius
|
||||||
|
deflection_angle: Deflection angle in radians
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tangent length
|
||||||
|
"""
|
||||||
|
if deflection_angle == 0 or radius == 0:
|
||||||
|
return 0.0
|
||||||
|
return radius * math.tan(deflection_angle / 2)
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_arc_length(radius: float, deflection_angle: float) -> float:
|
||||||
|
"""Calculate arc length for a circular curve.
|
||||||
|
|
||||||
|
L = R * Δ
|
||||||
|
|
||||||
|
Args:
|
||||||
|
radius: Curve radius
|
||||||
|
deflection_angle: Deflection angle in radians
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Arc length
|
||||||
|
"""
|
||||||
|
return radius * deflection_angle
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_bc_ec_points(
|
||||||
|
pi_x: float, pi_y: float, incoming_direction: float, outgoing_direction: float, tangent_length: float
|
||||||
|
) -> Tuple[Tuple[float, float], Tuple[float, float]]:
|
||||||
|
"""Calculate Begin Curve (BC) and End Curve (EC) points.
|
||||||
|
|
||||||
|
BC = PI - incoming_tangent_vector * T
|
||||||
|
EC = PI + outgoing_tangent_vector * T
|
||||||
|
|
||||||
|
Args:
|
||||||
|
pi_x: PI X coordinate
|
||||||
|
pi_y: PI Y coordinate
|
||||||
|
incoming_direction: Direction of incoming tangent (radians)
|
||||||
|
outgoing_direction: Direction of outgoing tangent (radians)
|
||||||
|
tangent_length: Calculated tangent length
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (BC point, EC point) as (x, y) tuples
|
||||||
|
"""
|
||||||
|
# BC is along the incoming tangent, before the PI
|
||||||
|
bc_x = pi_x - tangent_length * math.cos(incoming_direction)
|
||||||
|
bc_y = pi_y - tangent_length * math.sin(incoming_direction)
|
||||||
|
|
||||||
|
# EC is along the outgoing tangent, after the PI
|
||||||
|
ec_x = pi_x + tangent_length * math.cos(outgoing_direction)
|
||||||
|
ec_y = pi_y + tangent_length * math.sin(outgoing_direction)
|
||||||
|
|
||||||
|
return ((bc_x, bc_y), (ec_x, ec_y))
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Alignment Visualization Logic (Pure Python)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
def create_alignment_hierarchy(
|
||||||
|
ifc_tool: type[tool.Ifc],
|
||||||
|
alignment_tool: type[tool.Alignment],
|
||||||
|
alignment: ifcopenshell.entity_instance,
|
||||||
|
) -> object:
|
||||||
|
"""Create the Blender object hierarchy for an IFC alignment.
|
||||||
|
|
||||||
|
This is a core function that orchestrates the creation process
|
||||||
|
by calling tool methods. It contains the business logic but
|
||||||
|
delegates actual Blender operations to the tool layer.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ifc_tool: The IFC tool class for IFC operations
|
||||||
|
alignment_tool: The Alignment tool class for Blender operations
|
||||||
|
alignment: The IFC alignment entity
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The root Blender object for the alignment
|
||||||
|
"""
|
||||||
|
# Create the alignment object
|
||||||
|
alignment_obj = alignment_tool.create_object_for_alignment(alignment)
|
||||||
|
if not alignment_obj:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Get nested layouts via IfcRelNests
|
||||||
|
layouts = []
|
||||||
|
for rel in getattr(alignment, "IsNestedBy", []) or []:
|
||||||
|
for obj in rel.RelatedObjects or []:
|
||||||
|
if obj.is_a() in ("IfcAlignmentHorizontal", "IfcAlignmentVertical", "IfcAlignmentCant"):
|
||||||
|
layouts.append(obj)
|
||||||
|
|
||||||
|
# Create Blender objects for each layout and its segments
|
||||||
|
for layout in layouts:
|
||||||
|
layout_obj = alignment_tool.create_object_for_layout(layout, alignment_obj)
|
||||||
|
if layout_obj:
|
||||||
|
create_layout_segment_objects(alignment_tool, layout, layout_obj)
|
||||||
|
|
||||||
|
return alignment_obj
|
||||||
|
|
||||||
|
|
||||||
|
def create_layout_segment_objects(
|
||||||
|
alignment_tool: type[tool.Alignment],
|
||||||
|
layout: ifcopenshell.entity_instance,
|
||||||
|
layout_obj: object,
|
||||||
|
) -> list:
|
||||||
|
"""Create Blender objects for all segments in a layout.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
alignment_tool: The Alignment tool class
|
||||||
|
layout: The IFC layout entity
|
||||||
|
layout_obj: The parent Blender object
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of created segment Blender objects
|
||||||
|
"""
|
||||||
|
segment_objs = []
|
||||||
|
|
||||||
|
for rel in getattr(layout, "IsNestedBy", []) or []:
|
||||||
|
for i, segment in enumerate(rel.RelatedObjects or []):
|
||||||
|
if segment.is_a() == "IfcAlignmentSegment":
|
||||||
|
seg_obj = alignment_tool.create_object_for_segment(segment, i, layout_obj)
|
||||||
|
if seg_obj:
|
||||||
|
segment_objs.append(seg_obj)
|
||||||
|
|
||||||
|
return segment_objs
|
||||||
@@ -0,0 +1,780 @@
|
|||||||
|
# ==============================================================================
|
||||||
|
# Saikei Civil - Civil Engineering Tools for Blender
|
||||||
|
# Copyright (c) 2025 Michael Yoder / Desert Springs Civil Engineering PLLC
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
# You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
# Primary Author: Michael Yoder
|
||||||
|
# Company: Desert Springs Civil Engineering PLLC
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
"""Alignment Tool - Blender implementations for alignment visualization.
|
||||||
|
|
||||||
|
This module contains Blender-specific code for creating and managing
|
||||||
|
alignment objects in the 3D view. It bridges the core business logic
|
||||||
|
to the Blender environment.
|
||||||
|
|
||||||
|
All methods are classmethods following Bonsai's tool pattern.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
import bpy
|
||||||
|
from typing import TYPE_CHECKING, Optional, List
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
import ifcopenshell
|
||||||
|
|
||||||
|
|
||||||
|
class Alignment:
|
||||||
|
"""Tool class for alignment-related Blender operations.
|
||||||
|
|
||||||
|
Following Bonsai's tool pattern, all methods are classmethods
|
||||||
|
that can be called without instantiation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_ifc_file(cls) -> Optional[ifcopenshell.file]:
|
||||||
|
"""Get the current IFC file from Bonsai.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The IFC file object, or None if not available
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
import bonsai.tool as tool
|
||||||
|
|
||||||
|
return tool.Ifc.get()
|
||||||
|
except (ImportError, AttributeError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_object_for_alignment(cls, alignment: ifcopenshell.entity_instance) -> Optional[bpy.types.Object]:
|
||||||
|
"""Create a Blender object for an IFC alignment and link it properly.
|
||||||
|
|
||||||
|
This follows Bonsai's pattern for creating Blender representations:
|
||||||
|
1. Create a Blender Empty object
|
||||||
|
2. Link it to the IFC element via tool.Ifc.link()
|
||||||
|
3. Assign it to the appropriate collection via tool.Collector.assign()
|
||||||
|
|
||||||
|
Args:
|
||||||
|
alignment: The IFC alignment entity
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The created Blender object, or existing one if already linked
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
import bonsai.tool as tool
|
||||||
|
|
||||||
|
# Check if a Blender object already exists for this IFC element
|
||||||
|
existing_obj = tool.Ifc.get_object(alignment)
|
||||||
|
if existing_obj:
|
||||||
|
return existing_obj
|
||||||
|
|
||||||
|
# Create Blender Empty object with naming pattern "IfcClass/Name"
|
||||||
|
name = f"IfcAlignment/{alignment.Name or 'Unnamed'}"
|
||||||
|
obj = bpy.data.objects.new(name, None) # None = Empty object
|
||||||
|
obj.empty_display_type = "ARROWS"
|
||||||
|
obj.empty_display_size = 1.0
|
||||||
|
|
||||||
|
# Link the Blender object to the IFC element (creates bidirectional mapping)
|
||||||
|
tool.Ifc.link(alignment, obj)
|
||||||
|
|
||||||
|
# Also set ifc_definition_id manually as fallback for lookups
|
||||||
|
obj["ifc_definition_id"] = alignment.id()
|
||||||
|
|
||||||
|
# Assign to appropriate collection (Bonsai handles collection hierarchy)
|
||||||
|
tool.Collector.assign(obj)
|
||||||
|
|
||||||
|
return obj
|
||||||
|
except (ImportError, AttributeError) as e:
|
||||||
|
print(f"Warning: Could not create Blender object for alignment: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_object_for_layout(
|
||||||
|
cls, layout_entity: ifcopenshell.entity_instance, parent_obj: Optional[bpy.types.Object] = None
|
||||||
|
) -> Optional[bpy.types.Object]:
|
||||||
|
"""Create a Blender object for an IFC alignment layout.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
layout_entity: The IFC layout entity (IfcAlignmentHorizontal, etc.)
|
||||||
|
parent_obj: The parent Blender object (IfcAlignment object)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The created Blender object, or existing one if already linked
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
import bonsai.tool as tool
|
||||||
|
|
||||||
|
# Check if a Blender object already exists for this IFC element
|
||||||
|
existing_obj = tool.Ifc.get_object(layout_entity)
|
||||||
|
if existing_obj:
|
||||||
|
return existing_obj
|
||||||
|
|
||||||
|
# Determine the layout type from the IFC class
|
||||||
|
ifc_class = layout_entity.is_a()
|
||||||
|
name = f"{ifc_class}"
|
||||||
|
|
||||||
|
obj = bpy.data.objects.new(name, None)
|
||||||
|
obj.empty_display_type = "PLAIN_AXES"
|
||||||
|
obj.empty_display_size = 0.5
|
||||||
|
|
||||||
|
# Link to IFC element
|
||||||
|
tool.Ifc.link(layout_entity, obj)
|
||||||
|
|
||||||
|
# Also set ifc_definition_id manually as fallback for lookups
|
||||||
|
obj["ifc_definition_id"] = layout_entity.id()
|
||||||
|
|
||||||
|
# Set parent relationship in Blender (mirrors IFC nesting)
|
||||||
|
if parent_obj:
|
||||||
|
obj.parent = parent_obj
|
||||||
|
|
||||||
|
# Assign to same collection as parent (avoid "Unsorted")
|
||||||
|
if parent_obj and parent_obj.users_collection:
|
||||||
|
parent_obj.users_collection[0].objects.link(obj)
|
||||||
|
else:
|
||||||
|
tool.Collector.assign(obj)
|
||||||
|
|
||||||
|
return obj
|
||||||
|
except (ImportError, AttributeError) as e:
|
||||||
|
print(f"Warning: Could not create Blender object for layout: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_object_for_segment(
|
||||||
|
cls, segment: ifcopenshell.entity_instance, index: int, parent_obj: Optional[bpy.types.Object] = None
|
||||||
|
) -> Optional[bpy.types.Object]:
|
||||||
|
"""Create a Blender curve object for an IFC alignment segment.
|
||||||
|
|
||||||
|
Creates actual curve geometry (not just an empty) to visualize
|
||||||
|
the segment. LINE segments become straight curves, CIRCULARARC
|
||||||
|
segments become arcs.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
segment: The IfcAlignmentSegment entity
|
||||||
|
index: The segment index (for naming)
|
||||||
|
parent_obj: The parent Blender object (layout object)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The created Blender object, or existing one if already linked
|
||||||
|
"""
|
||||||
|
import math
|
||||||
|
|
||||||
|
try:
|
||||||
|
import bonsai.tool as tool
|
||||||
|
|
||||||
|
# Check if a Blender object already exists for this IFC element
|
||||||
|
existing_obj = tool.Ifc.get_object(segment)
|
||||||
|
if existing_obj:
|
||||||
|
return existing_obj
|
||||||
|
|
||||||
|
# Get segment parameters
|
||||||
|
if not hasattr(segment, "DesignParameters") or not segment.DesignParameters:
|
||||||
|
return None
|
||||||
|
|
||||||
|
dp = segment.DesignParameters
|
||||||
|
seg_type = getattr(dp, "PredefinedType", "UNKNOWN") or "UNKNOWN"
|
||||||
|
seg_length = getattr(dp, "SegmentLength", 0.0) or 0.0
|
||||||
|
|
||||||
|
# Skip zero-length terminal segments
|
||||||
|
if seg_length < 0.0001:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Get start point
|
||||||
|
start_point = None
|
||||||
|
if hasattr(dp, "StartPoint") and dp.StartPoint:
|
||||||
|
coords = dp.StartPoint.Coordinates
|
||||||
|
if len(coords) >= 2:
|
||||||
|
start_point = (coords[0], coords[1], 0.0)
|
||||||
|
|
||||||
|
if not start_point:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Get start direction - IFC stores this in degrees, convert to radians
|
||||||
|
start_direction_deg = getattr(dp, "StartDirection", 0.0) or 0.0
|
||||||
|
start_direction = math.radians(start_direction_deg)
|
||||||
|
|
||||||
|
name = f"Segment {index + 1} ({seg_type})"
|
||||||
|
|
||||||
|
# Create curve geometry based on segment type
|
||||||
|
if seg_type == "LINE":
|
||||||
|
obj = cls._create_line_segment(name, start_point, start_direction, seg_length)
|
||||||
|
elif seg_type == "CIRCULARARC":
|
||||||
|
# Get radius for arc (positive = left, negative = right in IFC)
|
||||||
|
radius = getattr(dp, "StartRadiusOfCurvature", None)
|
||||||
|
if radius is None or radius == 0:
|
||||||
|
# Fallback to line if no radius
|
||||||
|
obj = cls._create_line_segment(name, start_point, start_direction, seg_length)
|
||||||
|
else:
|
||||||
|
obj = cls._create_arc_segment(name, start_point, start_direction, seg_length, radius)
|
||||||
|
else:
|
||||||
|
# For unsupported types, create a simple line approximation
|
||||||
|
obj = cls._create_line_segment(name, start_point, start_direction, seg_length)
|
||||||
|
|
||||||
|
if not obj:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Link to IFC element
|
||||||
|
tool.Ifc.link(segment, obj)
|
||||||
|
|
||||||
|
# Also set ifc_definition_id manually as fallback for lookups
|
||||||
|
obj["ifc_definition_id"] = segment.id()
|
||||||
|
|
||||||
|
# Set parent relationship
|
||||||
|
if parent_obj:
|
||||||
|
obj.parent = parent_obj
|
||||||
|
|
||||||
|
# Assign to same collection as parent (avoid "Unsorted")
|
||||||
|
if parent_obj and parent_obj.users_collection:
|
||||||
|
parent_obj.users_collection[0].objects.link(obj)
|
||||||
|
else:
|
||||||
|
tool.Collector.assign(obj)
|
||||||
|
|
||||||
|
return obj
|
||||||
|
except (ImportError, AttributeError) as e:
|
||||||
|
print(f"Warning: Could not create Blender object for segment: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _create_line_segment(
|
||||||
|
cls, name: str, start_point: tuple, direction: float, length: float
|
||||||
|
) -> Optional[bpy.types.Object]:
|
||||||
|
"""Create a Blender curve for a LINE segment.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: Object name
|
||||||
|
start_point: (x, y, z) start coordinates
|
||||||
|
direction: Direction angle in radians (IFC uses bearing from North/Y-axis)
|
||||||
|
length: Segment length
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Blender curve object
|
||||||
|
"""
|
||||||
|
import math
|
||||||
|
|
||||||
|
# IFC uses standard math convention: angle counter-clockwise from +X axis
|
||||||
|
end_x = start_point[0] + length * math.cos(direction)
|
||||||
|
end_y = start_point[1] + length * math.sin(direction)
|
||||||
|
end_point = (end_x, end_y, start_point[2])
|
||||||
|
|
||||||
|
# Create curve data
|
||||||
|
curve_data = bpy.data.curves.new(name, type="CURVE")
|
||||||
|
curve_data.dimensions = "3D"
|
||||||
|
|
||||||
|
# Create a polyline spline
|
||||||
|
spline = curve_data.splines.new("POLY")
|
||||||
|
spline.points.add(1) # Start with 1 point, add 1 more = 2 total
|
||||||
|
|
||||||
|
# Set point coordinates (Blender uses 4D coords: x, y, z, w)
|
||||||
|
spline.points[0].co = (start_point[0], start_point[1], start_point[2], 1.0)
|
||||||
|
spline.points[1].co = (end_point[0], end_point[1], end_point[2], 1.0)
|
||||||
|
|
||||||
|
# Create object
|
||||||
|
obj = bpy.data.objects.new(name, curve_data)
|
||||||
|
|
||||||
|
# Set curve display properties
|
||||||
|
curve_data.bevel_depth = 0.0 # No thickness for now
|
||||||
|
obj.show_in_front = True # Always visible
|
||||||
|
|
||||||
|
return obj
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _create_arc_segment(
|
||||||
|
cls, name: str, start_point: tuple, direction: float, length: float, radius: float
|
||||||
|
) -> Optional[bpy.types.Object]:
|
||||||
|
"""Create a Blender curve for a CIRCULARARC segment.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: Object name
|
||||||
|
start_point: (x, y, z) start coordinates
|
||||||
|
direction: Start direction angle in radians (IFC uses bearing from North/Y-axis)
|
||||||
|
length: Arc length
|
||||||
|
radius: Radius of curvature (positive = curves left, negative = curves right)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Blender curve object
|
||||||
|
"""
|
||||||
|
import math
|
||||||
|
|
||||||
|
# Calculate arc parameters
|
||||||
|
# Arc length L = R * theta, so theta = L / R
|
||||||
|
abs_radius = abs(radius)
|
||||||
|
if abs_radius < 0.0001:
|
||||||
|
# Degenerate case - just make a line
|
||||||
|
return cls._create_line_segment(name, start_point, direction, length)
|
||||||
|
|
||||||
|
theta = length / abs_radius # Total angle swept
|
||||||
|
|
||||||
|
# Determine if curving left (positive radius) or right (negative radius)
|
||||||
|
curve_left = radius > 0
|
||||||
|
|
||||||
|
# Generate points along the arc
|
||||||
|
num_points = max(int(theta * 10) + 2, 8) # At least 8 points, more for larger arcs
|
||||||
|
|
||||||
|
# Create curve data
|
||||||
|
curve_data = bpy.data.curves.new(name, type="CURVE")
|
||||||
|
curve_data.dimensions = "3D"
|
||||||
|
|
||||||
|
# Create a polyline spline
|
||||||
|
spline = curve_data.splines.new("POLY")
|
||||||
|
spline.points.add(num_points - 1) # Add points (starts with 1)
|
||||||
|
|
||||||
|
# Calculate center of the arc
|
||||||
|
# Center is perpendicular to start direction at distance R
|
||||||
|
# For standard math convention (angle from +X, CCW):
|
||||||
|
# Perpendicular left = direction + 90°, perpendicular right = direction - 90°
|
||||||
|
if curve_left:
|
||||||
|
center_angle = direction + math.pi / 2
|
||||||
|
else:
|
||||||
|
center_angle = direction - math.pi / 2
|
||||||
|
|
||||||
|
center_x = start_point[0] + abs_radius * math.cos(center_angle)
|
||||||
|
center_y = start_point[1] + abs_radius * math.sin(center_angle)
|
||||||
|
|
||||||
|
# Start angle from center to start point
|
||||||
|
start_angle = math.atan2(start_point[1] - center_y, start_point[0] - center_x)
|
||||||
|
|
||||||
|
# Generate points
|
||||||
|
for i in range(num_points):
|
||||||
|
t = i / (num_points - 1) # Parameter from 0 to 1
|
||||||
|
if curve_left:
|
||||||
|
angle = start_angle + t * theta
|
||||||
|
else:
|
||||||
|
angle = start_angle - t * theta
|
||||||
|
|
||||||
|
px = center_x + abs_radius * math.cos(angle)
|
||||||
|
py = center_y + abs_radius * math.sin(angle)
|
||||||
|
pz = start_point[2]
|
||||||
|
|
||||||
|
spline.points[i].co = (px, py, pz, 1.0)
|
||||||
|
|
||||||
|
# Create object
|
||||||
|
obj = bpy.data.objects.new(name, curve_data)
|
||||||
|
|
||||||
|
# Set curve display properties
|
||||||
|
curve_data.bevel_depth = 0.0
|
||||||
|
obj.show_in_front = True
|
||||||
|
|
||||||
|
return obj
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_hierarchy_for_alignment(cls, alignment: ifcopenshell.entity_instance) -> Optional[bpy.types.Object]:
|
||||||
|
"""Create the full Blender object hierarchy for an alignment.
|
||||||
|
|
||||||
|
Creates:
|
||||||
|
- IfcAlignment object (root)
|
||||||
|
- IfcAlignmentHorizontal object (child)
|
||||||
|
- IfcAlignmentVertical object (child, if present)
|
||||||
|
- IfcAlignmentCant object (child, if present)
|
||||||
|
- Segment objects under each layout
|
||||||
|
|
||||||
|
Args:
|
||||||
|
alignment: The IFC alignment entity
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The root alignment Blender object
|
||||||
|
"""
|
||||||
|
# Create the alignment object
|
||||||
|
alignment_obj = cls.create_object_for_alignment(alignment)
|
||||||
|
if not alignment_obj:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Get nested layouts via IfcRelNests
|
||||||
|
layouts = []
|
||||||
|
for rel in getattr(alignment, "IsNestedBy", []) or []:
|
||||||
|
for obj in rel.RelatedObjects or []:
|
||||||
|
if obj.is_a() in ("IfcAlignmentHorizontal", "IfcAlignmentVertical", "IfcAlignmentCant"):
|
||||||
|
layouts.append(obj)
|
||||||
|
|
||||||
|
# Create Blender objects for each layout and its segments
|
||||||
|
for layout in layouts:
|
||||||
|
layout_obj = cls.create_object_for_layout(layout, alignment_obj)
|
||||||
|
if layout_obj:
|
||||||
|
cls.create_objects_for_layout_segments(layout, layout_obj)
|
||||||
|
|
||||||
|
return alignment_obj
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_objects_for_layout_segments(
|
||||||
|
cls, layout: ifcopenshell.entity_instance, layout_obj: bpy.types.Object
|
||||||
|
) -> List[bpy.types.Object]:
|
||||||
|
"""Create Blender objects for all segments in a layout.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
layout: The IFC layout entity (IfcAlignmentHorizontal, etc.)
|
||||||
|
layout_obj: The parent Blender object for the layout
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of created segment Blender objects
|
||||||
|
"""
|
||||||
|
segment_objs = []
|
||||||
|
|
||||||
|
# Get segments via IfcRelNests
|
||||||
|
for rel in getattr(layout, "IsNestedBy", []) or []:
|
||||||
|
for i, segment in enumerate(rel.RelatedObjects or []):
|
||||||
|
if segment.is_a() == "IfcAlignmentSegment":
|
||||||
|
seg_obj = cls.create_object_for_segment(segment, i, layout_obj)
|
||||||
|
if seg_obj:
|
||||||
|
segment_objs.append(seg_obj)
|
||||||
|
|
||||||
|
return segment_objs
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def update_pi_properties(cls, props, geometry_result) -> None:
|
||||||
|
"""Update Blender PropertyGroup with calculated geometry.
|
||||||
|
|
||||||
|
This bridges the pure Python calculation results back to
|
||||||
|
the Blender UI properties.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
props: The SaikeiAlignmentProperties PropertyGroup
|
||||||
|
geometry_result: PIGeometryResult from core.alignment
|
||||||
|
"""
|
||||||
|
pis = props.pis
|
||||||
|
for i, pi in enumerate(pis):
|
||||||
|
if i < len(geometry_result.stations):
|
||||||
|
pi.station = geometry_result.stations[i]
|
||||||
|
if i < len(geometry_result.lengths):
|
||||||
|
pi.length_to_next = geometry_result.lengths[i]
|
||||||
|
if i < len(geometry_result.directions):
|
||||||
|
pi.direction_to_next = geometry_result.directions[i]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _remove_blender_object(cls, obj: bpy.types.Object) -> bool:
|
||||||
|
"""Safely remove a Blender object and its data.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
obj: The Blender object to remove
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if removed successfully
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
import bonsai.tool as tool
|
||||||
|
|
||||||
|
# Unlink from IFC if linked
|
||||||
|
try:
|
||||||
|
tool.Ifc.unlink(obj)
|
||||||
|
except Exception:
|
||||||
|
pass # Object might not be linked
|
||||||
|
|
||||||
|
# Store data reference before removing object
|
||||||
|
data = obj.data
|
||||||
|
|
||||||
|
# Remove the object
|
||||||
|
bpy.data.objects.remove(obj, do_unlink=True)
|
||||||
|
|
||||||
|
# Clean up orphan curve/mesh data
|
||||||
|
if data and data.users == 0:
|
||||||
|
if isinstance(data, bpy.types.Curve):
|
||||||
|
bpy.data.curves.remove(data)
|
||||||
|
elif isinstance(data, bpy.types.Mesh):
|
||||||
|
bpy.data.meshes.remove(data)
|
||||||
|
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Warning: Could not remove object: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _find_object_by_ifc_id(cls, ifc_id: int) -> Optional[bpy.types.Object]:
|
||||||
|
"""Find a Blender object by its IFC definition ID.
|
||||||
|
|
||||||
|
Fallback method when tool.Ifc.get_object() doesn't work.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ifc_id: The IFC entity ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The Blender object, or None if not found
|
||||||
|
"""
|
||||||
|
for obj in bpy.data.objects:
|
||||||
|
if obj.get("ifc_definition_id") == ifc_id:
|
||||||
|
return obj
|
||||||
|
return None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _find_object_by_name_pattern(cls, name_pattern: str) -> Optional[bpy.types.Object]:
|
||||||
|
"""Find a Blender object by name pattern.
|
||||||
|
|
||||||
|
Last resort fallback that matches object name.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name_pattern: Name or partial name to match
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The Blender object, or None if not found
|
||||||
|
"""
|
||||||
|
# Try exact match first
|
||||||
|
if name_pattern in bpy.data.objects:
|
||||||
|
return bpy.data.objects[name_pattern]
|
||||||
|
|
||||||
|
# Try partial match (for names like "IfcAlignment/SH-21")
|
||||||
|
for obj in bpy.data.objects:
|
||||||
|
if name_pattern in obj.name:
|
||||||
|
return obj
|
||||||
|
return None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def remove_layout_segment_objects(cls, layout: ifcopenshell.entity_instance) -> int:
|
||||||
|
"""Remove all Blender objects for segments in a layout.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
layout: The IFC layout entity (IfcAlignmentHorizontal, etc.)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Number of objects removed
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
import bonsai.tool as tool
|
||||||
|
except ImportError:
|
||||||
|
tool = None
|
||||||
|
|
||||||
|
removed_count = 0
|
||||||
|
|
||||||
|
# Get segments via IfcRelNests
|
||||||
|
for rel in getattr(layout, "IsNestedBy", []) or []:
|
||||||
|
for segment in rel.RelatedObjects or []:
|
||||||
|
if segment.is_a() == "IfcAlignmentSegment":
|
||||||
|
obj = None
|
||||||
|
|
||||||
|
# Try to get object via Bonsai's tool
|
||||||
|
if tool:
|
||||||
|
try:
|
||||||
|
obj = tool.Ifc.get_object(segment)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Fallback: search by IFC ID
|
||||||
|
if not obj:
|
||||||
|
obj = cls._find_object_by_ifc_id(segment.id())
|
||||||
|
|
||||||
|
if obj and cls._remove_blender_object(obj):
|
||||||
|
removed_count += 1
|
||||||
|
|
||||||
|
return removed_count
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def remove_alignment_hierarchy(cls, alignment: ifcopenshell.entity_instance) -> int:
|
||||||
|
"""Remove all Blender objects for an alignment and its children.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
alignment: The IFC alignment entity
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Number of objects removed
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
import bonsai.tool as tool
|
||||||
|
except ImportError:
|
||||||
|
tool = None
|
||||||
|
|
||||||
|
removed_count = 0
|
||||||
|
alignment_name = alignment.Name or "Unnamed"
|
||||||
|
|
||||||
|
# Get nested layouts via IfcRelNests
|
||||||
|
for rel in getattr(alignment, "IsNestedBy", []) or []:
|
||||||
|
for layout in rel.RelatedObjects or []:
|
||||||
|
if layout.is_a() in ("IfcAlignmentHorizontal", "IfcAlignmentVertical", "IfcAlignmentCant"):
|
||||||
|
# Remove segment objects first
|
||||||
|
removed_count += cls.remove_layout_segment_objects(layout)
|
||||||
|
|
||||||
|
# Try to get layout object via Bonsai's tool
|
||||||
|
layout_obj = None
|
||||||
|
if tool:
|
||||||
|
try:
|
||||||
|
layout_obj = tool.Ifc.get_object(layout)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Fallback: search by IFC ID
|
||||||
|
if not layout_obj:
|
||||||
|
layout_obj = cls._find_object_by_ifc_id(layout.id())
|
||||||
|
|
||||||
|
# Last resort: search by name pattern
|
||||||
|
if not layout_obj:
|
||||||
|
layout_obj = cls._find_object_by_name_pattern(layout.is_a())
|
||||||
|
|
||||||
|
if layout_obj and cls._remove_blender_object(layout_obj):
|
||||||
|
removed_count += 1
|
||||||
|
|
||||||
|
# Try to get alignment object via Bonsai's tool
|
||||||
|
alignment_obj = None
|
||||||
|
if tool:
|
||||||
|
try:
|
||||||
|
alignment_obj = tool.Ifc.get_object(alignment)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Fallback: search by IFC ID
|
||||||
|
if not alignment_obj:
|
||||||
|
alignment_obj = cls._find_object_by_ifc_id(alignment.id())
|
||||||
|
|
||||||
|
# Last resort: search by name pattern (IfcAlignment/Name)
|
||||||
|
if not alignment_obj:
|
||||||
|
alignment_obj = cls._find_object_by_name_pattern(f"IfcAlignment/{alignment_name}")
|
||||||
|
|
||||||
|
if alignment_obj and cls._remove_blender_object(alignment_obj):
|
||||||
|
removed_count += 1
|
||||||
|
|
||||||
|
return removed_count
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def refresh_layout_visualization(
|
||||||
|
cls, layout: ifcopenshell.entity_instance, layout_obj: Optional[bpy.types.Object] = None
|
||||||
|
) -> List[bpy.types.Object]:
|
||||||
|
"""Refresh the visualization for a layout by removing and recreating segment objects.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
layout: The IFC layout entity
|
||||||
|
layout_obj: Optional parent Blender object (will be looked up if not provided)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of newly created segment objects
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
import bonsai.tool as tool
|
||||||
|
|
||||||
|
# Get or find the layout object
|
||||||
|
if layout_obj is None:
|
||||||
|
layout_obj = tool.Ifc.get_object(layout)
|
||||||
|
|
||||||
|
if layout_obj is None:
|
||||||
|
return []
|
||||||
|
|
||||||
|
# Remove existing segment objects
|
||||||
|
cls.remove_layout_segment_objects(layout)
|
||||||
|
|
||||||
|
# Create new segment objects
|
||||||
|
return cls.create_objects_for_layout_segments(layout, layout_obj)
|
||||||
|
except (ImportError, AttributeError) as e:
|
||||||
|
print(f"Warning: Could not refresh layout visualization: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
|
# Validation and Safe Wrappers
|
||||||
|
# =========================================================================
|
||||||
|
# These methods provide pre-validation before calling IfcOpenShell alignment
|
||||||
|
# API functions. This prevents issues like orphan layouts (from undo/redo)
|
||||||
|
# causing invalid IFC entities (e.g., IfcRelPositions with empty RelatedProducts).
|
||||||
|
#
|
||||||
|
# The key principle: validate BEFORE operations to prevent invalid data,
|
||||||
|
# rather than cleaning up after the fact.
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def validate_layout_has_parent_alignment(
|
||||||
|
cls, layout: "ifcopenshell.entity_instance"
|
||||||
|
) -> Optional["ifcopenshell.entity_instance"]:
|
||||||
|
"""Check if a layout entity has a valid parent IfcAlignment.
|
||||||
|
|
||||||
|
Orphan layouts (e.g., from undo/redo operations) can cause issues
|
||||||
|
when the alignment API tries to create referents, as the code
|
||||||
|
expects a parent alignment to exist.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
layout: The IFC layout entity (IfcAlignmentHorizontal, etc.)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The parent IfcAlignment if found, None otherwise
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
import ifcopenshell.api.alignment as align_api
|
||||||
|
|
||||||
|
return align_api.get_alignment(layout)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_alignment_for_layout(
|
||||||
|
cls, layout: "ifcopenshell.entity_instance"
|
||||||
|
) -> Optional["ifcopenshell.entity_instance"]:
|
||||||
|
"""Get the parent IfcAlignment for a layout entity.
|
||||||
|
|
||||||
|
This is an alias for validate_layout_has_parent_alignment that
|
||||||
|
makes the intent clearer when you need the alignment itself.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
layout: The IFC layout entity (IfcAlignmentHorizontal, etc.)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The parent IfcAlignment if found, None otherwise
|
||||||
|
"""
|
||||||
|
return cls.validate_layout_has_parent_alignment(layout)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def safe_layout_horizontal_by_pi_method(
|
||||||
|
cls, ifc_file: "ifcopenshell.file", layout: "ifcopenshell.entity_instance", hpoints: list, radii: list
|
||||||
|
) -> bool:
|
||||||
|
"""Safely add segments to a horizontal layout using PI method.
|
||||||
|
|
||||||
|
This wrapper validates that the layout has a valid parent alignment
|
||||||
|
before calling the IfcOpenShell API. This prevents the creation of
|
||||||
|
invalid IfcRelPositions entities.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ifc_file: The IFC file
|
||||||
|
layout: The IfcAlignmentHorizontal layout
|
||||||
|
hpoints: List of (X, Y) coordinate pairs for PIs
|
||||||
|
radii: List of curve radii
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if successful
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If layout has no parent alignment
|
||||||
|
"""
|
||||||
|
import ifcopenshell.api.alignment as align_api
|
||||||
|
|
||||||
|
# Validate layout has a parent alignment - this is the key check
|
||||||
|
# that prevents orphan stationing from being created
|
||||||
|
alignment = cls.validate_layout_has_parent_alignment(layout)
|
||||||
|
if alignment is None:
|
||||||
|
raise ValueError(
|
||||||
|
f"Layout #{layout.id()} ({layout.is_a()}) has no parent IfcAlignment. "
|
||||||
|
"This may be an orphan layout from undo/redo. "
|
||||||
|
"Cannot add segments without a valid parent alignment."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Now safe to call the API - stationing will be associated with alignment
|
||||||
|
align_api.layout_horizontal_alignment_by_pi_method(ifc_file, layout, hpoints, radii)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def safe_create_alignment_by_pi_method(
|
||||||
|
cls, ifc_file: "ifcopenshell.file", name: str, hpoints: list, radii: list, start_station: float = 0.0
|
||||||
|
) -> "ifcopenshell.entity_instance":
|
||||||
|
"""Safely create a new alignment using PI method.
|
||||||
|
|
||||||
|
When creating a new alignment, we don't need validation since
|
||||||
|
we're creating the alignment itself - stationing will be
|
||||||
|
properly associated with it.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ifc_file: The IFC file
|
||||||
|
name: Alignment name
|
||||||
|
hpoints: List of (X, Y) coordinate pairs for PIs
|
||||||
|
radii: List of curve radii
|
||||||
|
start_station: Starting station value
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The created IfcAlignment entity
|
||||||
|
"""
|
||||||
|
import ifcopenshell.api.alignment as align_api
|
||||||
|
|
||||||
|
# Create the alignment - this creates a new alignment so stationing
|
||||||
|
# will be properly associated with it
|
||||||
|
alignment = align_api.create_by_pi_method(
|
||||||
|
ifc_file, name=name, hpoints=hpoints, radii=radii, start_station=start_station
|
||||||
|
)
|
||||||
|
|
||||||
|
return alignment
|
||||||
Reference in New Issue
Block a user