Change from XY to EN and use strings not floats in Blender due to precision

This commit is contained in:
Dion Moult
2026-02-13 11:09:45 +11:00
parent cfe30a505e
commit 65af40e7d5
4 changed files with 45 additions and 157 deletions
@@ -199,8 +199,8 @@ def sync_pis_from_ifc(props):
props.pis.clear()
for pi_data in extracted_pis:
pi = props.pis.add()
pi.x = pi_data["x"]
pi.y = pi_data["y"]
pi.e = pi_data["e"]
pi.n = pi_data["n"]
pi.pi_type = pi_data["pi_type"]
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)
"""
# Incoming tangent direction
dx1 = curr_pi.x - prev_pi.x
dy1 = curr_pi.y - prev_pi.y
dx1 = float(curr_pi.e) - float(prev_pi.e)
dy1 = float(curr_pi.n) - float(prev_pi.n)
angle1 = math.atan2(dy1, dx1)
# Outgoing tangent direction
dx2 = next_pi.x - curr_pi.x
dy2 = next_pi.y - curr_pi.y
dx2 = float(next_pi.e) - float(curr_pi.e)
dy2 = float(next_pi.n) - float(curr_pi.n)
angle2 = math.atan2(dy2, dx2)
# 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]
# Full length between PIs
dx = end_pi.x - start_pi.x
dy = end_pi.y - start_pi.y
dx = float(end_pi.e) - float(start_pi.e)
dy = float(end_pi.n) - float(start_pi.n)
full_length = math.sqrt(dx * dx + dy * dy)
if not account_for_curves:
@@ -588,7 +588,7 @@ def recalculate_pi_geometry(props):
return
# 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)
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.pi_index = i
curve_row.display_type = "Curve"
curve_row.x = pi.x # Show PI coordinates on curve row
curve_row.y = pi.y
curve_row.e = pi.e # Show PI coordinates on curve row
curve_row.n = pi.n
curve_row.radius = pi.radius
curve_row.arc_length = compute_arc_length_for_pi(props, i)
else:
@@ -651,8 +651,8 @@ def rebuild_display_rows(props):
else:
point_row.display_type = "Mid"
point_row.x = pi.x
point_row.y = pi.y
point_row.e = pi.e
point_row.n = pi.n
# Add tangent segment row after this point/curve (except after last PI)
if i < len(pis) - 1:
@@ -699,23 +699,23 @@ class SAIKEI_OT_add_pi(Operator):
# Set default position based on existing PIs
if len(props.pis) == 1:
# First PI - start at origin
pi.x = 0.0
pi.y = 0.0
pi.e = str(0.0)
pi.n = str(0.0)
pi.pi_type = "ENDPOINT"
elif len(props.pis) == 2:
# Second PI - offset from first
prev = props.pis[0]
pi.x = prev.x + 100.0
pi.y = prev.y
pi.e = str(float(prev.e) + 100.0)
pi.n = prev.n
pi.pi_type = "ENDPOINT"
else:
# Additional PIs - extrapolate from last two
prev = props.pis[-2]
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
dy = prev.y - prev_prev.y if len(props.pis) > 2 else 0.0
pi.x = prev.x + dx
pi.y = prev.y + dy
de = float(prev.e) - prev_prev.e if len(props.pis) > 2 else 100.0
dn = float(prev.n) - prev_prev.n if len(props.pis) > 2 else 0.0
pi.e = str(float(prev.e) + de)
pi.n = str(float(prev.n) + dn)
pi.pi_type = "TANGENT"
# Previous endpoint becomes tangent or curve
@@ -918,7 +918,8 @@ class SAIKEI_OT_pick_pi_from_viewport(Operator):
points = []
for pi in props.pis:
# 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))
return points
@@ -941,11 +942,11 @@ class SAIKEI_OT_pick_pi_from_viewport(Operator):
# 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)
ifc_coord = tool.Georeference.xyz2enh((coord[0], coord[1], 0.0))
pi = props.pis.add()
pi.x = ifc_coord[0]
pi.y = ifc_coord[1]
pi.e = str(ifc_coord[0])
pi.n = str(ifc_coord[1])
# Determine PI type based on position in list
if len(props.pis) == 1:
@@ -1005,7 +1006,7 @@ class SAIKEI_OT_recalculate_pis(Operator):
return {"CANCELLED"}
# 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]]
# 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
# 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]]
# Check if there's an active alignment we should add to instead of creating new
+4 -17
View File
@@ -62,21 +62,8 @@ class AlignmentPI(PropertyGroup):
"""
# Coordinates
x: FloatProperty(
name="X",
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",
)
e: StringProperty(name="E", description="Easting", default="0.0")
n: StringProperty(name="N", description="Northing", default="0.0")
# PI Type
pi_type: EnumProperty(
@@ -177,8 +164,8 @@ class AlignmentDisplayRow(PropertyGroup):
display_type: StringProperty(name="Type", default="")
# Point coordinates (only for POINT rows)
x: FloatProperty(name="X", default=0.0, precision=3, unit="LENGTH")
y: FloatProperty(name="Y", default=0.0, precision=3, unit="LENGTH")
e: StringProperty(name="E", default="0.0")
n: StringProperty(name="N", default="0.0")
# Segment properties (only for SEGMENT rows)
length: FloatProperty(name="Length", default=0.0, precision=2, unit="LENGTH")
+9 -9
View File
@@ -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
if pi:
sub = row.row(align=True)
sub.prop(pi, "x", text="")
sub.prop(pi, "y", text="")
sub.prop(pi, "e", text="")
sub.prop(pi, "n", text="")
else:
row.label(text=f"{item.x:.2f}")
row.label(text=f"{item.y:.2f}")
row.label(text=f"{item.e:.2f}")
row.label(text=f"{item.n:.2f}")
# Length column - empty for point rows
row.label(text="")
@@ -86,8 +86,8 @@ class SAIKEI_UL_alignment_pis(UIList):
row.label(text="Curve", icon="SPHERECURVE")
# Show PI coordinates on curve row
row.label(text=f"{item.x:.2f}")
row.label(text=f"{item.y:.2f}")
row.label(text=f"{item.e:.2f}")
row.label(text=f"{item.n:.2f}")
# Arc length
row.label(text=f"{item.arc_length:.2f}")
@@ -254,8 +254,8 @@ class SAIKEI_PT_pi_editor(Panel):
header = layout.row(align=True)
header.label(text="No.")
header.label(text="Type")
header.label(text="X")
header.label(text="Y")
header.label(text="E")
header.label(text="N")
header.label(text="Length")
header.label(text="Radius")
@@ -320,4 +320,4 @@ class SAIKEI_PT_alignment_stationing(Panel):
# Stationing operators
col = layout.column(align=True)
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")
+4 -104
View File
@@ -571,7 +571,8 @@ class Alignment:
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]
# 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
curve_data = bpy.data.curves.new(name, type="CURVE")
@@ -923,104 +924,6 @@ 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])
# =========================================================================
# PI Edit Mode Methods
# =========================================================================
@@ -1248,7 +1151,7 @@ class Alignment:
for i, pi in enumerate(pis):
# 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
name = f"PI.{i + 1:03d}"
@@ -1344,10 +1247,7 @@ class Alignment:
for i, empty in enumerate(empties):
# Convert Blender position to IFC coordinates
blender_pos = empty.location
ifc_pos = cls.blender_to_ifc_coordinates(
blender_pos.x, blender_pos.y, blender_pos.z
)
ifc_pos = tool.Georeference.xyz2enh(tuple(empty.matrix_world.translation))
hpoints.append((ifc_pos[0], ifc_pos[1]))
# Collect radii for interior PIs only (not first or last)