fix(task-22): headless named views are written as plan views and restore carries the orientation
ezdxf's VIEW default is direction=(1, 1, 1) — an isometric view — so every named view the headless engine wrote to disk was oblique while the tool claimed to store the plan window of center/height; AutoCAD restored it in that DCS. view_named_save now writes direction (0,0,1), target (0,0,0) and view_twist 0 explicitly. view_named_restore copies direction/target/twist from the VIEW into the *Active VPORT instead of flattening a foreign oblique view to plan, reading the attributes the way the file carries them (ezdxf forces those tags on export). Measured live on AutoCAD 2026: Views.Add already creates a plan view, so the COM path only gains a note. Tests: the VIEW entry and the reopened DXF assert 11/21/31 = 0/0/1 and the target; a hand-built oblique, twisted VIEW restores its orientation into the VPORT and a tool-saved view puts it back on plan. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
This commit is contained in:
co-authored by
Claude Opus 5
parent
dcc8f07bf4
commit
4bf686e943
@@ -1662,6 +1662,9 @@ class ComBackend(AutoCADBackend):
|
||||
view = self._com_view(doc, clean)
|
||||
replaced = view is not None
|
||||
if view is None:
|
||||
# Measured (AutoCAD 2026): Views.Add creates a plan view —
|
||||
# Direction (0, 0, 1), Target (0, 0, 0) — so unlike ezdxf's
|
||||
# VIEW default of (1, 1, 1) nothing has to be forced here.
|
||||
view = doc.Views.Add(clean)
|
||||
view.Center = _av([cx, cy])
|
||||
view.Height = float(h)
|
||||
|
||||
@@ -2576,7 +2576,22 @@ class EzdxfBackend(AutoCADBackend):
|
||||
stored = existing.dxf.name if replaced else clean
|
||||
if replaced:
|
||||
doc.views.remove(stored)
|
||||
doc.views.add(stored, dxfattribs={"center": (cx, cy, 0.0), "height": h, "width": w})
|
||||
# The tool stores a plan window. ezdxf's VIEW default is
|
||||
# direction=(1, 1, 1) — an isometric view — so the orientation must
|
||||
# be written explicitly or AutoCAD restores the entry oblique and
|
||||
# reads ``center`` in that DCS. Measured: the VPORT default is plan,
|
||||
# which is why a save→restore round trip alone never shows it.
|
||||
doc.views.add(
|
||||
stored,
|
||||
dxfattribs={
|
||||
"center": (cx, cy, 0.0),
|
||||
"height": h,
|
||||
"width": w,
|
||||
"direction": (0.0, 0.0, 1.0),
|
||||
"target": (0.0, 0.0, 0.0),
|
||||
"view_twist": 0.0,
|
||||
},
|
||||
)
|
||||
self._mark_dirty()
|
||||
return {
|
||||
"ok": True,
|
||||
@@ -2610,6 +2625,14 @@ class EzdxfBackend(AutoCADBackend):
|
||||
vport.dxf.height = h
|
||||
if h > 0:
|
||||
vport.dxf.aspect_ratio = w / h
|
||||
# The orientation travels with the window: a foreign drawing's
|
||||
# VIEW may be oblique or twisted, and flattening it to a plan view
|
||||
# of the same center would be a silent lie. The attribute read
|
||||
# mirrors what the file carries: ezdxf forces these tags on export,
|
||||
# so an unset direction is (1, 1, 1) on disk as well as here.
|
||||
vport.dxf.direction = tuple(view.dxf.direction)
|
||||
vport.dxf.target = tuple(view.dxf.target)
|
||||
vport.dxf.view_twist = float(view.dxf.view_twist)
|
||||
self._mark_dirty()
|
||||
return {
|
||||
"ok": True,
|
||||
|
||||
@@ -106,6 +106,57 @@ async def test_view_replace_restore_and_list(backend):
|
||||
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 ───────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user