An unnamed UCS (UCS Origin / 3P without saving, the common kind) has an empty UCSNAME exactly like WCS does, so both engines answered `current: world` while the drawing was not in WCS — verified live on AutoCAD 2026 (`_.UCS _O 10,10,0` → UCSNAME="" WORLDUCS=0) and on the DXF it writes ($UCSNAME='' $UCSORG=(0,20,0)). A silent wrong value. - COM: a named match still wins; otherwise WORLDUCS (AutoCAD's own answer) decides, and WORLDUCS=0 appends a `name: None` row built from UCSORG/UCSXDIR/UCSYDIR. - ezdxf: the same rule against $UCSORG/$UCSXDIR/$UCSYDIR via the new `engineering.environment.ucs.is_world_axes`; a stale $UCSNAME falls back to the frame instead of leaving no row current. - Server `ucs_list` result: `current` is null for an unnamed UCS (never `world`), plus `current_unnamed: bool` so null is unambiguous. - Tests: helper, headless (unnamed row, stale name, and the exact AutoCAD DXF over the wire), and COM fake reading WORLDUCS + the frame variables. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
83 lines
3.1 KiB
Python
83 lines
3.1 KiB
Python
"""UCS axes, validated and normalised before either engine writes.
|
|
|
|
Tool coordinates stay WCS on both engines (the repository rule): a UCS is
|
|
stored and made current for the operator's benefit, and nothing here starts
|
|
interpreting inputs in it.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import math
|
|
|
|
WORLD = "world"
|
|
ORTHO_TOLERANCE_DEG = 1e-3
|
|
|
|
#: The WCS frame — what ``$UCSORG`` / ``$UCSXDIR`` / ``$UCSYDIR`` (and the
|
|
#: UCSORG / UCSXDIR / UCSYDIR system variables) hold while no UCS is active.
|
|
WORLD_ORIGIN = (0.0, 0.0, 0.0)
|
|
WORLD_X_AXIS = (1.0, 0.0, 0.0)
|
|
WORLD_Y_AXIS = (0.0, 1.0, 0.0)
|
|
WORLD_TOLERANCE = 1e-9
|
|
|
|
|
|
def is_world_axes(origin, x_axis, y_axis, *, tol: float = WORLD_TOLERANCE) -> bool:
|
|
"""True when the frame *is* the WCS — the test ``$UCSNAME == ""`` cannot make.
|
|
|
|
An unnamed UCS (``UCS Origin`` / ``UCS 3P`` without saving — the common
|
|
kind) also has an empty name, so "is the drawing in WCS" has to be
|
|
answered from the frame itself: AutoCAD's own answer is WORLDUCS, and
|
|
this is the same comparison for a header that has no such variable.
|
|
"""
|
|
for got, want in (
|
|
(origin, WORLD_ORIGIN),
|
|
(x_axis, WORLD_X_AXIS),
|
|
(y_axis, WORLD_Y_AXIS),
|
|
):
|
|
coords = tuple(float(c) for c in got)
|
|
if len(coords) != 3:
|
|
return False
|
|
if any(abs(a - b) > tol for a, b in zip(coords, want, strict=True)):
|
|
return False
|
|
return True
|
|
|
|
|
|
def _vec3(value, where: str) -> tuple[float, float, float]:
|
|
if not isinstance(value, (list, tuple)) or len(value) != 3:
|
|
raise ValueError(f"{where} must be an [x, y, z] triple")
|
|
out = []
|
|
for axis, item in zip("xyz", value, strict=True):
|
|
if isinstance(item, bool) or not isinstance(item, (int, float)):
|
|
raise ValueError(f"{where}.{axis} must be a number, got {item!r}")
|
|
number = float(item)
|
|
if not math.isfinite(number):
|
|
raise ValueError(f"{where}.{axis} must be finite")
|
|
out.append(number)
|
|
return out[0], out[1], out[2]
|
|
|
|
|
|
def _unit(vector, where: str) -> tuple[float, float, float]:
|
|
length = math.sqrt(sum(c * c for c in vector))
|
|
if length < 1e-12:
|
|
raise ValueError(f"{where} must not be a zero vector")
|
|
return vector[0] / length, vector[1] / length, vector[2] / length
|
|
|
|
|
|
def resolve_ucs_axes(origin, x_axis, y_axis) -> dict:
|
|
"""Unit axes plus the measured angle; non-orthogonal axes are refused by that angle."""
|
|
o = _vec3(origin, "origin")
|
|
x = _unit(_vec3(x_axis, "x_axis"), "x_axis")
|
|
y = _unit(_vec3(y_axis, "y_axis"), "y_axis")
|
|
dot = max(-1.0, min(1.0, sum(a * b for a, b in zip(x, y, strict=True))))
|
|
angle = math.degrees(math.acos(dot))
|
|
if abs(angle - 90.0) > ORTHO_TOLERANCE_DEG:
|
|
raise ValueError(
|
|
f"ucs_set: x_axis and y_axis must be perpendicular; measured {angle:.2f}° "
|
|
f"between them (tolerance {ORTHO_TOLERANCE_DEG}°)"
|
|
)
|
|
z = (
|
|
x[1] * y[2] - x[2] * y[1],
|
|
x[2] * y[0] - x[0] * y[2],
|
|
x[0] * y[1] - x[1] * y[0],
|
|
)
|
|
return {"origin": o, "x_axis": x, "y_axis": y, "z_axis": z, "angle_deg": angle}
|