mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-10 06:00:51 +00:00
Create selectable curve geometry for alignment segments
Replace segment empty objects with actual curve geometry so that selecting a segment in the Outliner highlights the corresponding line/curve in the viewport. Changes: - Add get_segment_vertices() using IfcOpenShell's evaluate_segment() to sample points along individual segments via the geometry engine - Replace _create_segment_empty with _create_segment_curve that creates Blender CURVE objects with actual geometry - Remove single HorizontalCurve in favor of per-segment curves - Supports all segment types (LINE, CIRCULARARC, CLOTHOID, spirals, etc.) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -247,6 +247,108 @@ class Alignment:
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
|
# Segment Geometry Utilities
|
||||||
|
# =========================================================================
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_segment_vertices(
|
||||||
|
cls, segment: "ifcopenshell.entity_instance", distance_interval: float = 1.0
|
||||||
|
) -> Optional[List[Tuple[float, float, float]]]:
|
||||||
|
"""Get vertices for a single alignment segment using IfcOpenShell's geometry engine.
|
||||||
|
|
||||||
|
Uses the proven IfcOpenShell geometry engine to evaluate points along
|
||||||
|
the segment, supporting all segment types (LINE, CIRCULARARC, CLOTHOID,
|
||||||
|
spirals, etc.).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
segment: The IfcAlignmentSegment entity
|
||||||
|
distance_interval: Distance between sample points (default 1.0 units)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of (x, y, z) tuples representing vertices along the segment,
|
||||||
|
or None if geometry cannot be generated
|
||||||
|
"""
|
||||||
|
import ifcopenshell.api.alignment as align_api
|
||||||
|
from ifcopenshell.api.alignment import util as align_util
|
||||||
|
import ifcopenshell.util.unit
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
# Skip zero-length segments
|
||||||
|
if cls.is_zero_length_segment(segment):
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Get the mapped curve segment(s) for this alignment segment
|
||||||
|
try:
|
||||||
|
mapped_segments = align_api.get_mapped_segments(segment)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[Alignment] get_mapped_segments failed: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not mapped_segments:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Get IFC file for unit scale
|
||||||
|
ifc_file = tool.Ifc.get()
|
||||||
|
if not ifc_file:
|
||||||
|
return None
|
||||||
|
|
||||||
|
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
|
||||||
|
|
||||||
|
all_vertices = []
|
||||||
|
|
||||||
|
# Process each curve segment (usually 1, but HELMERTCURVE has 2)
|
||||||
|
for curve_segment in mapped_segments:
|
||||||
|
if curve_segment is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Get segment length
|
||||||
|
try:
|
||||||
|
seg_length = abs(curve_segment.SegmentLength.wrappedValue)
|
||||||
|
except (AttributeError, TypeError):
|
||||||
|
try:
|
||||||
|
seg_length = abs(float(curve_segment.SegmentLength))
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if seg_length < 1e-6:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Calculate number of sample points
|
||||||
|
num_points = max(int(seg_length / distance_interval) + 1, 2)
|
||||||
|
|
||||||
|
# Sample points along the segment using IfcOpenShell's geometry engine
|
||||||
|
for i in range(num_points):
|
||||||
|
# Calculate distance along segment (don't exceed segment length)
|
||||||
|
if num_points > 1:
|
||||||
|
dist_along = min(i * distance_interval, seg_length)
|
||||||
|
# Ensure we get the last point exactly at segment end
|
||||||
|
if i == num_points - 1:
|
||||||
|
dist_along = seg_length
|
||||||
|
else:
|
||||||
|
dist_along = 0.0
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Use IfcOpenShell's evaluate_segment to get transform matrix
|
||||||
|
transform_matrix = align_util.evaluate_segment(curve_segment, dist_along)
|
||||||
|
|
||||||
|
# Extract position from 4x4 matrix
|
||||||
|
# Matrix is transposed by util.py, so translation is in row 3
|
||||||
|
x = float(transform_matrix[3, 0]) / unit_scale
|
||||||
|
y = float(transform_matrix[3, 1]) / unit_scale
|
||||||
|
z = float(transform_matrix[3, 2]) / unit_scale
|
||||||
|
|
||||||
|
all_vertices.append((x, y, z))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# If evaluation fails at this point, skip it
|
||||||
|
continue
|
||||||
|
|
||||||
|
if len(all_vertices) < 2:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return all_vertices
|
||||||
|
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
# Blender Object Creation
|
# Blender Object Creation
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
@@ -446,13 +548,13 @@ class Alignment:
|
|||||||
return obj
|
return obj
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _create_segment_empty(
|
def _create_segment_curve(
|
||||||
cls, segment: "ifcopenshell.entity_instance", index: int, parent_obj: Optional[bpy.types.Object] = None
|
cls, segment: "ifcopenshell.entity_instance", index: int, parent_obj: Optional[bpy.types.Object] = None
|
||||||
) -> Optional[bpy.types.Object]:
|
) -> Optional[bpy.types.Object]:
|
||||||
"""Create an empty Blender object linked to an IFC segment.
|
"""Create a Blender curve object for an IFC alignment segment.
|
||||||
|
|
||||||
Creates an empty (no geometry) for segment selection/editing.
|
Creates actual curve geometry using IfcOpenShell's geometry engine,
|
||||||
The layout curve handles visualization.
|
so the segment can be selected and highlighted in the viewport.
|
||||||
|
|
||||||
Zero-length segments (required terminators) are skipped as they should
|
Zero-length segments (required terminators) are skipped as they should
|
||||||
be invisible to users.
|
be invisible to users.
|
||||||
@@ -463,8 +565,8 @@ class Alignment:
|
|||||||
parent_obj: The parent Blender object (layout object)
|
parent_obj: The parent Blender object (layout object)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The created Blender object, or existing one if already linked,
|
The created Blender curve object, or existing one if already linked,
|
||||||
or None for zero-length segments
|
or None for zero-length segments or geometry failures
|
||||||
"""
|
"""
|
||||||
# Skip zero-length segments - they are required terminators but should be invisible
|
# Skip zero-length segments - they are required terminators but should be invisible
|
||||||
if cls.is_zero_length_segment(segment):
|
if cls.is_zero_length_segment(segment):
|
||||||
@@ -475,7 +577,7 @@ class Alignment:
|
|||||||
if existing_obj:
|
if existing_obj:
|
||||||
return existing_obj
|
return existing_obj
|
||||||
|
|
||||||
# Get segment parameters for naming and positioning
|
# Get segment parameters for naming
|
||||||
if not hasattr(segment, "DesignParameters") or not segment.DesignParameters:
|
if not hasattr(segment, "DesignParameters") or not segment.DesignParameters:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -483,18 +585,30 @@ class Alignment:
|
|||||||
seg_type = getattr(dp, "PredefinedType", "UNKNOWN") or "UNKNOWN"
|
seg_type = getattr(dp, "PredefinedType", "UNKNOWN") or "UNKNOWN"
|
||||||
name = f"Segment {index + 1} ({seg_type})"
|
name = f"Segment {index + 1} ({seg_type})"
|
||||||
|
|
||||||
# Create empty (no geometry - the layout curve handles visualization)
|
# Get vertices for this segment using IfcOpenShell's geometry engine
|
||||||
obj = bpy.data.objects.new(name, None)
|
vertices = cls.get_segment_vertices(segment, distance_interval=1.0)
|
||||||
obj.empty_display_type = "PLAIN_AXES"
|
if not vertices or len(vertices) < 2:
|
||||||
obj.empty_display_size = 0.5
|
# Fall back to empty if geometry fails
|
||||||
|
obj = bpy.data.objects.new(name, None)
|
||||||
|
obj.empty_display_type = "PLAIN_AXES"
|
||||||
|
obj.empty_display_size = 0.5
|
||||||
|
else:
|
||||||
|
# Transform vertices from IFC to Blender coordinates
|
||||||
|
blender_vertices = [cls.ifc_to_blender_coordinates(v[0], v[1], v[2]) for v in vertices]
|
||||||
|
|
||||||
# Position at segment start point (convert from IFC to Blender coordinates)
|
# Create Blender curve from vertices
|
||||||
if hasattr(dp, "StartPoint") and dp.StartPoint:
|
curve_data = bpy.data.curves.new(name, type="CURVE")
|
||||||
coords = dp.StartPoint.Coordinates
|
curve_data.dimensions = "3D"
|
||||||
if len(coords) >= 2:
|
|
||||||
# Transform from IFC global to Blender local coordinates
|
spline = curve_data.splines.new("POLY")
|
||||||
blender_coords = cls.ifc_to_blender_coordinates(coords[0], coords[1], 0.0)
|
spline.points.add(len(blender_vertices) - 1)
|
||||||
obj.location = blender_coords
|
|
||||||
|
for i, vert in enumerate(blender_vertices):
|
||||||
|
spline.points[i].co = (vert[0], vert[1], vert[2], 1.0)
|
||||||
|
|
||||||
|
obj = bpy.data.objects.new(name, curve_data)
|
||||||
|
obj.show_in_front = True
|
||||||
|
curve_data.bevel_depth = 0.0
|
||||||
|
|
||||||
# Link to IFC element
|
# Link to IFC element
|
||||||
tool.Ifc.link(segment, obj)
|
tool.Ifc.link(segment, obj)
|
||||||
@@ -552,28 +666,23 @@ class Alignment:
|
|||||||
def create_objects_for_layout_segments(
|
def create_objects_for_layout_segments(
|
||||||
cls, layout: "ifcopenshell.entity_instance", layout_obj: bpy.types.Object
|
cls, layout: "ifcopenshell.entity_instance", layout_obj: bpy.types.Object
|
||||||
) -> List[bpy.types.Object]:
|
) -> List[bpy.types.Object]:
|
||||||
"""Create Blender objects for all segments in a layout.
|
"""Create Blender curve objects for all segments in a layout.
|
||||||
|
|
||||||
Creates a single curve from the IFC representation for visualization,
|
Each segment becomes its own selectable curve object, using IfcOpenShell's
|
||||||
plus empty objects for each segment (for selection/editing).
|
geometry engine to generate accurate geometry for all segment types
|
||||||
|
(LINE, CIRCULARARC, CLOTHOID, spirals, etc.).
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
layout: The IFC layout entity (IfcAlignmentHorizontal, etc.)
|
layout: The IFC layout entity (IfcAlignmentHorizontal, etc.)
|
||||||
layout_obj: The parent Blender object for the layout
|
layout_obj: The parent Blender object for the layout
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List of created Blender objects (curve + segment empties)
|
List of created Blender curve objects for each segment
|
||||||
"""
|
"""
|
||||||
result_objs = []
|
result_objs = []
|
||||||
|
|
||||||
# Create layout curve from IFC representation (for visualization)
|
# Create individual curve objects for each segment
|
||||||
# This uses IfcOpenShell's geometry engine which supports all segment types
|
# Each segment is its own selectable object with actual geometry
|
||||||
curve_obj = cls.create_curve_from_representation(layout, layout_obj)
|
|
||||||
if curve_obj:
|
|
||||||
result_objs.append(curve_obj)
|
|
||||||
|
|
||||||
# Create empty objects for each segment (for selection/editing)
|
|
||||||
# Use a separate counter for visible segments to ensure consistent numbering
|
|
||||||
visible_index = 0
|
visible_index = 0
|
||||||
for rel in getattr(layout, "IsNestedBy", []) or []:
|
for rel in getattr(layout, "IsNestedBy", []) or []:
|
||||||
for segment in rel.RelatedObjects or []:
|
for segment in rel.RelatedObjects or []:
|
||||||
@@ -581,7 +690,7 @@ class Alignment:
|
|||||||
# Skip zero-length segments (they are invisible terminators)
|
# Skip zero-length segments (they are invisible terminators)
|
||||||
if cls.is_zero_length_segment(segment):
|
if cls.is_zero_length_segment(segment):
|
||||||
continue
|
continue
|
||||||
seg_obj = cls._create_segment_empty(segment, visible_index, layout_obj)
|
seg_obj = cls._create_segment_curve(segment, visible_index, layout_obj)
|
||||||
if seg_obj:
|
if seg_obj:
|
||||||
result_objs.append(seg_obj)
|
result_objs.append(seg_obj)
|
||||||
visible_index += 1 # Always increment for consistent numbering
|
visible_index += 1 # Always increment for consistent numbering
|
||||||
|
|||||||
Reference in New Issue
Block a user