mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-10 17:58:20 +00:00
Refactor Saikei alignment module to follow Bonsai architecture
- Move math/calculation functions from core to tool layer (calculate_pi_geometry, calculate_deflection_angle, etc.) - Remove duplicate get_ifc_file() wrappers, use tool.Ifc.get() directly - Remove redundant ifc_definition_id manual settings (tool.Ifc.link handles this) - Remove fallback object lookup methods (_find_object_by_ifc_id, _find_object_by_name_pattern) - Simplify remove methods to use tool.Ifc.get_object() directly - Clean up defensive try/except ImportError blocks - Update is_ifc4x3() to use tool.Ifc.get_schema() - Update license headers to Bonsai standard Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
committed by
Dion Moult
parent
8416ca46a7
commit
04ad0b92bd
@@ -1,21 +1,20 @@
|
||||
# ==============================================================================
|
||||
# Saikei Civil - Civil Engineering Tools for Blender
|
||||
# Copyright (c) 2025 Michael Yoder / Desert Springs Civil Engineering PLLC
|
||||
# Bonsai - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2025, 2026 Michael Yoder <myoder@desertspringscivil.com>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# This file is part of Bonsai.
|
||||
#
|
||||
# Bonsai 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,
|
||||
# Bonsai 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
|
||||
# ==============================================================================
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
"""Data caching layer for the alignment module
|
||||
@@ -24,15 +23,7 @@ This module provides cached access to alignment data for UI display,
|
||||
following Bonsai's data loading pattern.
|
||||
"""
|
||||
|
||||
|
||||
def get_ifc_file():
|
||||
"""Get the current IFC file from Bonsai"""
|
||||
try:
|
||||
import bonsai.tool as tool
|
||||
|
||||
return tool.Ifc.get()
|
||||
except (ImportError, AttributeError):
|
||||
return None
|
||||
import bonsai.tool as tool
|
||||
|
||||
|
||||
class AlignmentData:
|
||||
@@ -50,7 +41,7 @@ class AlignmentData:
|
||||
"segments": [],
|
||||
}
|
||||
|
||||
ifc = get_ifc_file()
|
||||
ifc = tool.Ifc.get()
|
||||
if ifc is None:
|
||||
cls.is_loaded = True
|
||||
return
|
||||
|
||||
@@ -593,17 +593,17 @@ def on_radius_changed(pi, context):
|
||||
|
||||
|
||||
def recalculate_pi_geometry(props):
|
||||
"""Recalculate lengths and stations for all PIs using core logic."""
|
||||
"""Recalculate lengths and stations for all PIs using tool layer."""
|
||||
pis = props.pis
|
||||
if len(pis) < 2:
|
||||
rebuild_display_rows(props)
|
||||
return
|
||||
|
||||
# Extract PI coordinates for pure Python calculation
|
||||
# Extract PI coordinates for calculation
|
||||
pi_coords = [(pi.x, pi.y) for pi in pis]
|
||||
|
||||
# Use core function for calculation
|
||||
result = core.calculate_pi_geometry(pi_coords, props.start_station)
|
||||
# Use tool layer for calculation (math belongs in tool, not core)
|
||||
result = tool.Alignment.calculate_pi_geometry(pi_coords, props.start_station)
|
||||
|
||||
# Update Blender properties with results
|
||||
tool.Alignment.update_pi_properties(props, result)
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
# ==============================================================================
|
||||
# Saikei Civil - Civil Engineering Tools for Blender
|
||||
# Copyright (c) 2025 Michael Yoder / Desert Springs Civil Engineering PLLC
|
||||
# Bonsai - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2025, 2026 Michael Yoder <myoder@desertspringscivil.com>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# This file is part of Bonsai.
|
||||
#
|
||||
# Bonsai 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,
|
||||
# Bonsai 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
|
||||
# ==============================================================================
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
"""Property groups for the alignment module"""
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
# ==============================================================================
|
||||
# Saikei Civil - Civil Engineering Tools for Blender
|
||||
# Copyright (c) 2025 Michael Yoder / Desert Springs Civil Engineering PLLC
|
||||
# Bonsai - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2025, 2026 Michael Yoder <myoder@desertspringscivil.com>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# This file is part of Bonsai.
|
||||
#
|
||||
# Bonsai 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,
|
||||
# Bonsai 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
|
||||
# ==============================================================================
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
"""UI panels for the alignment module
|
||||
@@ -24,23 +23,13 @@ All panels appear in the VIEW_3D N-panel under the "Saikei Civil" tab.
|
||||
"""
|
||||
|
||||
import bpy
|
||||
import bonsai.tool as tool
|
||||
from bpy.types import Panel, UIList
|
||||
|
||||
|
||||
def get_ifc_file():
|
||||
"""Get the current IFC file from Bonsai"""
|
||||
try:
|
||||
import bonsai.tool as tool
|
||||
|
||||
return tool.Ifc.get()
|
||||
except (ImportError, AttributeError):
|
||||
return None
|
||||
|
||||
|
||||
def is_ifc4x3():
|
||||
"""Check if the current IFC file is IFC4X3 schema"""
|
||||
ifc = get_ifc_file()
|
||||
return ifc is not None and ifc.schema == "IFC4X3"
|
||||
return tool.Ifc.get_schema() == "IFC4X3"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
@@ -148,7 +137,7 @@ class SAIKEI_PT_horizontal_alignment(Panel):
|
||||
|
||||
# Status box
|
||||
box = layout.box()
|
||||
ifc = get_ifc_file()
|
||||
ifc = tool.Ifc.get()
|
||||
|
||||
if ifc is None:
|
||||
box.label(text="No IFC file loaded", icon="ERROR")
|
||||
|
||||
@@ -1,33 +1,38 @@
|
||||
# ==============================================================================
|
||||
# Saikei Civil - Civil Engineering Tools for Blender
|
||||
# Copyright (c) 2025 Michael Yoder / Desert Springs Civil Engineering PLLC
|
||||
# Bonsai - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2025, 2026 Michael Yoder <myoder@desertspringscivil.com>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# This file is part of Bonsai.
|
||||
#
|
||||
# Bonsai 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,
|
||||
# Bonsai 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
|
||||
# ==============================================================================
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
"""Core alignment business logic - Pure Python, NO bpy imports.
|
||||
"""Core alignment business logic - Orchestration only, 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.
|
||||
This module contains alignment-related business logic and workflow
|
||||
orchestration. All calculations and algorithms are in the tool layer.
|
||||
Functions receive tool classes as parameters following Bonsai's
|
||||
dependency injection pattern.
|
||||
|
||||
NOTE: Math, calculations, and algorithms belong in tool/alignment.py.
|
||||
This module only handles:
|
||||
- Business rules and validation
|
||||
- Workflow orchestration (calling tool methods in sequence)
|
||||
- Decision-making about what should happen
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
import math
|
||||
from typing import TYPE_CHECKING, List, Tuple, Optional
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from dataclasses import dataclass
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -57,149 +62,8 @@ class PIPoint:
|
||||
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)
|
||||
# Alignment Visualization Logic (Business Logic Orchestration)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
|
||||
+290
-281
@@ -1,21 +1,20 @@
|
||||
# ==============================================================================
|
||||
# Saikei Civil - Civil Engineering Tools for Blender
|
||||
# Copyright (c) 2025 Michael Yoder / Desert Springs Civil Engineering PLLC
|
||||
# Bonsai - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2025, 2026 Michael Yoder <myoder@desertspringscivil.com>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# This file is part of Bonsai.
|
||||
#
|
||||
# Bonsai 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,
|
||||
# Bonsai 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
|
||||
# ==============================================================================
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
"""Alignment Tool - Blender implementations for alignment visualization.
|
||||
@@ -29,12 +28,30 @@ All methods are classmethods following Bonsai's tool pattern.
|
||||
|
||||
from __future__ import annotations
|
||||
import bpy
|
||||
from typing import TYPE_CHECKING, Optional, List
|
||||
import math
|
||||
import bonsai.tool as tool
|
||||
from typing import TYPE_CHECKING, Optional, List, Tuple
|
||||
from dataclasses import dataclass
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import ifcopenshell
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Data Classes for PI Geometry Results
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@dataclass
|
||||
class PIGeometryResult:
|
||||
"""Result of PI geometry calculation."""
|
||||
|
||||
stations: List[float]
|
||||
lengths: List[float]
|
||||
directions: List[float]
|
||||
total_length: float
|
||||
|
||||
|
||||
class Alignment:
|
||||
"""Tool class for alignment-related Blender operations.
|
||||
|
||||
@@ -42,19 +59,146 @@ class Alignment:
|
||||
that can be called without instantiation.
|
||||
"""
|
||||
|
||||
# =========================================================================
|
||||
# Geometry Calculation Methods
|
||||
# =========================================================================
|
||||
|
||||
@classmethod
|
||||
def get_ifc_file(cls) -> Optional[ifcopenshell.file]:
|
||||
"""Get the current IFC file from Bonsai.
|
||||
def calculate_pi_geometry(
|
||||
cls, pis: List[Tuple[float, float]], start_station: float = 0.0
|
||||
) -> PIGeometryResult:
|
||||
"""Calculate lengths, stations, and directions for a list of PI points.
|
||||
|
||||
Args:
|
||||
pis: List of (x, y) coordinate tuples for each PI
|
||||
start_station: Starting station value
|
||||
|
||||
Returns:
|
||||
The IFC file object, or None if not available
|
||||
PIGeometryResult containing calculated values
|
||||
"""
|
||||
try:
|
||||
import bonsai.tool as tool
|
||||
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,
|
||||
)
|
||||
|
||||
return tool.Ifc.get()
|
||||
except (ImportError, AttributeError):
|
||||
return None
|
||||
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
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def calculate_deflection_angle(cls, 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)
|
||||
|
||||
@classmethod
|
||||
def calculate_tangent_length(cls, 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)
|
||||
|
||||
@classmethod
|
||||
def calculate_arc_length(cls, 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
|
||||
|
||||
@classmethod
|
||||
def calculate_bc_ec_points(
|
||||
cls,
|
||||
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))
|
||||
|
||||
# =========================================================================
|
||||
# Blender Object Creation
|
||||
# =========================================================================
|
||||
|
||||
@classmethod
|
||||
def create_object_for_alignment(cls, alignment: ifcopenshell.entity_instance) -> Optional[bpy.types.Object]:
|
||||
@@ -71,33 +215,24 @@ class Alignment:
|
||||
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
|
||||
|
||||
# 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
|
||||
|
||||
# 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)
|
||||
|
||||
# Link the Blender object to the IFC element (creates bidirectional mapping)
|
||||
tool.Ifc.link(alignment, obj)
|
||||
# Assign to appropriate collection (Bonsai handles collection hierarchy)
|
||||
tool.Collector.assign(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
|
||||
return obj
|
||||
|
||||
@classmethod
|
||||
def create_object_for_layout(
|
||||
@@ -112,42 +247,33 @@ class Alignment:
|
||||
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
|
||||
|
||||
# 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}"
|
||||
|
||||
# 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
|
||||
|
||||
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)
|
||||
|
||||
# Link to IFC element
|
||||
tool.Ifc.link(layout_entity, obj)
|
||||
# Set parent relationship in Blender (mirrors IFC nesting)
|
||||
if parent_obj:
|
||||
obj.parent = parent_obj
|
||||
|
||||
# Also set ifc_definition_id manually as fallback for lookups
|
||||
obj["ifc_definition_id"] = layout_entity.id()
|
||||
# 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)
|
||||
|
||||
# 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
|
||||
return obj
|
||||
|
||||
@classmethod
|
||||
def create_object_for_segment(
|
||||
@@ -167,83 +293,72 @@ class Alignment:
|
||||
Returns:
|
||||
The created Blender object, or existing one if already linked
|
||||
"""
|
||||
import math
|
||||
# Check if a Blender object already exists for this IFC element
|
||||
existing_obj = tool.Ifc.get_object(segment)
|
||||
if existing_obj:
|
||||
return existing_obj
|
||||
|
||||
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}")
|
||||
# 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)
|
||||
|
||||
# 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
|
||||
|
||||
@classmethod
|
||||
def _create_line_segment(
|
||||
cls, name: str, start_point: tuple, direction: float, length: float
|
||||
@@ -259,8 +374,6 @@ class Alignment:
|
||||
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)
|
||||
@@ -303,8 +416,6 @@ class Alignment:
|
||||
Returns:
|
||||
Blender curve object
|
||||
"""
|
||||
import math
|
||||
|
||||
# Calculate arc parameters
|
||||
# Arc length L = R * theta, so theta = L / R
|
||||
abs_radius = abs(radius)
|
||||
@@ -458,71 +569,26 @@ class Alignment:
|
||||
Returns:
|
||||
True if removed successfully
|
||||
"""
|
||||
# Unlink from IFC if linked
|
||||
try:
|
||||
import bonsai.tool as tool
|
||||
tool.Ifc.unlink(obj=obj)
|
||||
except Exception:
|
||||
pass # Object might not be linked
|
||||
|
||||
# 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
|
||||
|
||||
# Store data reference before removing object
|
||||
data = obj.data
|
||||
# Remove the object
|
||||
bpy.data.objects.remove(obj, do_unlink=True)
|
||||
|
||||
# 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)
|
||||
|
||||
# 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
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def remove_layout_segment_objects(cls, layout: ifcopenshell.entity_instance) -> int:
|
||||
@@ -534,30 +600,13 @@ class Alignment:
|
||||
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())
|
||||
|
||||
obj = tool.Ifc.get_object(segment)
|
||||
if obj and cls._remove_blender_object(obj):
|
||||
removed_count += 1
|
||||
|
||||
@@ -573,13 +622,7 @@ class Alignment:
|
||||
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 []:
|
||||
@@ -588,41 +631,13 @@ class Alignment:
|
||||
# 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())
|
||||
|
||||
# Remove layout object
|
||||
layout_obj = tool.Ifc.get_object(layout)
|
||||
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}")
|
||||
|
||||
# Remove alignment object
|
||||
alignment_obj = tool.Ifc.get_object(alignment)
|
||||
if alignment_obj and cls._remove_blender_object(alignment_obj):
|
||||
removed_count += 1
|
||||
|
||||
@@ -641,25 +656,19 @@ class Alignment:
|
||||
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)
|
||||
|
||||
# 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}")
|
||||
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)
|
||||
|
||||
# =========================================================================
|
||||
# Validation and Safe Wrappers
|
||||
# =========================================================================
|
||||
|
||||
Reference in New Issue
Block a user