fix(task-22): live named views read and restore the display (VIEWCTR/VIEWSIZE, ZOOM Window), UCS sysvars come from the document, and a frozen current layer is thawed before restore makes it current
Four review findings, all measured by the reviewer on AutoCAD 2026:
1. ucs_list and the ucs_restore('world') CMDACTIVE guard read
app.GetVariable, but GetVariable is an AcadDocument member — the tool
was dead live and the guard's swallowed read forced cmd_active to 0,
sending `_.UCS _W` into any open prompt. Both read the document now,
and a failed CMDACTIVE read refuses instead of proceeding. The fake
app in tests/test_views_ucs.py has no GetVariable any more, so the
old code cannot pass.
2. view_named_save defaulted from doc.ActiveViewport.Center/Height/Width,
which does not track the display (it answered the initial view after
`_.ZOOM _C 5,6 20`). The current view is VIEWCTR / VIEWSIZE at the
SCREENSIZE aspect — what `-VIEW _S` records (width 40.267 for height
20). The fake viewport is deliberately stale so a read of it fails.
3. view_named_restore wrote Center/Height/Width into the AcadViewport;
AutoCAD anchors the lower-left corner and widens to the display
aspect, landing a 10x20 view at (5, 6) on VIEWCTR (20.134, 6). It now
zooms the saved rectangle (app.ZoomWindow), which centres and fits
like `-VIEW _R` (VIEWCTR = saved centre, VIEWSIZE = max(h, w/aspect)),
reports `applied: "zoom_window"` and reads VIEWCTR / VIEWSIZE back.
Headless the same reconciliation moved the centre whenever the
*Active VPORT's aspect was rewritten from w/h (even the 1.34 default
opened at x = 11.734): the aspect the file carries is left alone —
it is the display's, written by AutoCAD at save — and the window is
fitted into it; the payload adds `vport_height` / `aspect_ratio`.
4. layer_state_restore set ActiveLayer before the loop and outside the
try; AutoCAD refuses to make a frozen layer current, so the spec's
own save → freeze → restore path raised an opaque COM error with
nothing applied. When `frozen` is being restored the state's current
layer is thawed first; the write is wrapped and a refusal lands in
`warnings`. The fake document and layer now model both refusals.
Live AutoCAD was not running in this round (GetActiveObject failed, no
acad process), so the fix is measured against the reviewer's numbers,
not re-measured.
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
bbc9aeeb63
commit
c7dd72410a
+72
-27
@@ -1534,9 +1534,22 @@ class ComBackend(AutoCADBackend):
|
||||
warnings: list[str] = []
|
||||
# Current first: AutoCAD refuses to freeze the active layer, so the
|
||||
# active one must already be the state's before the loop freezes.
|
||||
# And it refuses to make a *frozen* layer current (measured, AutoCAD
|
||||
# 2026: ``doc.ActiveLayer = <frozen>`` raises 'Error setting active
|
||||
# layer'), so when the state thaws its own current layer that thaw
|
||||
# comes before the ActiveLayer write instead of after it in the
|
||||
# loop. Only a requested property is touched: without "frozen" the
|
||||
# refusal stands and lands in ``warnings`` like every other one.
|
||||
if "current" in props and state["current_layer"] not in missing:
|
||||
doc.ActiveLayer = doc.Layers.Item(state["current_layer"])
|
||||
current = state["current_layer"]
|
||||
target_name = state["current_layer"]
|
||||
target = doc.Layers.Item(target_name)
|
||||
try:
|
||||
if "frozen" in props and not bool(state["layers"][target_name]["frozen"]):
|
||||
target.Freeze = False
|
||||
doc.ActiveLayer = target
|
||||
current = target_name
|
||||
except Exception as exc: # e.g. the state's current layer is frozen
|
||||
warnings.append(f"{target_name}: cannot be made current: {exc}")
|
||||
applied = 0
|
||||
for layer_name, snap in state["layers"].items():
|
||||
if layer_name in missing:
|
||||
@@ -1633,6 +1646,13 @@ class ComBackend(AutoCADBackend):
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _com_display_aspect(doc) -> float:
|
||||
"""Width / height of the current viewport's display, from SCREENSIZE."""
|
||||
size = doc.GetVariable("SCREENSIZE")
|
||||
sx, sy = float(size[0]), float(size[1])
|
||||
return sx / sy if sx > 0 and sy > 0 else 1.0
|
||||
|
||||
@staticmethod
|
||||
def _com_view_row(view) -> dict:
|
||||
center = view.Center
|
||||
@@ -1652,12 +1672,20 @@ class ComBackend(AutoCADBackend):
|
||||
|
||||
def _sync():
|
||||
doc = _acad_doc()
|
||||
vport = doc.ActiveViewport
|
||||
vp_center = vport.Center
|
||||
vp_height = float(vport.Height) or 1.0
|
||||
aspect = float(vport.Width) / vp_height if vp_height else 1.5
|
||||
cx, cy = args["center"] or (float(vp_center[0]), float(vp_center[1]))
|
||||
h = args["height"] or vp_height
|
||||
# "The current view" is VIEWCTR / VIEWSIZE at the display's aspect
|
||||
# (SCREENSIZE), which is what AutoCAD's own `-VIEW _S` records.
|
||||
# NOT ``doc.ActiveViewport.Center/Height/Width``: that object does
|
||||
# not track the display — measured (AutoCAD 2026) it still
|
||||
# answered the document's initial view after `_.ZOOM _C 5,6 20`,
|
||||
# after ``app.ZoomCenter`` and after its own values were written.
|
||||
if args["center"] is None or args["height"] is None or args["width"] is None:
|
||||
view_center = doc.GetVariable("VIEWCTR")
|
||||
view_height = float(doc.GetVariable("VIEWSIZE")) or 1.0
|
||||
aspect = self._com_display_aspect(doc)
|
||||
else:
|
||||
view_center, view_height, aspect = None, 1.0, 1.0
|
||||
cx, cy = args["center"] or (float(view_center[0]), float(view_center[1]))
|
||||
h = args["height"] or view_height
|
||||
w = args["width"] or h * aspect
|
||||
view = self._com_view(doc, clean)
|
||||
replaced = view is not None
|
||||
@@ -1695,14 +1723,26 @@ class ComBackend(AutoCADBackend):
|
||||
f"view_named_restore: no named view {clean!r}; saved views: {names}"
|
||||
)
|
||||
row = self._com_view_row(view)
|
||||
vport = doc.ActiveViewport
|
||||
vport.Center = _av(row["center"])
|
||||
vport.Height = row["height"]
|
||||
vport.Width = row["width"]
|
||||
# ActiveX applies viewport changes only when the object is assigned back.
|
||||
doc.ActiveViewport = vport
|
||||
_regen()
|
||||
return {"ok": True, **row, "applied": "active_viewport", "backend": "com"}
|
||||
# NOT ``vport.Center/Height/Width`` + ``doc.ActiveViewport = vport``:
|
||||
# AutoCAD reconciles a width that does not match the display aspect
|
||||
# by anchoring the viewport's lower-left corner and widening, so the
|
||||
# view lands off-centre (measured, AutoCAD 2026, display aspect
|
||||
# 2.013: a 10x20 view at (5, 6) restored to VIEWCTR (20.134, 6)).
|
||||
# `-VIEW _R` centres the saved window and fits it — VIEWCTR = the
|
||||
# saved centre, VIEWSIZE = max(height, width / aspect) — and so
|
||||
# does ZOOM Window on the same rectangle, without a SendCommand.
|
||||
(cx, cy), h, w = row["center"], row["height"], row["width"]
|
||||
_acad_app().ZoomWindow(
|
||||
_apoint(cx - w / 2.0, cy - h / 2.0), _apoint(cx + w / 2.0, cy + h / 2.0)
|
||||
)
|
||||
result = {"ok": True, **row, "applied": "zoom_window", "backend": "com"}
|
||||
try: # the read-back is what a reviewer measures the tool against
|
||||
ctr = doc.GetVariable("VIEWCTR")
|
||||
result["viewctr"] = [float(ctr[0]), float(ctr[1])]
|
||||
result["viewsize"] = float(doc.GetVariable("VIEWSIZE"))
|
||||
except Exception as exc:
|
||||
log.debug("VIEWCTR/VIEWSIZE read-back failed: %s", exc)
|
||||
return result
|
||||
|
||||
return await self._run(_sync)
|
||||
|
||||
@@ -1736,9 +1776,10 @@ class ComBackend(AutoCADBackend):
|
||||
from engineering.environment.ucs import WORLD, WORLD_ORIGIN, WORLD_X_AXIS, WORLD_Y_AXIS
|
||||
|
||||
def _sync():
|
||||
app = _acad_app()
|
||||
# GetVariable is an AcadDocument member; AcadApplication has none
|
||||
# (measured: ``hasattr(app, "GetVariable") is False``).
|
||||
doc = _acad_doc()
|
||||
current = str(app.GetVariable("UCSNAME") or "")
|
||||
current = str(doc.GetVariable("UCSNAME") or "")
|
||||
rows = [
|
||||
{
|
||||
"name": WORLD,
|
||||
@@ -1757,15 +1798,15 @@ class ComBackend(AutoCADBackend):
|
||||
# (UCS Origin / 3P without saving) as well as for WCS — verified
|
||||
# live (AutoCAD 2026: after `_.UCS _O 10,10,0`, UCSNAME="" and
|
||||
# WORLDUCS=0). WORLDUCS is AutoCAD's own answer to "is this WCS".
|
||||
if int(app.GetVariable("WORLDUCS")) == 1:
|
||||
if int(doc.GetVariable("WORLDUCS")) == 1:
|
||||
rows[0]["current"] = True
|
||||
else:
|
||||
rows.append(
|
||||
{
|
||||
"name": None,
|
||||
"origin": [float(c) for c in app.GetVariable("UCSORG")],
|
||||
"x_axis": [float(c) for c in app.GetVariable("UCSXDIR")],
|
||||
"y_axis": [float(c) for c in app.GetVariable("UCSYDIR")],
|
||||
"origin": [float(c) for c in doc.GetVariable("UCSORG")],
|
||||
"x_axis": [float(c) for c in doc.GetVariable("UCSXDIR")],
|
||||
"y_axis": [float(c) for c in doc.GetVariable("UCSYDIR")],
|
||||
"current": True,
|
||||
}
|
||||
)
|
||||
@@ -1820,16 +1861,20 @@ class ComBackend(AutoCADBackend):
|
||||
clean = validate_name(name, what="UCS name")
|
||||
|
||||
def _sync():
|
||||
app = _acad_app()
|
||||
doc = _acad_doc()
|
||||
if clean.lower() == WORLD:
|
||||
# No ActiveX member selects WCS; the command does. Same guard as
|
||||
# system_run_command: never send into an active prompt.
|
||||
# system_run_command: never send into an active prompt. The
|
||||
# read is a document member, and a failed read REFUSES: a
|
||||
# swallowed error here once forced cmd_active to 0 and sent
|
||||
# `_.UCS _W` into whatever prompt was open.
|
||||
try:
|
||||
cmd_active = int(app.GetVariable("CMDACTIVE"))
|
||||
cmd_active = int(doc.GetVariable("CMDACTIVE"))
|
||||
except Exception as exc:
|
||||
log.debug("CMDACTIVE read failed, proceeding: %s", exc)
|
||||
cmd_active = 0
|
||||
raise RuntimeError(
|
||||
"ucs_restore: cannot verify AutoCAD is idle (CMDACTIVE read "
|
||||
f"failed: {exc}); nothing was sent"
|
||||
) from exc
|
||||
if cmd_active:
|
||||
raise RuntimeError(
|
||||
"AutoCAD has an active command or prompt (CMDACTIVE="
|
||||
|
||||
@@ -76,7 +76,10 @@ class EnvironmentContract(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def view_named_restore(self, name: str) -> dict:
|
||||
"""ezdxf: sets the ``*Active`` VPORT, ``"applied": "header_only"``."""
|
||||
"""COM: ZOOM Window on the saved rectangle (``"applied": "zoom_window"``
|
||||
plus the read-back ``viewctr`` / ``viewsize``); ezdxf: fits the window
|
||||
into the ``*Active`` VPORT without touching its aspect ratio,
|
||||
``"applied": "header_only"`` plus ``vport_height`` / ``aspect_ratio``."""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
|
||||
@@ -2650,10 +2650,20 @@ class EzdxfBackend(AutoCADBackend):
|
||||
cx, cy = float(view.dxf.center.x), float(view.dxf.center.y)
|
||||
h, w = float(view.dxf.height), float(view.dxf.width)
|
||||
vport = self._active_vport(doc)
|
||||
# The *Active VPORT's aspect ratio is the DISPLAY's, written by
|
||||
# AutoCAD at its last save, and AutoCAD reconciles the stored
|
||||
# window against the real display by its lower-left corner: a
|
||||
# stored aspect that differs from the display's shifts the centre
|
||||
# by (display − stored) · height / 2. Measured (AutoCAD 2026,
|
||||
# display aspect 2.013): a 10x20 view at (5, 6) written with
|
||||
# aspect 0.5 opened at VIEWCTR (20.134, 6). So the aspect is left
|
||||
# as the file carries it and the window is FITTED into it —
|
||||
# height = max(h, w / aspect), the same rule `-VIEW _R` applies —
|
||||
# which is exactly what AutoCAD's own restore-then-save writes.
|
||||
aspect = float(vport.dxf.get("aspect_ratio", 1.34)) or 1.34
|
||||
fit_height = max(h, w / aspect)
|
||||
vport.dxf.center = (cx, cy)
|
||||
vport.dxf.height = h
|
||||
if h > 0:
|
||||
vport.dxf.aspect_ratio = w / h
|
||||
vport.dxf.height = fit_height
|
||||
# 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
|
||||
@@ -2674,6 +2684,8 @@ class EzdxfBackend(AutoCADBackend):
|
||||
# initial view when it opens the file. No live display moves.
|
||||
"applied": "header_only",
|
||||
"store": "vport_active",
|
||||
"vport_height": fit_height,
|
||||
"aspect_ratio": aspect,
|
||||
"backend": "ezdxf",
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user