mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-28 03:05:33 +00:00
Change from XY to EN and use strings not floats in Blender due to precision
This commit is contained in:
@@ -199,8 +199,8 @@ def sync_pis_from_ifc(props):
|
|||||||
props.pis.clear()
|
props.pis.clear()
|
||||||
for pi_data in extracted_pis:
|
for pi_data in extracted_pis:
|
||||||
pi = props.pis.add()
|
pi = props.pis.add()
|
||||||
pi.x = pi_data["x"]
|
pi.e = pi_data["e"]
|
||||||
pi.y = pi_data["y"]
|
pi.n = pi_data["n"]
|
||||||
pi.pi_type = pi_data["pi_type"]
|
pi.pi_type = pi_data["pi_type"]
|
||||||
pi.radius = pi_data.get("radius", 0.0)
|
pi.radius = pi_data.get("radius", 0.0)
|
||||||
|
|
||||||
@@ -449,13 +449,13 @@ def compute_deflection_angle(prev_pi, curr_pi, next_pi):
|
|||||||
Deflection angle in radians (signed: positive=left, negative=right)
|
Deflection angle in radians (signed: positive=left, negative=right)
|
||||||
"""
|
"""
|
||||||
# Incoming tangent direction
|
# Incoming tangent direction
|
||||||
dx1 = curr_pi.x - prev_pi.x
|
dx1 = float(curr_pi.e) - float(prev_pi.e)
|
||||||
dy1 = curr_pi.y - prev_pi.y
|
dy1 = float(curr_pi.n) - float(prev_pi.n)
|
||||||
angle1 = math.atan2(dy1, dx1)
|
angle1 = math.atan2(dy1, dx1)
|
||||||
|
|
||||||
# Outgoing tangent direction
|
# Outgoing tangent direction
|
||||||
dx2 = next_pi.x - curr_pi.x
|
dx2 = float(next_pi.e) - float(curr_pi.e)
|
||||||
dy2 = next_pi.y - curr_pi.y
|
dy2 = float(next_pi.n) - float(curr_pi.n)
|
||||||
angle2 = math.atan2(dy2, dx2)
|
angle2 = math.atan2(dy2, dx2)
|
||||||
|
|
||||||
# Deflection angle
|
# Deflection angle
|
||||||
@@ -546,8 +546,8 @@ def compute_segment_length(props, start_pi_index, account_for_curves=True):
|
|||||||
end_pi = pis[start_pi_index + 1]
|
end_pi = pis[start_pi_index + 1]
|
||||||
|
|
||||||
# Full length between PIs
|
# Full length between PIs
|
||||||
dx = end_pi.x - start_pi.x
|
dx = float(end_pi.e) - float(start_pi.e)
|
||||||
dy = end_pi.y - start_pi.y
|
dy = float(end_pi.n) - float(start_pi.n)
|
||||||
full_length = math.sqrt(dx * dx + dy * dy)
|
full_length = math.sqrt(dx * dx + dy * dy)
|
||||||
|
|
||||||
if not account_for_curves:
|
if not account_for_curves:
|
||||||
@@ -588,7 +588,7 @@ def recalculate_pi_geometry(props):
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Extract PI coordinates for calculation
|
# Extract PI coordinates for calculation
|
||||||
pi_coords = [(pi.x, pi.y) for pi in pis]
|
pi_coords = [(float(pi.e), float(pi.n)) for pi in pis]
|
||||||
|
|
||||||
# Use tool layer for calculation (math belongs in tool, not core)
|
# Use tool layer for calculation (math belongs in tool, not core)
|
||||||
result = tool.Alignment.calculate_pi_geometry(pi_coords, props.start_station)
|
result = tool.Alignment.calculate_pi_geometry(pi_coords, props.start_station)
|
||||||
@@ -636,8 +636,8 @@ def rebuild_display_rows(props):
|
|||||||
curve_row.segment_number = segment_num
|
curve_row.segment_number = segment_num
|
||||||
curve_row.pi_index = i
|
curve_row.pi_index = i
|
||||||
curve_row.display_type = "Curve"
|
curve_row.display_type = "Curve"
|
||||||
curve_row.x = pi.x # Show PI coordinates on curve row
|
curve_row.e = pi.e # Show PI coordinates on curve row
|
||||||
curve_row.y = pi.y
|
curve_row.n = pi.n
|
||||||
curve_row.radius = pi.radius
|
curve_row.radius = pi.radius
|
||||||
curve_row.arc_length = compute_arc_length_for_pi(props, i)
|
curve_row.arc_length = compute_arc_length_for_pi(props, i)
|
||||||
else:
|
else:
|
||||||
@@ -651,8 +651,8 @@ def rebuild_display_rows(props):
|
|||||||
else:
|
else:
|
||||||
point_row.display_type = "Mid"
|
point_row.display_type = "Mid"
|
||||||
|
|
||||||
point_row.x = pi.x
|
point_row.e = pi.e
|
||||||
point_row.y = pi.y
|
point_row.n = pi.n
|
||||||
|
|
||||||
# Add tangent segment row after this point/curve (except after last PI)
|
# Add tangent segment row after this point/curve (except after last PI)
|
||||||
if i < len(pis) - 1:
|
if i < len(pis) - 1:
|
||||||
@@ -699,23 +699,23 @@ class SAIKEI_OT_add_pi(Operator):
|
|||||||
# Set default position based on existing PIs
|
# Set default position based on existing PIs
|
||||||
if len(props.pis) == 1:
|
if len(props.pis) == 1:
|
||||||
# First PI - start at origin
|
# First PI - start at origin
|
||||||
pi.x = 0.0
|
pi.e = str(0.0)
|
||||||
pi.y = 0.0
|
pi.n = str(0.0)
|
||||||
pi.pi_type = "ENDPOINT"
|
pi.pi_type = "ENDPOINT"
|
||||||
elif len(props.pis) == 2:
|
elif len(props.pis) == 2:
|
||||||
# Second PI - offset from first
|
# Second PI - offset from first
|
||||||
prev = props.pis[0]
|
prev = props.pis[0]
|
||||||
pi.x = prev.x + 100.0
|
pi.e = str(float(prev.e) + 100.0)
|
||||||
pi.y = prev.y
|
pi.n = prev.n
|
||||||
pi.pi_type = "ENDPOINT"
|
pi.pi_type = "ENDPOINT"
|
||||||
else:
|
else:
|
||||||
# Additional PIs - extrapolate from last two
|
# Additional PIs - extrapolate from last two
|
||||||
prev = props.pis[-2]
|
prev = props.pis[-2]
|
||||||
prev_prev = props.pis[-3] if len(props.pis) > 2 else prev
|
prev_prev = props.pis[-3] if len(props.pis) > 2 else prev
|
||||||
dx = prev.x - prev_prev.x if len(props.pis) > 2 else 100.0
|
de = float(prev.e) - prev_prev.e if len(props.pis) > 2 else 100.0
|
||||||
dy = prev.y - prev_prev.y if len(props.pis) > 2 else 0.0
|
dn = float(prev.n) - prev_prev.n if len(props.pis) > 2 else 0.0
|
||||||
pi.x = prev.x + dx
|
pi.e = str(float(prev.e) + de)
|
||||||
pi.y = prev.y + dy
|
pi.n = str(float(prev.n) + dn)
|
||||||
pi.pi_type = "TANGENT"
|
pi.pi_type = "TANGENT"
|
||||||
|
|
||||||
# Previous endpoint becomes tangent or curve
|
# Previous endpoint becomes tangent or curve
|
||||||
@@ -918,7 +918,8 @@ class SAIKEI_OT_pick_pi_from_viewport(Operator):
|
|||||||
points = []
|
points = []
|
||||||
for pi in props.pis:
|
for pi in props.pis:
|
||||||
# Convert from IFC to Blender coordinates
|
# Convert from IFC to Blender coordinates
|
||||||
blender_coord = tool.Alignment.ifc_to_blender_coordinates(pi.x, pi.y, 0.0)
|
blender_coord = tool.Georeference.enh2xyz((float(pi.e), float(pi.n), 0.0))
|
||||||
|
print(repr(blender_coord))
|
||||||
points.append(Vector(blender_coord))
|
points.append(Vector(blender_coord))
|
||||||
return points
|
return points
|
||||||
|
|
||||||
@@ -941,11 +942,11 @@ class SAIKEI_OT_pick_pi_from_viewport(Operator):
|
|||||||
|
|
||||||
# Convert Blender coordinates to IFC coordinates
|
# Convert Blender coordinates to IFC coordinates
|
||||||
# PIs are stored in IFC coordinate space (global/map 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)
|
ifc_coord = tool.Georeference.xyz2enh((coord[0], coord[1], 0.0))
|
||||||
|
|
||||||
pi = props.pis.add()
|
pi = props.pis.add()
|
||||||
pi.x = ifc_coord[0]
|
pi.e = str(ifc_coord[0])
|
||||||
pi.y = ifc_coord[1]
|
pi.n = str(ifc_coord[1])
|
||||||
|
|
||||||
# Determine PI type based on position in list
|
# Determine PI type based on position in list
|
||||||
if len(props.pis) == 1:
|
if len(props.pis) == 1:
|
||||||
@@ -1005,7 +1006,7 @@ class SAIKEI_OT_recalculate_pis(Operator):
|
|||||||
return {"CANCELLED"}
|
return {"CANCELLED"}
|
||||||
|
|
||||||
# Collect updated PI data
|
# Collect updated PI data
|
||||||
hpoints = [(pi.x, pi.y) for pi in props.pis]
|
hpoints = [(float(pi.e), float(pi.n)) for pi in props.pis]
|
||||||
radii = [pi.radius for pi in props.pis[1:-1]]
|
radii = [pi.radius for pi in props.pis[1:-1]]
|
||||||
|
|
||||||
# Remove Blender visualization for segments (not the whole hierarchy)
|
# Remove Blender visualization for segments (not the whole hierarchy)
|
||||||
@@ -1154,7 +1155,7 @@ class SAIKEI_OT_create_alignment_by_pi(Operator):
|
|||||||
props = context.scene.SaikeiAlignmentProperties
|
props = context.scene.SaikeiAlignmentProperties
|
||||||
|
|
||||||
# Collect PI data
|
# Collect PI data
|
||||||
hpoints = [(pi.x, pi.y) for pi in props.pis]
|
hpoints = [(float(pi.e), float(pi.n)) for pi in props.pis]
|
||||||
radii = [pi.radius for pi in props.pis[1:-1]]
|
radii = [pi.radius for pi in props.pis[1:-1]]
|
||||||
|
|
||||||
# Check if there's an active alignment we should add to instead of creating new
|
# Check if there's an active alignment we should add to instead of creating new
|
||||||
|
|||||||
@@ -62,21 +62,8 @@ class AlignmentPI(PropertyGroup):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# Coordinates
|
# Coordinates
|
||||||
x: FloatProperty(
|
e: StringProperty(name="E", description="Easting", default="0.0")
|
||||||
name="X",
|
n: StringProperty(name="N", description="Northing", default="0.0")
|
||||||
description="X coordinate (Easting)",
|
|
||||||
default=0.0,
|
|
||||||
precision=3,
|
|
||||||
unit="LENGTH",
|
|
||||||
)
|
|
||||||
|
|
||||||
y: FloatProperty(
|
|
||||||
name="Y",
|
|
||||||
description="Y coordinate (Northing)",
|
|
||||||
default=0.0,
|
|
||||||
precision=3,
|
|
||||||
unit="LENGTH",
|
|
||||||
)
|
|
||||||
|
|
||||||
# PI Type
|
# PI Type
|
||||||
pi_type: EnumProperty(
|
pi_type: EnumProperty(
|
||||||
@@ -177,8 +164,8 @@ class AlignmentDisplayRow(PropertyGroup):
|
|||||||
display_type: StringProperty(name="Type", default="")
|
display_type: StringProperty(name="Type", default="")
|
||||||
|
|
||||||
# Point coordinates (only for POINT rows)
|
# Point coordinates (only for POINT rows)
|
||||||
x: FloatProperty(name="X", default=0.0, precision=3, unit="LENGTH")
|
e: StringProperty(name="E", default="0.0")
|
||||||
y: FloatProperty(name="Y", default=0.0, precision=3, unit="LENGTH")
|
n: StringProperty(name="N", default="0.0")
|
||||||
|
|
||||||
# Segment properties (only for SEGMENT rows)
|
# Segment properties (only for SEGMENT rows)
|
||||||
length: FloatProperty(name="Length", default=0.0, precision=2, unit="LENGTH")
|
length: FloatProperty(name="Length", default=0.0, precision=2, unit="LENGTH")
|
||||||
|
|||||||
@@ -64,11 +64,11 @@ class SAIKEI_UL_alignment_pis(UIList):
|
|||||||
pi = data.pis[item.pi_index] if item.pi_index < len(data.pis) else None
|
pi = data.pis[item.pi_index] if item.pi_index < len(data.pis) else None
|
||||||
if pi:
|
if pi:
|
||||||
sub = row.row(align=True)
|
sub = row.row(align=True)
|
||||||
sub.prop(pi, "x", text="")
|
sub.prop(pi, "e", text="")
|
||||||
sub.prop(pi, "y", text="")
|
sub.prop(pi, "n", text="")
|
||||||
else:
|
else:
|
||||||
row.label(text=f"{item.x:.2f}")
|
row.label(text=f"{item.e:.2f}")
|
||||||
row.label(text=f"{item.y:.2f}")
|
row.label(text=f"{item.n:.2f}")
|
||||||
|
|
||||||
# Length column - empty for point rows
|
# Length column - empty for point rows
|
||||||
row.label(text="")
|
row.label(text="")
|
||||||
@@ -86,8 +86,8 @@ class SAIKEI_UL_alignment_pis(UIList):
|
|||||||
row.label(text="Curve", icon="SPHERECURVE")
|
row.label(text="Curve", icon="SPHERECURVE")
|
||||||
|
|
||||||
# Show PI coordinates on curve row
|
# Show PI coordinates on curve row
|
||||||
row.label(text=f"{item.x:.2f}")
|
row.label(text=f"{item.e:.2f}")
|
||||||
row.label(text=f"{item.y:.2f}")
|
row.label(text=f"{item.n:.2f}")
|
||||||
|
|
||||||
# Arc length
|
# Arc length
|
||||||
row.label(text=f"{item.arc_length:.2f}")
|
row.label(text=f"{item.arc_length:.2f}")
|
||||||
@@ -254,8 +254,8 @@ class SAIKEI_PT_pi_editor(Panel):
|
|||||||
header = layout.row(align=True)
|
header = layout.row(align=True)
|
||||||
header.label(text="No.")
|
header.label(text="No.")
|
||||||
header.label(text="Type")
|
header.label(text="Type")
|
||||||
header.label(text="X")
|
header.label(text="E")
|
||||||
header.label(text="Y")
|
header.label(text="N")
|
||||||
header.label(text="Length")
|
header.label(text="Length")
|
||||||
header.label(text="Radius")
|
header.label(text="Radius")
|
||||||
|
|
||||||
@@ -320,4 +320,4 @@ class SAIKEI_PT_alignment_stationing(Panel):
|
|||||||
# Stationing operators
|
# Stationing operators
|
||||||
col = layout.column(align=True)
|
col = layout.column(align=True)
|
||||||
col.operator("saikei.add_stationing_referent", icon="EMPTY_AXIS")
|
col.operator("saikei.add_stationing_referent", icon="EMPTY_AXIS")
|
||||||
col.operator("saikei.name_segments", icon="FONT_DATA")
|
col.operator("saikei.name_segments", icon="FONT_DATA")
|
||||||
|
|||||||
@@ -571,7 +571,8 @@ class Alignment:
|
|||||||
obj.empty_display_size = 0.5
|
obj.empty_display_size = 0.5
|
||||||
else:
|
else:
|
||||||
# Transform vertices from IFC to Blender coordinates
|
# Transform vertices from IFC to Blender coordinates
|
||||||
blender_vertices = [cls.ifc_to_blender_coordinates(v[0], v[1], v[2]) for v in vertices]
|
# TODO: I think this is wrong, this is probably from Local to Blender, not Global to Blender
|
||||||
|
blender_vertices = [tool.Georeference.enh2xyz((v[0], v[1], v[2])) for v in vertices]
|
||||||
|
|
||||||
# Create Blender curve from vertices
|
# Create Blender curve from vertices
|
||||||
curve_data = bpy.data.curves.new(name, type="CURVE")
|
curve_data = bpy.data.curves.new(name, type="CURVE")
|
||||||
@@ -923,104 +924,6 @@ class Alignment:
|
|||||||
|
|
||||||
return 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])
|
|
||||||
|
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
# PI Edit Mode Methods
|
# PI Edit Mode Methods
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
@@ -1248,7 +1151,7 @@ class Alignment:
|
|||||||
|
|
||||||
for i, pi in enumerate(pis):
|
for i, pi in enumerate(pis):
|
||||||
# Convert IFC coordinates to Blender coordinates
|
# Convert IFC coordinates to Blender coordinates
|
||||||
blender_pos = cls.ifc_to_blender_coordinates(pi["x"], pi["y"], 0.0)
|
blender_pos = tool.Georeference.enh2xyz((float(pi["e"]), float(pi["n"]), 0.0))
|
||||||
|
|
||||||
# Create EMPTY object
|
# Create EMPTY object
|
||||||
name = f"PI.{i + 1:03d}"
|
name = f"PI.{i + 1:03d}"
|
||||||
@@ -1344,10 +1247,7 @@ class Alignment:
|
|||||||
|
|
||||||
for i, empty in enumerate(empties):
|
for i, empty in enumerate(empties):
|
||||||
# Convert Blender position to IFC coordinates
|
# Convert Blender position to IFC coordinates
|
||||||
blender_pos = empty.location
|
ifc_pos = tool.Georeference.xyz2enh(tuple(empty.matrix_world.translation))
|
||||||
ifc_pos = cls.blender_to_ifc_coordinates(
|
|
||||||
blender_pos.x, blender_pos.y, blender_pos.z
|
|
||||||
)
|
|
||||||
hpoints.append((ifc_pos[0], ifc_pos[1]))
|
hpoints.append((ifc_pos[0], ifc_pos[1]))
|
||||||
|
|
||||||
# Collect radii for interior PIs only (not first or last)
|
# Collect radii for interior PIs only (not first or last)
|
||||||
|
|||||||
Reference in New Issue
Block a user