mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-10 01:41:57 +00:00
Fix PI picker viewport coordinate calculation and add georeference support
- Fix modal operator to use absolute mouse coordinates converted to 3D viewport region space, instead of event.mouse_region_x/y which are relative to whichever region received the event - Store 3D viewport area, region, and region_data references in invoke() for consistent raycasting throughout modal operation - Add coordinate transformation methods (blender_to_ifc_coordinates and ifc_to_blender_coordinates) for projects with geospatial Blender offsets - Transform alignment curve vertices from IFC global to Blender local coordinates when has_blender_offset is enabled - Add try/except for piecewise-step-size geometry setting in util.py Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -793,11 +793,34 @@ class SAIKEI_OT_pick_pi_from_viewport(Operator):
|
||||
bl_description = "Click in the viewport to add PI points. Right-click or Escape to finish."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
# Store reference to 3D view for modal
|
||||
_area = None
|
||||
_region = None
|
||||
_rv3d = None
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return poll_ifc4x3(cls, context)
|
||||
|
||||
def invoke(self, context, event):
|
||||
# Find the 3D viewport area, region, and region_data
|
||||
for area in context.screen.areas:
|
||||
if area.type == "VIEW_3D":
|
||||
self._area = area
|
||||
for region in area.regions:
|
||||
if region.type == "WINDOW":
|
||||
self._region = region
|
||||
break
|
||||
for space in area.spaces:
|
||||
if space.type == "VIEW_3D":
|
||||
self._rv3d = space.region_3d
|
||||
break
|
||||
break
|
||||
|
||||
if not self._region or not self._rv3d:
|
||||
self.report({"ERROR"}, "No 3D Viewport found")
|
||||
return {"CANCELLED"}
|
||||
|
||||
context.window.cursor_set("CROSSHAIR")
|
||||
context.window_manager.modal_handler_add(self)
|
||||
self.report({"INFO"}, "Click to add PIs. Right-click or Escape to finish.")
|
||||
@@ -809,7 +832,8 @@ class SAIKEI_OT_pick_pi_from_viewport(Operator):
|
||||
coord = self.get_ground_intersection(context, event)
|
||||
if coord:
|
||||
self.add_pi_at_location(context, coord)
|
||||
context.area.tag_redraw()
|
||||
if self._area:
|
||||
self._area.tag_redraw()
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
elif event.type in {"RIGHTMOUSE", "ESC"}:
|
||||
@@ -827,9 +851,19 @@ class SAIKEI_OT_pick_pi_from_viewport(Operator):
|
||||
"""Raycast from mouse to Z=0 ground plane"""
|
||||
from bpy_extras.view3d_utils import region_2d_to_origin_3d, region_2d_to_vector_3d
|
||||
|
||||
region = context.region
|
||||
rv3d = context.region_data
|
||||
coord = (event.mouse_region_x, event.mouse_region_y)
|
||||
region = self._region
|
||||
rv3d = self._rv3d
|
||||
|
||||
# Guard against None context
|
||||
if region is None or rv3d is None:
|
||||
return None
|
||||
|
||||
# Use absolute mouse coordinates and convert to the 3D viewport region's local coords
|
||||
# event.mouse_region_x/y are relative to whatever region received the event,
|
||||
# which may not be the 3D viewport region we stored
|
||||
region_x = event.mouse_x - region.x
|
||||
region_y = event.mouse_y - region.y
|
||||
coord = (region_x, region_y)
|
||||
|
||||
origin = region_2d_to_origin_3d(region, rv3d, coord)
|
||||
direction = region_2d_to_vector_3d(region, rv3d, coord)
|
||||
@@ -843,12 +877,21 @@ class SAIKEI_OT_pick_pi_from_viewport(Operator):
|
||||
return None
|
||||
|
||||
def add_pi_at_location(self, context, coord):
|
||||
"""Add a new PI at the given (x, y) coordinate"""
|
||||
"""Add a new PI at the given (x, y) coordinate.
|
||||
|
||||
The coordinate is in Blender world space. If there's a Blender offset
|
||||
configured (for geospatial coordinates), we convert to IFC global
|
||||
coordinates before storing.
|
||||
"""
|
||||
props = context.scene.SaikeiAlignmentProperties
|
||||
|
||||
# Convert Blender coordinates to IFC coordinates
|
||||
# PIs are stored in IFC coordinate space (global/map coordinates)
|
||||
ifc_coord = tool.Alignment.blender_to_ifc_coordinates(coord[0], coord[1], 0.0)
|
||||
|
||||
pi = props.pis.add()
|
||||
pi.x = coord[0]
|
||||
pi.y = coord[1]
|
||||
pi.x = ifc_coord[0]
|
||||
pi.y = ifc_coord[1]
|
||||
|
||||
# Determine PI type based on position in list
|
||||
if len(props.pis) == 1:
|
||||
|
||||
@@ -286,6 +286,10 @@ class Alignment:
|
||||
Uses IfcOpenShell's geometry engine to generate vertices, supporting
|
||||
all segment types (LINE, CIRCULARARC, CLOTHOID, spirals, etc.).
|
||||
|
||||
The vertices from IFC are in global/map coordinates. If a Blender offset
|
||||
is configured (for handling large geospatial coordinates), the vertices
|
||||
are transformed to Blender local coordinates.
|
||||
|
||||
Args:
|
||||
layout: The IFC layout entity (IfcAlignmentHorizontal, etc.)
|
||||
parent_obj: The parent Blender object (alignment object)
|
||||
@@ -295,11 +299,14 @@ class Alignment:
|
||||
"""
|
||||
import ifcopenshell.api.alignment as align_api
|
||||
from ifcopenshell.api.alignment import util as align_util
|
||||
import ifcopenshell.util.geolocation
|
||||
import ifcopenshell.util.unit
|
||||
|
||||
# Get the layout's curve representation
|
||||
try:
|
||||
rep_curve = align_api.get_layout_curve(layout)
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
print(f"[Alignment] get_layout_curve failed: {e}")
|
||||
rep_curve = None
|
||||
|
||||
if rep_curve is None:
|
||||
@@ -308,13 +315,48 @@ class Alignment:
|
||||
# Generate vertices using IfcOpenShell's geometry engine
|
||||
try:
|
||||
vertices = align_util.generate_vertices(rep_curve, distance_interval=1.0)
|
||||
except (ValueError, NotImplementedError, RuntimeError):
|
||||
except (ValueError, NotImplementedError, RuntimeError) as e:
|
||||
# RuntimeError can occur if IfcOpenShell version doesn't support certain settings
|
||||
print(f"[Alignment] generate_vertices failed: {e}")
|
||||
return None
|
||||
|
||||
if len(vertices) < 2:
|
||||
print(f"[Alignment] Not enough vertices ({len(vertices)}), need at least 2")
|
||||
return None
|
||||
|
||||
# Check if we need to apply Blender offset transformation
|
||||
# IFC vertices are in global/map coordinates, we need to convert to Blender local
|
||||
gprops = tool.Georeference.get_georeference_props()
|
||||
ifc_file = tool.Ifc.get()
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file) if ifc_file else 1.0
|
||||
|
||||
if gprops.has_blender_offset:
|
||||
offset_x = float(gprops.blender_offset_x) * unit_scale
|
||||
offset_y = float(gprops.blender_offset_y) * unit_scale
|
||||
offset_z = float(gprops.blender_offset_z) * unit_scale
|
||||
x_axis_abscissa = float(gprops.blender_x_axis_abscissa)
|
||||
x_axis_ordinate = float(gprops.blender_x_axis_ordinate)
|
||||
|
||||
# Transform each vertex from IFC global to Blender local
|
||||
transformed_vertices = []
|
||||
for vert in vertices:
|
||||
# Create a 4x4 identity matrix with translation set to vertex position
|
||||
import numpy as np
|
||||
matrix = np.eye(4)
|
||||
matrix[0, 3] = vert[0]
|
||||
matrix[1, 3] = vert[1]
|
||||
matrix[2, 3] = vert[2]
|
||||
|
||||
# Apply global2local transformation
|
||||
local_matrix = ifcopenshell.util.geolocation.global2local(
|
||||
matrix, offset_x, offset_y, offset_z, x_axis_abscissa, x_axis_ordinate
|
||||
)
|
||||
|
||||
# Extract transformed position
|
||||
transformed_vertices.append((local_matrix[0, 3], local_matrix[1, 3], local_matrix[2, 3]))
|
||||
|
||||
vertices = transformed_vertices
|
||||
|
||||
# Create Blender curve from vertices
|
||||
layout_type = layout.is_a().replace("IfcAlignment", "") # "Horizontal", "Vertical", etc.
|
||||
name = f"{layout_type}Curve"
|
||||
@@ -331,6 +373,9 @@ class Alignment:
|
||||
obj.show_in_front = True
|
||||
curve_data.bevel_depth = 0.0
|
||||
|
||||
# Set a visible color for the curve (black, like construction lines)
|
||||
obj.color = (0.0, 0.0, 0.0, 1.0) # Black color
|
||||
|
||||
# Set parent relationship
|
||||
if parent_obj:
|
||||
obj.parent = parent_obj
|
||||
@@ -378,11 +423,13 @@ class Alignment:
|
||||
obj.empty_display_type = "PLAIN_AXES"
|
||||
obj.empty_display_size = 0.5
|
||||
|
||||
# Position at segment start point
|
||||
# Position at segment start point (convert from IFC to Blender coordinates)
|
||||
if hasattr(dp, "StartPoint") and dp.StartPoint:
|
||||
coords = dp.StartPoint.Coordinates
|
||||
if len(coords) >= 2:
|
||||
obj.location = (coords[0], coords[1], 0.0)
|
||||
# Transform from IFC global to Blender local coordinates
|
||||
blender_coords = cls.ifc_to_blender_coordinates(coords[0], coords[1], 0.0)
|
||||
obj.location = blender_coords
|
||||
|
||||
# Link to IFC element
|
||||
tool.Ifc.link(segment, obj)
|
||||
@@ -718,3 +765,101 @@ class Alignment:
|
||||
)
|
||||
|
||||
return alignment
|
||||
|
||||
# =========================================================================
|
||||
# Coordinate Transformation Methods
|
||||
# =========================================================================
|
||||
|
||||
@classmethod
|
||||
def blender_to_ifc_coordinates(cls, x: float, y: float, z: float = 0.0) -> Tuple[float, float, float]:
|
||||
"""Convert Blender local coordinates to IFC global/map coordinates.
|
||||
|
||||
When a Blender offset is configured (for handling large geospatial coordinates),
|
||||
this transforms from Blender's local coordinate system (near origin) to
|
||||
the IFC global coordinate system (large geospatial values).
|
||||
|
||||
Args:
|
||||
x: X coordinate in Blender space
|
||||
y: Y coordinate in Blender space
|
||||
z: Z coordinate in Blender space (default 0.0)
|
||||
|
||||
Returns:
|
||||
Tuple of (x, y, z) in IFC global coordinates
|
||||
"""
|
||||
import ifcopenshell.util.geolocation
|
||||
import ifcopenshell.util.unit
|
||||
|
||||
gprops = tool.Georeference.get_georeference_props()
|
||||
ifc_file = tool.Ifc.get()
|
||||
|
||||
if not gprops.has_blender_offset or ifc_file is None:
|
||||
# No transformation needed
|
||||
return (x, y, z)
|
||||
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
|
||||
offset_x = float(gprops.blender_offset_x) * unit_scale
|
||||
offset_y = float(gprops.blender_offset_y) * unit_scale
|
||||
offset_z = float(gprops.blender_offset_z) * unit_scale
|
||||
x_axis_abscissa = float(gprops.blender_x_axis_abscissa)
|
||||
x_axis_ordinate = float(gprops.blender_x_axis_ordinate)
|
||||
|
||||
# Create a 4x4 identity matrix with translation set to position
|
||||
import numpy as np
|
||||
matrix = np.eye(4)
|
||||
matrix[0, 3] = x
|
||||
matrix[1, 3] = y
|
||||
matrix[2, 3] = z
|
||||
|
||||
# Apply local2global transformation (inverse of global2local)
|
||||
global_matrix = ifcopenshell.util.geolocation.local2global(
|
||||
matrix, offset_x, offset_y, offset_z, x_axis_abscissa, x_axis_ordinate
|
||||
)
|
||||
|
||||
return (global_matrix[0, 3], global_matrix[1, 3], global_matrix[2, 3])
|
||||
|
||||
@classmethod
|
||||
def ifc_to_blender_coordinates(cls, x: float, y: float, z: float = 0.0) -> Tuple[float, float, float]:
|
||||
"""Convert IFC global/map coordinates to Blender local coordinates.
|
||||
|
||||
When a Blender offset is configured (for handling large geospatial coordinates),
|
||||
this transforms from the IFC global coordinate system (large geospatial values)
|
||||
to Blender's local coordinate system (near origin).
|
||||
|
||||
Args:
|
||||
x: X coordinate in IFC global space
|
||||
y: Y coordinate in IFC global space
|
||||
z: Z coordinate in IFC global space (default 0.0)
|
||||
|
||||
Returns:
|
||||
Tuple of (x, y, z) in Blender local coordinates
|
||||
"""
|
||||
import ifcopenshell.util.geolocation
|
||||
import ifcopenshell.util.unit
|
||||
|
||||
gprops = tool.Georeference.get_georeference_props()
|
||||
ifc_file = tool.Ifc.get()
|
||||
|
||||
if not gprops.has_blender_offset or ifc_file is None:
|
||||
# No transformation needed
|
||||
return (x, y, z)
|
||||
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
|
||||
offset_x = float(gprops.blender_offset_x) * unit_scale
|
||||
offset_y = float(gprops.blender_offset_y) * unit_scale
|
||||
offset_z = float(gprops.blender_offset_z) * unit_scale
|
||||
x_axis_abscissa = float(gprops.blender_x_axis_abscissa)
|
||||
x_axis_ordinate = float(gprops.blender_x_axis_ordinate)
|
||||
|
||||
# Create a 4x4 identity matrix with translation set to position
|
||||
import numpy as np
|
||||
matrix = np.eye(4)
|
||||
matrix[0, 3] = x
|
||||
matrix[1, 3] = y
|
||||
matrix[2, 3] = z
|
||||
|
||||
# Apply global2local transformation
|
||||
local_matrix = ifcopenshell.util.geolocation.global2local(
|
||||
matrix, offset_x, offset_y, offset_z, x_axis_abscissa, x_axis_ordinate
|
||||
)
|
||||
|
||||
return (local_matrix[0, 3], local_matrix[1, 3], local_matrix[2, 3])
|
||||
|
||||
@@ -93,8 +93,11 @@ def generate_vertices(rep_curve: entity_instance, distance_interval: float = 5.0
|
||||
try:
|
||||
s.set("piecewise-step-type", 0) # 0 = step-size is maximum step size, 1 = step-size is mininimum number of steps
|
||||
except RuntimeError:
|
||||
pass # Setting not available in older IfcOpenShell versions
|
||||
s.set("piecewise-step-size", distance_interval)
|
||||
print("[util.py] piecewise-step-type setting not available, skipping")
|
||||
try:
|
||||
s.set("piecewise-step-size", distance_interval)
|
||||
except RuntimeError:
|
||||
print("[util.py] piecewise-step-size setting not available, skipping")
|
||||
shape = ifcopenshell.geom.create_shape(s, rep_curve)
|
||||
vertices = shape.verts
|
||||
if len(vertices) == 0:
|
||||
|
||||
Reference in New Issue
Block a user