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
502 lines
20 KiB
Python
502 lines
20 KiB
Python
"""Named views and UCS on both engines.
|
|
|
|
Headless, a named view is a VIEW table entry and "restore" writes the
|
|
``*Active`` VPORT (the view the file opens on) — ezdxf's header has no
|
|
``$VIEWCTR``; measured. A UCS is a UCS table entry plus the ``$UCS*`` header
|
|
variables. Tool coordinates stay WCS on both engines: a UCS is stored and
|
|
made current for the operator, and no tool interprets its inputs in it.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import types
|
|
|
|
import pytest
|
|
from fastmcp import Client
|
|
|
|
import server
|
|
from engineering.environment.ucs import is_world_axes, resolve_ucs_axes
|
|
from engineering.environment.views import resolve_view_args
|
|
|
|
pytestmark = pytest.mark.asyncio
|
|
|
|
|
|
# ── pure helpers ────────────────────────────────────────────────────────────
|
|
|
|
|
|
def test_view_args_validate_before_any_write():
|
|
assert resolve_view_args([10, 20], 50, None) == {
|
|
"center": (10.0, 20.0),
|
|
"height": 50.0,
|
|
"width": None,
|
|
}
|
|
assert resolve_view_args(None, None, None) == {"center": None, "height": None, "width": None}
|
|
with pytest.raises(ValueError, match="height must be > 0"):
|
|
resolve_view_args(None, 0, None)
|
|
with pytest.raises(ValueError, match="width must be > 0"):
|
|
resolve_view_args(None, 10, -1)
|
|
with pytest.raises(ValueError, match="center"):
|
|
resolve_view_args([1], None, None)
|
|
with pytest.raises(ValueError, match="finite"):
|
|
resolve_view_args([float("nan"), 0], None, None)
|
|
|
|
|
|
def test_ucs_axes_are_normalised_and_orthogonality_is_measured():
|
|
axes = resolve_ucs_axes([10, 10, 0], [0, 2, 0], [-3, 0, 0])
|
|
assert axes["origin"] == (10.0, 10.0, 0.0)
|
|
assert axes["x_axis"] == (0.0, 1.0, 0.0) and axes["y_axis"] == (-1.0, 0.0, 0.0)
|
|
assert axes["z_axis"] == (0.0, 0.0, 1.0)
|
|
assert axes["angle_deg"] == pytest.approx(90.0)
|
|
with pytest.raises(ValueError, match=r"measured 45\.00"):
|
|
resolve_ucs_axes([0, 0, 0], [1, 0, 0], [1, 1, 0])
|
|
with pytest.raises(ValueError, match="x_axis.*zero"):
|
|
resolve_ucs_axes([0, 0, 0], [0, 0, 0], [0, 1, 0])
|
|
with pytest.raises(ValueError, match="origin"):
|
|
resolve_ucs_axes([0, 0], [1, 0, 0], [0, 1, 0])
|
|
|
|
|
|
def test_world_is_decided_by_the_frame_not_the_name():
|
|
assert is_world_axes((0, 0, 0), (1, 0, 0), (0, 1, 0))
|
|
assert is_world_axes([0.0, 0.0, 1e-12], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0])
|
|
assert not is_world_axes((0, 20, 0), (1, 0, 0), (0, 1, 0)) # UCS Origin
|
|
assert not is_world_axes((0, 0, 0), (0, 1, 0), (-1, 0, 0)) # rotated 90°
|
|
assert not is_world_axes((0, 0), (1, 0, 0), (0, 1, 0))
|
|
|
|
|
|
# ── headless: named views ───────────────────────────────────────────────────
|
|
|
|
|
|
async def test_view_defaults_to_the_drawing_extents(backend):
|
|
await backend.entity_create_line(0, 0, 100, 50)
|
|
result = await backend.view_named_save("OVERALL")
|
|
assert result["ok"] and result["replaced"] is False
|
|
assert result["center"] == [50.0, 25.0]
|
|
# extents 100 x 50 at the *Active VPORT's default aspect 1.34: the width
|
|
# is the binding side, so height = 100 / 1.34
|
|
assert result["height"] == pytest.approx(100.0 / 1.34)
|
|
assert result["width"] == pytest.approx(100.0)
|
|
view = backend._doc.views.get("OVERALL")
|
|
assert tuple(view.dxf.center)[:2] == (50.0, 25.0)
|
|
|
|
|
|
async def test_view_on_an_empty_drawing_needs_an_explicit_window(backend):
|
|
with pytest.raises(ValueError, match="no extents"):
|
|
await backend.view_named_save("EMPTY")
|
|
assert "EMPTY" not in backend._doc.views
|
|
result = await backend.view_named_save("EMPTY", center=[0, 0], height=10)
|
|
assert result["center"] == [0.0, 0.0] and result["height"] == 10.0
|
|
assert result["width"] == pytest.approx(13.4)
|
|
|
|
|
|
async def test_view_replace_restore_and_list(backend):
|
|
await backend.view_named_save("V", center=[1, 2], height=10, width=15)
|
|
again = await backend.view_named_save("v", center=[5, 5], height=20)
|
|
assert again["replaced"] is True and again["name"] == "V"
|
|
assert len(list(backend._doc.views)) == 1
|
|
restored = await backend.view_named_restore("v")
|
|
assert restored == {
|
|
"ok": True,
|
|
"name": "V",
|
|
"center": [5.0, 5.0],
|
|
"height": 20.0,
|
|
"width": pytest.approx(26.8),
|
|
"applied": "header_only",
|
|
"store": "vport_active",
|
|
"backend": "ezdxf",
|
|
}
|
|
(vport,) = backend._doc.viewports.get("*Active")
|
|
assert tuple(vport.dxf.center)[:2] == (5.0, 5.0) and vport.dxf.height == 20.0
|
|
assert await backend.view_named_list() == [
|
|
{"name": "V", "center": [5.0, 5.0], "height": 20.0, "width": pytest.approx(26.8)}
|
|
]
|
|
with pytest.raises(ValueError, match="no named view 'NOPE'"):
|
|
await backend.view_named_restore("NOPE")
|
|
with pytest.raises(ValueError, match="height must be > 0"):
|
|
await backend.view_named_save("BAD", center=[0, 0], height=0)
|
|
assert "BAD" not in backend._doc.views
|
|
|
|
|
|
async def test_view_is_written_as_a_plan_view_on_disk(backend, tmp_path):
|
|
"""ezdxf's VIEW default is ``direction=(1, 1, 1)`` — an isometric view. The
|
|
tool stores the plan window of ``center``/``height``, so the entry must
|
|
say so explicitly (DXF 11/21/31, 12/22/32 and 50) or AutoCAD restores it
|
|
oblique and reads ``center`` in that DCS."""
|
|
await backend.entity_create_line(0, 0, 100, 50)
|
|
await backend.view_named_save("OVERALL")
|
|
view = backend._doc.views.get("OVERALL")
|
|
assert tuple(view.dxf.direction) == (0.0, 0.0, 1.0)
|
|
assert tuple(view.dxf.target) == (0.0, 0.0, 0.0)
|
|
assert view.dxf.view_twist == 0.0
|
|
|
|
import ezdxf
|
|
|
|
path = tmp_path / "views.dxf"
|
|
await backend.drawing_save_as(str(path))
|
|
reopened = ezdxf.readfile(str(path)).views.get("OVERALL")
|
|
assert tuple(reopened.dxf.direction) == (0.0, 0.0, 1.0)
|
|
assert tuple(reopened.dxf.target) == (0.0, 0.0, 0.0)
|
|
assert tuple(reopened.dxf.center)[:2] == (50.0, 25.0)
|
|
|
|
|
|
async def test_view_restore_carries_the_orientation_into_the_vport(backend):
|
|
"""A foreign drawing's VIEW may be oblique or twisted; restoring it must
|
|
not silently flatten it to a plan view of the same center."""
|
|
backend._doc.views.add(
|
|
"ISO",
|
|
dxfattribs={
|
|
"center": (5.0, 6.0, 0.0),
|
|
"height": 20.0,
|
|
"width": 30.0,
|
|
"direction": (1.0, 1.0, 1.0),
|
|
"target": (1.0, 2.0, 3.0),
|
|
"view_twist": 15.0,
|
|
},
|
|
)
|
|
result = await backend.view_named_restore("iso")
|
|
assert result["name"] == "ISO" and result["center"] == [5.0, 6.0]
|
|
(vport,) = backend._doc.viewports.get("*Active")
|
|
assert tuple(vport.dxf.direction) == (1.0, 1.0, 1.0)
|
|
assert tuple(vport.dxf.target) == (1.0, 2.0, 3.0)
|
|
assert vport.dxf.view_twist == 15.0
|
|
# ...and restoring a view the tool saved puts the VPORT back on plan.
|
|
await backend.view_named_save("PLAN", center=[0, 0], height=10)
|
|
await backend.view_named_restore("PLAN")
|
|
(vport,) = backend._doc.viewports.get("*Active")
|
|
assert tuple(vport.dxf.direction) == (0.0, 0.0, 1.0)
|
|
assert tuple(vport.dxf.target) == (0.0, 0.0, 0.0)
|
|
assert vport.dxf.view_twist == 0.0
|
|
|
|
|
|
# ── headless: UCS ───────────────────────────────────────────────────────────
|
|
|
|
|
|
async def test_ucs_set_stores_the_entry_and_makes_it_current(backend):
|
|
result = await backend.ucs_set("FRONT", [10, 10, 0], [0, 1, 0], [-1, 0, 0])
|
|
assert result == {
|
|
"ok": True,
|
|
"name": "FRONT",
|
|
"origin": [10.0, 10.0, 0.0],
|
|
"x_axis": [0.0, 1.0, 0.0],
|
|
"y_axis": [-1.0, 0.0, 0.0],
|
|
"replaced": False,
|
|
"current": True,
|
|
"backend": "ezdxf",
|
|
}
|
|
header = backend._doc.header
|
|
assert header["$UCSNAME"] == "FRONT" and tuple(header["$UCSORG"]) == (10.0, 10.0, 0.0)
|
|
assert tuple(header["$UCSXDIR"]) == (0.0, 1.0, 0.0)
|
|
rows = await backend.ucs_list()
|
|
assert rows[0] == {
|
|
"name": "world",
|
|
"origin": [0.0, 0.0, 0.0],
|
|
"x_axis": [1.0, 0.0, 0.0],
|
|
"y_axis": [0.0, 1.0, 0.0],
|
|
"current": False,
|
|
}
|
|
assert rows[1]["name"] == "FRONT" and rows[1]["current"] is True
|
|
|
|
|
|
async def test_ucs_non_orthogonal_is_refused_before_any_write(backend):
|
|
with pytest.raises(ValueError, match="measured 60"):
|
|
await backend.ucs_set("SKEW", [0, 0, 0], [1, 0, 0], [0.5, 0.8660254, 0])
|
|
assert "SKEW" not in backend._doc.ucs and backend._doc.header["$UCSNAME"] == ""
|
|
|
|
|
|
async def test_ucs_restore_world_and_named(backend):
|
|
await backend.ucs_set("FRONT", [10, 10, 0], [0, 1, 0], [-1, 0, 0])
|
|
world = await backend.ucs_restore("world")
|
|
assert world["name"] == "world" and backend._doc.header["$UCSNAME"] == ""
|
|
assert tuple(backend._doc.header["$UCSXDIR"]) == (1.0, 0.0, 0.0)
|
|
assert (await backend.ucs_list())[0]["current"] is True
|
|
named = await backend.ucs_restore("front")
|
|
assert named["name"] == "FRONT" and backend._doc.header["$UCSNAME"] == "FRONT"
|
|
with pytest.raises(ValueError, match="no UCS named 'NOPE'"):
|
|
await backend.ucs_restore("NOPE")
|
|
with pytest.raises(ValueError, match="reserved"):
|
|
await backend.ucs_set("world", [0, 0, 0], [1, 0, 0], [0, 1, 0])
|
|
|
|
|
|
async def test_ucs_replace_keeps_one_entry(backend):
|
|
await backend.ucs_set("A", [0, 0, 0], [1, 0, 0], [0, 1, 0])
|
|
again = await backend.ucs_set("a", [1, 1, 0], [1, 0, 0], [0, 1, 0])
|
|
assert again["replaced"] is True and again["name"] == "A"
|
|
assert len(list(backend._doc.ucs)) == 1
|
|
|
|
|
|
async def test_ucs_list_does_not_call_an_unnamed_ucs_world(backend):
|
|
"""``UCS Origin`` / ``UCS 3P`` without saving — the common kind — leaves
|
|
``$UCSNAME`` empty exactly like WCS does (AutoCAD 2026 writes
|
|
``$UCSNAME=''``, ``$UCSORG=(0,20,0)`` after ``_.UCS _O``). The frame, not
|
|
the name, says whether the drawing is in WCS."""
|
|
await backend.ucs_set("FRONT", [10, 10, 0], [0, 1, 0], [-1, 0, 0])
|
|
header = backend._doc.header
|
|
header["$UCSNAME"] = ""
|
|
header["$UCSORG"] = (50.0, 20.0, 0.0)
|
|
header["$UCSXDIR"] = (0.0, 1.0, 0.0)
|
|
header["$UCSYDIR"] = (-1.0, 0.0, 0.0)
|
|
rows = await backend.ucs_list()
|
|
assert [r["name"] for r in rows] == ["world", "FRONT", None]
|
|
assert [r["current"] for r in rows] == [False, False, True]
|
|
assert rows[2] == {
|
|
"name": None,
|
|
"origin": [50.0, 20.0, 0.0],
|
|
"x_axis": [0.0, 1.0, 0.0],
|
|
"y_axis": [-1.0, 0.0, 0.0],
|
|
"current": True,
|
|
}
|
|
# ...and the same empty name with the WCS frame really is world.
|
|
await backend.ucs_restore("world")
|
|
rows = await backend.ucs_list()
|
|
assert [r["name"] for r in rows] == ["world", "FRONT"]
|
|
assert rows[0]["current"] is True and rows[1]["current"] is False
|
|
|
|
|
|
async def test_ucs_list_a_stale_name_falls_back_to_the_frame(backend):
|
|
"""``$UCSNAME`` naming an entry the table no longer holds is not a
|
|
current row; the frame decides between world and unnamed."""
|
|
header = backend._doc.header
|
|
header["$UCSNAME"] = "GONE"
|
|
rows = await backend.ucs_list()
|
|
assert [(r["name"], r["current"]) for r in rows] == [("world", True)]
|
|
header["$UCSORG"] = (0.0, 20.0, 0.0)
|
|
rows = await backend.ucs_list()
|
|
assert [(r["name"], r["current"]) for r in rows] == [("world", False), (None, True)]
|
|
assert rows[1]["origin"] == [0.0, 20.0, 0.0]
|
|
|
|
|
|
async def test_ucs_list_tool_reports_an_unnamed_ucs_over_the_wire(monkeypatch, tmp_path):
|
|
"""The exact file AutoCAD writes after ``_.UCS _O 0,20,0`` without saving:
|
|
``$UCSNAME=''`` and ``$UCSORG=(0,20,0)``. The tool used to answer
|
|
``current: "world"``."""
|
|
import ezdxf
|
|
|
|
doc = ezdxf.new("R2018")
|
|
doc.ucs.add(
|
|
"FRONT", dxfattribs={"origin": (10, 10, 0), "xaxis": (0, 1, 0), "yaxis": (-1, 0, 0)}
|
|
)
|
|
doc.header["$UCSNAME"] = ""
|
|
doc.header["$UCSORG"] = (0.0, 20.0, 0.0)
|
|
path = tmp_path / "unnamed_ucs.dxf"
|
|
doc.saveas(str(path))
|
|
|
|
monkeypatch.setenv("AUTOCAD_MCP_BACKEND", "ezdxf")
|
|
async with Client(server.mcp) as client:
|
|
await client.call_tool("drawing_open", {"path": str(path)})
|
|
payload = (await client.call_tool("ucs_list", {})).structured_content
|
|
assert payload["current"] is None and payload["current_unnamed"] is True
|
|
assert payload["count"] == 3
|
|
assert [r["name"] for r in payload["ucs"]] == ["world", "FRONT", None]
|
|
assert payload["ucs"][0]["current"] is False
|
|
assert payload["ucs"][2]["current"] is True
|
|
assert payload["ucs"][2]["origin"] == [0.0, 20.0, 0.0]
|
|
|
|
|
|
# ── live engine, against a fake ActiveX surface ──────────────────────────────
|
|
|
|
|
|
class _FakeView:
|
|
def __init__(self, name):
|
|
self.Name = name
|
|
self.Center = (0.0, 0.0)
|
|
self.Height = 100.0
|
|
self.Width = 150.0
|
|
|
|
|
|
class _FakeViews:
|
|
def __init__(self):
|
|
self.views: list[_FakeView] = []
|
|
self.calls: list[tuple[str, tuple]] = []
|
|
|
|
@property
|
|
def Count(self):
|
|
return len(self.views)
|
|
|
|
def Item(self, key):
|
|
if isinstance(key, int):
|
|
return self.views[key]
|
|
for v in self.views:
|
|
if v.Name.lower() == key.lower():
|
|
return v
|
|
raise RuntimeError(f"no view {key}")
|
|
|
|
def Add(self, name):
|
|
self.calls.append(("Add", (name,)))
|
|
v = _FakeView(name)
|
|
self.views.append(v)
|
|
return v
|
|
|
|
|
|
class _FakeUcs:
|
|
def __init__(self, name, origin, xv, yv):
|
|
self.Name = name
|
|
self.Origin = origin
|
|
self.XVector = xv
|
|
self.YVector = yv
|
|
|
|
|
|
class _FakeUcsCollection:
|
|
def __init__(self):
|
|
self.items: list[_FakeUcs] = []
|
|
self.calls: list[tuple[str, tuple]] = []
|
|
|
|
@property
|
|
def Count(self):
|
|
return len(self.items)
|
|
|
|
def Item(self, key):
|
|
if isinstance(key, int):
|
|
return self.items[key]
|
|
for u in self.items:
|
|
if u.Name.lower() == key.lower():
|
|
return u
|
|
raise RuntimeError(f"no ucs {key}")
|
|
|
|
def Add(self, origin, x_point, y_point, name):
|
|
self.calls.append(("Add", (origin, x_point, y_point, name)))
|
|
# ActiveX stores unit vectors derived from the two points.
|
|
xv = tuple(p - o for p, o in zip(x_point, origin, strict=True))
|
|
yv = tuple(p - o for p, o in zip(y_point, origin, strict=True))
|
|
u = _FakeUcs(name, origin, xv, yv)
|
|
self.items.append(u)
|
|
return u
|
|
|
|
|
|
class _FakeDocument:
|
|
def __init__(self):
|
|
self.Views = _FakeViews()
|
|
self.UserCoordinateSystems = _FakeUcsCollection()
|
|
self._viewport = types.SimpleNamespace(Center=(30.0, 40.0), Height=100.0, Width=150.0)
|
|
self.viewport_sets = 0
|
|
self.ActiveUCS = None
|
|
self.commands: list[str] = []
|
|
|
|
@property
|
|
def ActiveViewport(self):
|
|
return self._viewport
|
|
|
|
@ActiveViewport.setter
|
|
def ActiveViewport(self, value):
|
|
self._viewport = value
|
|
self.viewport_sets += 1
|
|
|
|
def SendCommand(self, macro):
|
|
self.commands.append(macro)
|
|
|
|
|
|
@pytest.fixture
|
|
def com_backend(monkeypatch):
|
|
pytest.importorskip("win32com.client", reason="pywin32 not installed")
|
|
from backends import com_backend as module
|
|
|
|
document = _FakeDocument()
|
|
variables = {
|
|
"CMDACTIVE": 0,
|
|
"UCSNAME": "",
|
|
"WORLDUCS": 1,
|
|
"UCSORG": (0.0, 0.0, 0.0),
|
|
"UCSXDIR": (1.0, 0.0, 0.0),
|
|
"UCSYDIR": (0.0, 1.0, 0.0),
|
|
}
|
|
app = types.SimpleNamespace(GetVariable=lambda name: variables[name])
|
|
monkeypatch.setattr(module, "_acad_doc", lambda: document)
|
|
monkeypatch.setattr(module, "_acad_app", lambda: app)
|
|
monkeypatch.setattr(module, "_regen", lambda: None)
|
|
monkeypatch.setattr(module, "_apoint", lambda x, y, z=0.0: (float(x), float(y), float(z)))
|
|
monkeypatch.setattr(module, "_av", lambda values: tuple(float(v) for v in values))
|
|
backend = module.ComBackend()
|
|
|
|
async def _run_inline(func, *args, **kwargs):
|
|
return func(*args, **kwargs)
|
|
|
|
monkeypatch.setattr(backend, "_run", _run_inline)
|
|
return backend, document, variables
|
|
|
|
|
|
async def test_com_view_save_defaults_to_the_active_viewport(com_backend):
|
|
backend, document, _ = com_backend
|
|
result = await backend.view_named_save("HOME")
|
|
assert document.Views.calls == [("Add", ("HOME",))]
|
|
(view,) = document.Views.views
|
|
assert view.Center == (30.0, 40.0) and view.Height == 100.0 and view.Width == 150.0
|
|
assert result["center"] == [30.0, 40.0] and result["replaced"] is False
|
|
again = await backend.view_named_save("home", center=[1, 2], height=10)
|
|
assert again["replaced"] is True and len(document.Views.views) == 1
|
|
assert view.Center == (1.0, 2.0) and view.Height == 10.0 and view.Width == 15.0
|
|
|
|
|
|
async def test_com_view_restore_sets_the_active_viewport(com_backend):
|
|
backend, document, _ = com_backend
|
|
await backend.view_named_save("HOME", center=[5, 6], height=20, width=30)
|
|
result = await backend.view_named_restore("HOME")
|
|
assert result["applied"] == "active_viewport"
|
|
assert document.ActiveViewport.Center == (5.0, 6.0)
|
|
assert document.ActiveViewport.Height == 20.0 and document.ActiveViewport.Width == 30.0
|
|
assert document.viewport_sets == 1, "ActiveX applies a viewport change only on re-assignment"
|
|
assert await backend.view_named_list() == [
|
|
{"name": "HOME", "center": [5.0, 6.0], "height": 20.0, "width": 30.0}
|
|
]
|
|
|
|
|
|
async def test_com_ucs_add_uses_points_on_the_axes_not_vectors(com_backend):
|
|
backend, document, variables = com_backend
|
|
result = await backend.ucs_set("FRONT", [10, 10, 0], [0, 5, 0], [-1, 0, 0])
|
|
assert document.UserCoordinateSystems.calls == [
|
|
("Add", ((10.0, 10.0, 0.0), (10.0, 11.0, 0.0), (9.0, 10.0, 0.0), "FRONT"))
|
|
]
|
|
assert document.ActiveUCS is document.UserCoordinateSystems.items[0]
|
|
assert result["x_axis"] == [0.0, 1.0, 0.0] and result["current"] is True
|
|
variables["UCSNAME"] = "FRONT"
|
|
rows = await backend.ucs_list()
|
|
assert rows[0]["name"] == "world" and rows[0]["current"] is False
|
|
assert rows[1] == {
|
|
"name": "FRONT",
|
|
"origin": [10.0, 10.0, 0.0],
|
|
"x_axis": [0.0, 1.0, 0.0],
|
|
"y_axis": [-1.0, 0.0, 0.0],
|
|
"current": True,
|
|
}
|
|
|
|
|
|
async def test_com_ucs_list_reads_worlducs_not_the_empty_name(com_backend):
|
|
"""Verified live on AutoCAD 2026: after ``_.UCS _O 10,10,0`` (unnamed),
|
|
``UCSNAME=""`` and ``WORLDUCS=0`` — the same empty name WCS has."""
|
|
backend, document, variables = com_backend
|
|
await backend.ucs_set("FRONT", [10, 10, 0], [0, 1, 0], [-1, 0, 0])
|
|
variables.update(
|
|
{
|
|
"UCSNAME": "",
|
|
"WORLDUCS": 0,
|
|
"UCSORG": (0.0, 20.0, 0.0),
|
|
"UCSXDIR": (1.0, 0.0, 0.0),
|
|
"UCSYDIR": (0.0, 1.0, 0.0),
|
|
}
|
|
)
|
|
rows = await backend.ucs_list()
|
|
assert [(r["name"], r["current"]) for r in rows] == [
|
|
("world", False),
|
|
("FRONT", False),
|
|
(None, True),
|
|
]
|
|
assert rows[2]["origin"] == [0.0, 20.0, 0.0] and rows[2]["x_axis"] == [1.0, 0.0, 0.0]
|
|
# WORLDUCS=1 with the same empty name is world, and no unnamed row appears.
|
|
variables.update({"WORLDUCS": 1, "UCSORG": (0.0, 0.0, 0.0)})
|
|
rows = await backend.ucs_list()
|
|
assert [(r["name"], r["current"]) for r in rows] == [("world", True), ("FRONT", False)]
|
|
|
|
|
|
async def test_com_ucs_restore_world_sends_the_ucs_command_when_idle(com_backend):
|
|
backend, document, variables = com_backend
|
|
result = await backend.ucs_restore("world")
|
|
assert document.commands == ["_.UCS _W\n"] and result["name"] == "world"
|
|
variables["CMDACTIVE"] = 1
|
|
with pytest.raises(RuntimeError, match="CMDACTIVE"):
|
|
await backend.ucs_restore("world")
|
|
assert len(document.commands) == 1
|
|
|
|
|
|
async def test_com_ucs_refuses_before_activex(com_backend):
|
|
backend, document, _ = com_backend
|
|
with pytest.raises(ValueError, match="measured"):
|
|
await backend.ucs_set("SKEW", [0, 0, 0], [1, 0, 0], [1, 1, 0])
|
|
assert document.UserCoordinateSystems.calls == []
|