mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-10-02 13:04:49 +00:00
Use IfcOpenShell geometry engine for alignment visualization
Replace manual per-segment geometry creation with IfcOpenShell's built-in generate_vertices() utility. This provides automatic support for all curve types (CLOTHOID, spirals, etc.) and removes ~75 lines of manual geometry code. Changes: - Add create_curve_from_representation() using IfcOpenShell geometry engine - Add _create_segment_empty() for segment selection without geometry - Update create_objects_for_layout_segments() to use new methods - Delete manual geometry methods: create_object_for_segment(), _create_line_segment(), _create_arc_segment() Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -114,21 +114,16 @@ def create_layout_segment_objects(
|
|||||||
) -> list:
|
) -> list:
|
||||||
"""Create Blender objects for all segments in a layout.
|
"""Create Blender objects for all segments in a layout.
|
||||||
|
|
||||||
|
Delegates to the tool layer which creates both:
|
||||||
|
- A curve from the IFC representation (for visualization)
|
||||||
|
- Empty objects for each segment (for selection/editing)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
alignment_tool: The Alignment tool class
|
alignment_tool: The Alignment tool class
|
||||||
layout: The IFC layout entity
|
layout: The IFC layout entity
|
||||||
layout_obj: The parent Blender object
|
layout_obj: The parent Blender object
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List of created segment Blender objects
|
List of created Blender objects (curve + segment empties)
|
||||||
"""
|
"""
|
||||||
segment_objs = []
|
return alignment_tool.create_objects_for_layout_segments(layout, layout_obj)
|
||||||
|
|
||||||
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
|
|
||||||
|
|||||||
@@ -276,14 +276,80 @@ class Alignment:
|
|||||||
return obj
|
return obj
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create_object_for_segment(
|
def create_curve_from_representation(
|
||||||
cls, segment: ifcopenshell.entity_instance, index: int, parent_obj: Optional[bpy.types.Object] = None
|
cls,
|
||||||
|
layout: "ifcopenshell.entity_instance",
|
||||||
|
parent_obj: Optional[bpy.types.Object] = None,
|
||||||
) -> Optional[bpy.types.Object]:
|
) -> Optional[bpy.types.Object]:
|
||||||
"""Create a Blender curve object for an IFC alignment segment.
|
"""Create a Blender curve from an alignment layout's IFC representation.
|
||||||
|
|
||||||
Creates actual curve geometry (not just an empty) to visualize
|
Uses IfcOpenShell's geometry engine to generate vertices, supporting
|
||||||
the segment. LINE segments become straight curves, CIRCULARARC
|
all segment types (LINE, CIRCULARARC, CLOTHOID, spirals, etc.).
|
||||||
segments become arcs.
|
|
||||||
|
Args:
|
||||||
|
layout: The IFC layout entity (IfcAlignmentHorizontal, etc.)
|
||||||
|
parent_obj: The parent Blender object (alignment object)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The created Blender curve object, or None if no representation
|
||||||
|
"""
|
||||||
|
import ifcopenshell.api.alignment as align_api
|
||||||
|
from ifcopenshell.api.alignment import util as align_util
|
||||||
|
|
||||||
|
# Get the layout's curve representation
|
||||||
|
try:
|
||||||
|
rep_curve = align_api.get_layout_curve(layout)
|
||||||
|
except Exception:
|
||||||
|
rep_curve = None
|
||||||
|
|
||||||
|
if rep_curve is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Generate vertices using IfcOpenShell's geometry engine
|
||||||
|
try:
|
||||||
|
vertices = align_util.generate_vertices(rep_curve, distance_interval=1.0)
|
||||||
|
except (ValueError, NotImplementedError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
if len(vertices) < 2:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Create Blender curve from vertices
|
||||||
|
layout_type = layout.is_a().replace("IfcAlignment", "") # "Horizontal", "Vertical", etc.
|
||||||
|
name = f"{layout_type}Curve"
|
||||||
|
curve_data = bpy.data.curves.new(name, type="CURVE")
|
||||||
|
curve_data.dimensions = "3D"
|
||||||
|
|
||||||
|
spline = curve_data.splines.new("POLY")
|
||||||
|
spline.points.add(len(vertices) - 1)
|
||||||
|
|
||||||
|
for i, vert in enumerate(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
|
||||||
|
|
||||||
|
# Set parent relationship
|
||||||
|
if parent_obj:
|
||||||
|
obj.parent = parent_obj
|
||||||
|
|
||||||
|
# Assign to same collection as parent
|
||||||
|
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_segment_empty(
|
||||||
|
cls, segment: "ifcopenshell.entity_instance", index: int, parent_obj: Optional[bpy.types.Object] = None
|
||||||
|
) -> Optional[bpy.types.Object]:
|
||||||
|
"""Create an empty Blender object linked to an IFC segment.
|
||||||
|
|
||||||
|
Creates an empty (no geometry) for segment selection/editing.
|
||||||
|
The layout curve handles visualization.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
segment: The IfcAlignmentSegment entity
|
segment: The IfcAlignmentSegment entity
|
||||||
@@ -298,51 +364,24 @@ class Alignment:
|
|||||||
if existing_obj:
|
if existing_obj:
|
||||||
return existing_obj
|
return existing_obj
|
||||||
|
|
||||||
# Get segment parameters
|
# Get segment parameters for naming and positioning
|
||||||
if not hasattr(segment, "DesignParameters") or not segment.DesignParameters:
|
if not hasattr(segment, "DesignParameters") or not segment.DesignParameters:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
dp = segment.DesignParameters
|
dp = segment.DesignParameters
|
||||||
seg_type = getattr(dp, "PredefinedType", "UNKNOWN") or "UNKNOWN"
|
seg_type = getattr(dp, "PredefinedType", "UNKNOWN") or "UNKNOWN"
|
||||||
seg_length = getattr(dp, "SegmentLength", 0.0) or 0.0
|
name = f"Segment {index + 1} ({seg_type})"
|
||||||
|
|
||||||
# Skip zero-length terminal segments
|
# Create empty (no geometry - the layout curve handles visualization)
|
||||||
if seg_length < 0.0001:
|
obj = bpy.data.objects.new(name, None)
|
||||||
return None
|
obj.empty_display_type = "PLAIN_AXES"
|
||||||
|
obj.empty_display_size = 0.5
|
||||||
|
|
||||||
# Get start point
|
# Position at segment start point
|
||||||
start_point = None
|
|
||||||
if hasattr(dp, "StartPoint") and dp.StartPoint:
|
if hasattr(dp, "StartPoint") and dp.StartPoint:
|
||||||
coords = dp.StartPoint.Coordinates
|
coords = dp.StartPoint.Coordinates
|
||||||
if len(coords) >= 2:
|
if len(coords) >= 2:
|
||||||
start_point = (coords[0], coords[1], 0.0)
|
obj.location = (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
|
# Link to IFC element
|
||||||
tool.Ifc.link(segment, obj)
|
tool.Ifc.link(segment, obj)
|
||||||
@@ -351,7 +390,7 @@ class Alignment:
|
|||||||
if parent_obj:
|
if parent_obj:
|
||||||
obj.parent = parent_obj
|
obj.parent = parent_obj
|
||||||
|
|
||||||
# Assign to same collection as parent (avoid "Unsorted")
|
# Assign to same collection as parent
|
||||||
if parent_obj and parent_obj.users_collection:
|
if parent_obj and parent_obj.users_collection:
|
||||||
parent_obj.users_collection[0].objects.link(obj)
|
parent_obj.users_collection[0].objects.link(obj)
|
||||||
else:
|
else:
|
||||||
@@ -360,125 +399,7 @@ class Alignment:
|
|||||||
return obj
|
return obj
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _create_line_segment(
|
def create_hierarchy_for_alignment(cls, alignment: "ifcopenshell.entity_instance") -> Optional[bpy.types.Object]:
|
||||||
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
|
|
||||||
"""
|
|
||||||
# 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
|
|
||||||
"""
|
|
||||||
# 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.
|
"""Create the full Blender object hierarchy for an alignment.
|
||||||
|
|
||||||
Creates:
|
Creates:
|
||||||
@@ -516,28 +437,37 @@ class Alignment:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
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 objects for all segments in a layout.
|
||||||
|
|
||||||
|
Creates a single curve from the IFC representation for visualization,
|
||||||
|
plus empty objects for each segment (for selection/editing).
|
||||||
|
|
||||||
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 segment Blender objects
|
List of created Blender objects (curve + segment empties)
|
||||||
"""
|
"""
|
||||||
segment_objs = []
|
result_objs = []
|
||||||
|
|
||||||
# Get segments via IfcRelNests
|
# Create layout curve from IFC representation (for visualization)
|
||||||
|
# This uses IfcOpenShell's geometry engine which supports all segment types
|
||||||
|
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)
|
||||||
for rel in getattr(layout, "IsNestedBy", []) or []:
|
for rel in getattr(layout, "IsNestedBy", []) or []:
|
||||||
for i, segment in enumerate(rel.RelatedObjects or []):
|
for i, segment in enumerate(rel.RelatedObjects or []):
|
||||||
if segment.is_a() == "IfcAlignmentSegment":
|
if segment.is_a() == "IfcAlignmentSegment":
|
||||||
seg_obj = cls.create_object_for_segment(segment, i, layout_obj)
|
seg_obj = cls._create_segment_empty(segment, i, layout_obj)
|
||||||
if seg_obj:
|
if seg_obj:
|
||||||
segment_objs.append(seg_obj)
|
result_objs.append(seg_obj)
|
||||||
|
|
||||||
return segment_objs
|
return result_objs
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def update_pi_properties(cls, props, geometry_result) -> None:
|
def update_pi_properties(cls, props, geometry_result) -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user