feat(pagesetup): page_setup_list/apply on both engines — the PDF's own /MediaBox is the evidence
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
49fc6ac6a5
commit
f46fda10cb
@@ -30,6 +30,7 @@ from backends.contracts import (
|
||||
LayerContract,
|
||||
LayoutContract,
|
||||
LinetypeContract,
|
||||
PageSetupContract,
|
||||
PremiumContract,
|
||||
SettingsContract,
|
||||
SolidContract,
|
||||
@@ -168,6 +169,7 @@ class AutoCADBackend(
|
||||
IdentityContract,
|
||||
DrawingContract,
|
||||
LayoutContract,
|
||||
PageSetupContract,
|
||||
SolidContract,
|
||||
EntityCreationContract,
|
||||
DimensionContract,
|
||||
|
||||
@@ -811,6 +811,7 @@ class ComBackend(AutoCADBackend):
|
||||
"refiner": FeatureCapability(True, "shared"),
|
||||
"delivery": FeatureCapability(True, "shared"),
|
||||
"paper_space": FeatureCapability(True, "native"),
|
||||
"dwt_write": FeatureCapability(True, "native"),
|
||||
"viewport_render": FeatureCapability(True, "native"),
|
||||
"solid_3d": _solid_3d_capability(),
|
||||
"lisp": FeatureCapability(True, "sanitized"),
|
||||
@@ -1954,6 +1955,183 @@ class ComBackend(AutoCADBackend):
|
||||
# CopyObjects-and-delete that would destroy and recreate geometry in the
|
||||
# operator's open drawing when it goes wrong.
|
||||
|
||||
# ── page setup (track E) ─────────────────────────────────────────────────
|
||||
#
|
||||
# Layout is an IAcadPlotConfiguration. Constants from the AutoCAD 2026
|
||||
# typelib: AcPlotPaperUnits acMillimeters=1; AcPlotRotation ac0degrees=0;
|
||||
# AcPlotType acExtents=1 / acLayout=5; AcPlotScale acScaleToFit=0, ac1_1=16
|
||||
# (the full table is engineering.standards.papers.ACTIVEX_PLOT_SCALE).
|
||||
# Fake-tested in tests/test_page_setup.py; executed live by
|
||||
# scripts/smoke_settings_com.py (Task 24).
|
||||
|
||||
_AC_MILLIMETERS = 1
|
||||
_AC_NO_ROTATION = 0
|
||||
|
||||
@staticmethod
|
||||
def _com_page_setup_row(layout) -> dict:
|
||||
from engineering.standards.papers import (
|
||||
PLOT_TYPE_NAMES,
|
||||
activex_scale_label,
|
||||
paper_from_size,
|
||||
scale_label,
|
||||
)
|
||||
|
||||
def _get(attr):
|
||||
try:
|
||||
return getattr(layout, attr)
|
||||
except Exception as exc:
|
||||
log.debug("Layout.%s read failed: %s", attr, exc)
|
||||
return None
|
||||
|
||||
units = _get("PaperUnits")
|
||||
factor = 25.4 if units == 0 else 1.0
|
||||
size = None
|
||||
try:
|
||||
width, height = layout.GetPaperSize()
|
||||
size = [round(float(width) * factor, 2), round(float(height) * factor, 2)]
|
||||
except Exception as exc:
|
||||
log.debug("Layout.GetPaperSize failed: %s", exc)
|
||||
margins = None
|
||||
try:
|
||||
(left, bottom), (right, top) = layout.GetPaperMargins()
|
||||
margins = [round(float(v) * factor, 3) for v in (top, bottom, left, right)]
|
||||
except Exception as exc:
|
||||
log.debug("Layout.GetPaperMargins failed: %s", exc)
|
||||
rotation = _get("PlotRotation") or 0
|
||||
if size and int(rotation) in (1, 3):
|
||||
size = [size[1], size[0]]
|
||||
if _get("UseStandardScale"):
|
||||
scale = activex_scale_label(_get("StandardScale") or 0)
|
||||
else:
|
||||
try:
|
||||
numerator, denominator = layout.GetCustomScale()
|
||||
scale = scale_label(float(numerator), float(denominator))
|
||||
except Exception as exc:
|
||||
log.debug("Layout.GetCustomScale failed: %s", exc)
|
||||
scale = None
|
||||
media = _get("CanonicalMediaName")
|
||||
plot_type = _get("PlotType")
|
||||
return {
|
||||
"layout": layout.Name,
|
||||
"paper": (paper_from_size(*size) if size else None) or media,
|
||||
"canonical_media_name": media,
|
||||
"size_mm": size,
|
||||
"orientation": (
|
||||
None if not size else ("landscape" if size[0] >= size[1] else "portrait")
|
||||
),
|
||||
"plot_style": _get("StyleSheet"),
|
||||
"scale": scale,
|
||||
"plot_area": PLOT_TYPE_NAMES.get(plot_type, plot_type),
|
||||
"device": _get("ConfigName"),
|
||||
"margins_mm": margins,
|
||||
"center": bool(_get("CenterPlot")),
|
||||
}
|
||||
|
||||
def _com_paper_layout(self, doc, raw: str, operation: str):
|
||||
resolved = self._find_layout(doc, raw)
|
||||
if resolved is None:
|
||||
available = [n for n in self._layout_names(doc) if n != "Model"]
|
||||
raise ValueError(
|
||||
f"{operation}: layout {raw!r} not found (paper-space layouts: "
|
||||
f"{', '.join(available) or 'none'})"
|
||||
)
|
||||
if resolved == "Model":
|
||||
raise ValueError(
|
||||
f"{operation}: Model space has no sheet; plot it with "
|
||||
"drawing_export_pdf(layout=None) or apply the setup to a paper-space layout"
|
||||
)
|
||||
return doc.Layouts.Item(resolved)
|
||||
|
||||
async def page_setup_list(self, layout: str | None = None) -> list[dict]:
|
||||
def _sync():
|
||||
doc = _acad_doc()
|
||||
if layout is not None and self._layout_name(layout):
|
||||
target = self._com_paper_layout(doc, layout, "page_setup_list")
|
||||
return [self._com_page_setup_row(target)]
|
||||
return [
|
||||
self._com_page_setup_row(doc.Layouts.Item(name))
|
||||
for name in self._layout_names(doc)
|
||||
if name != "Model"
|
||||
]
|
||||
|
||||
return await self._run(_sync)
|
||||
|
||||
async def page_setup_apply(self, layout: str, setup: dict) -> dict:
|
||||
from engineering.standards.papers import require_page_setup
|
||||
|
||||
resolved = require_page_setup(setup)
|
||||
if resolved["margins_mm"] is not None:
|
||||
raise ValueError(
|
||||
"page_setup_apply: margins_mm cannot be written on the COM backend — AutoCAD "
|
||||
"takes the printable margins from the plotter configuration (.pc3). Omit "
|
||||
"margins_mm; the device's margins are read back in the result."
|
||||
)
|
||||
|
||||
def _sync():
|
||||
doc = _acad_doc()
|
||||
target = self._com_paper_layout(doc, layout, "page_setup_apply")
|
||||
before = self._com_page_setup_row(target)
|
||||
previous_device = before["device"]
|
||||
target.RefreshPlotDeviceInfo()
|
||||
target.ConfigName = resolved["device"]
|
||||
target.RefreshPlotDeviceInfo()
|
||||
media = resolved["canonical_media_name"]
|
||||
names = [str(n) for n in (target.GetCanonicalMediaNames() or ())]
|
||||
if media not in names:
|
||||
# Roll the one write back before refusing, so a refusal leaves
|
||||
# the layout as it was.
|
||||
target.ConfigName = previous_device
|
||||
nearest = [n for n in names if n.split("_(")[0] == media.split("_(")[0]]
|
||||
raise ValueError(
|
||||
f"page_setup_apply: device {resolved['device']!r} has no media "
|
||||
f"{media!r} (same paper on this device: {', '.join(nearest) or 'none'}; "
|
||||
f"{len(names)} media in total)"
|
||||
)
|
||||
target.CanonicalMediaName = media
|
||||
target.PaperUnits = self._AC_MILLIMETERS
|
||||
target.PlotRotation = self._AC_NO_ROTATION
|
||||
target.StyleSheet = resolved["plot_style"]
|
||||
target.PlotWithPlotStyles = True
|
||||
target.PlotType = int(resolved["plot_type"])
|
||||
code = resolved["activex_standard_scale"]
|
||||
if code is not None:
|
||||
target.StandardScale = int(code)
|
||||
target.UseStandardScale = True
|
||||
else:
|
||||
numerator, denominator = (float(v) for v in resolved["scale_ratio"])
|
||||
target.SetCustomScale(numerator, denominator)
|
||||
target.UseStandardScale = False
|
||||
target.CenterPlot = bool(resolved["center"])
|
||||
after = self._com_page_setup_row(target)
|
||||
changed = {
|
||||
key: [before[key], after[key]]
|
||||
for key in after
|
||||
if key != "layout" and before.get(key) != after[key]
|
||||
}
|
||||
return {
|
||||
"ok": True,
|
||||
"layout": target.Name,
|
||||
"applied": dict(resolved),
|
||||
"changed": changed,
|
||||
"plot_style_known": bool(resolved["plot_style_known"]),
|
||||
"viewports_kept": True,
|
||||
"backend": "com",
|
||||
}
|
||||
|
||||
return await self._run(_sync)
|
||||
|
||||
async def plot_style_list(self) -> list[dict]:
|
||||
# Completed in Task 18 (installed ctb scan); the catalogue rows already answer.
|
||||
from engineering.standards.papers import CTB_CATALOG
|
||||
|
||||
return [{"name": name, "source": "catalog", "installed": None} for name in CTB_CATALOG]
|
||||
|
||||
async def drawing_template_save(self, path, name=None, description=None) -> dict:
|
||||
# Completed in Task 20; declared here so the ABC instantiates.
|
||||
raise UnsupportedCapabilityError(
|
||||
"dwt_write", "drawing_template_save: not implemented yet (Task 20)"
|
||||
)
|
||||
|
||||
# ── 3D solids (native ActiveX; gated behind ENABLE_3D at the tool layer) ─
|
||||
|
||||
async def solid_box(
|
||||
|
||||
@@ -21,6 +21,7 @@ from backends.contracts.identity import IdentityContract
|
||||
from backends.contracts.layers import LayerContract
|
||||
from backends.contracts.layouts import LayoutContract
|
||||
from backends.contracts.linetypes import LinetypeContract
|
||||
from backends.contracts.pagesetup import PageSetupContract
|
||||
from backends.contracts.premium import PremiumContract
|
||||
from backends.contracts.settings import SettingsContract
|
||||
from backends.contracts.solids import SolidContract
|
||||
@@ -42,6 +43,7 @@ __all__ = [
|
||||
"LayerContract",
|
||||
"LayoutContract",
|
||||
"LinetypeContract",
|
||||
"PageSetupContract",
|
||||
"PremiumContract",
|
||||
"SettingsContract",
|
||||
"SolidContract",
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
"""Page setup, plot styles and templates (v1.6 track E, group P).
|
||||
|
||||
``AutoCADBackend`` composes every contract in this package, so importing
|
||||
``backends.base.AutoCADBackend`` is unchanged for callers.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
|
||||
class PageSetupContract(ABC):
|
||||
@abstractmethod
|
||||
async def page_setup_list(self, layout: str | None = None) -> list[dict]:
|
||||
"""Page setup of one paper-space layout, or of every one.
|
||||
|
||||
Rows: ``{layout, paper, canonical_media_name, size_mm: [w, h],
|
||||
orientation, plot_style, scale, plot_area, device,
|
||||
margins_mm: [top, bottom, left, right], center}``. ``paper`` is the
|
||||
catalogue name when the size matches one within 0.5 mm, otherwise the
|
||||
media name as stored. Raises ``ValueError`` for an unknown layout.
|
||||
``Model`` is never listed: model space is plotted through
|
||||
``drawing_export_pdf(layout=None)`` and carries no sheet.
|
||||
"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
async def page_setup_apply(self, layout: str, setup: dict) -> dict:
|
||||
"""Write a page setup resolved by
|
||||
``engineering.standards.papers.resolve_page_setup`` to a layout.
|
||||
|
||||
Returns ``{ok, layout, applied: {...setup}, changed: {key: [old, new]},
|
||||
plot_style_known, viewports_kept}``. ``changed`` is computed from the
|
||||
read-back before and after, so re-applying the same setup reports
|
||||
``{}``. Refuses ``Model``, an unknown layout, and a ``setup`` missing a
|
||||
resolved key, before anything is written. Never deletes viewports.
|
||||
"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
async def plot_style_list(self) -> list[dict]:
|
||||
"""``[{name, source: "catalog"|"installed", installed: bool|None}]`` —
|
||||
the authored ctb catalogue plus, on a live engine, the files present
|
||||
in the plot style search path. ``installed`` is ``None`` where no
|
||||
installation can be scanned."""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
async def drawing_template_save(
|
||||
self, path: str, name: str | None = None, description: str | None = None
|
||||
) -> dict:
|
||||
"""Save the current drawing as a template.
|
||||
|
||||
``.dxf`` writes on both engines; ``.dwt`` writes on the live engine
|
||||
(``SaveAs(path, ac2018_Template)``) and raises
|
||||
``UnsupportedCapabilityError("dwt_write")`` headlessly. Returns
|
||||
``{ok, path, format: "dxf"|"dwt", backend, name, description_written}``.
|
||||
"""
|
||||
...
|
||||
+197
-2
@@ -1044,6 +1044,9 @@ class EzdxfBackend(AutoCADBackend):
|
||||
"refiner": FeatureCapability(True, "shared"),
|
||||
"delivery": FeatureCapability(True, "shared"),
|
||||
"paper_space": FeatureCapability(True, "native"),
|
||||
"dwt_write": FeatureCapability(
|
||||
False, reason="dwt_is_a_dwg_container_requires_live_autocad"
|
||||
),
|
||||
# Measured, not assumed: a sheet holding nothing but a viewport
|
||||
# renders the model geometry that viewport looks at, at its
|
||||
# scale and position, clipped to its window. v1.4 declared this
|
||||
@@ -1525,13 +1528,29 @@ class EzdxfBackend(AutoCADBackend):
|
||||
from ezdxf.addons.drawing import Frontend, RenderContext
|
||||
from ezdxf.addons.drawing.matplotlib import MatplotlibBackend
|
||||
|
||||
fig = _new_agg_figure()
|
||||
# Track E: for a sheet, the figure *is* the paper. Sized from
|
||||
# the layout's page setup and left unadjusted, so the PDF's
|
||||
# /MediaBox is the paper size — the evidence page_setup_apply
|
||||
# is judged by. Measured: adjust_figure=True gave 691.2 x 345.6
|
||||
# pt whatever the sheet was.
|
||||
paper = None
|
||||
if resolved is not None and resolved != "Model":
|
||||
paper = self._paper_frame(target)
|
||||
fig = _new_agg_figure(figsize=paper["figsize"] if paper else None)
|
||||
ax = fig.add_axes([0, 0, 1, 1])
|
||||
ctx = RenderContext(doc)
|
||||
out = MatplotlibBackend(ax)
|
||||
out = MatplotlibBackend(ax, adjust_figure=paper is None)
|
||||
Frontend(ctx, out).draw_layout(target, finalize=True)
|
||||
if paper is not None:
|
||||
ax.set_xlim(*paper["xlim"])
|
||||
ax.set_ylim(*paper["ylim"])
|
||||
ax.set_aspect("equal", adjustable="box")
|
||||
fig.savefig(path, dpi=150)
|
||||
result = {"ok": True, "path": path}
|
||||
if paper is not None:
|
||||
result["paper_mm"] = paper["size_mm"]
|
||||
if not paper["rotation_applied"]:
|
||||
result["rotation_applied"] = False
|
||||
if resolved is not None and resolved != "Model":
|
||||
result["layout"] = resolved
|
||||
result["note"] = (
|
||||
@@ -2294,6 +2313,182 @@ class EzdxfBackend(AutoCADBackend):
|
||||
text = f"{float(value):.4f}".rstrip("0").rstrip(".")
|
||||
return text or "0"
|
||||
|
||||
# ── page setup (track E) ─────────────────────────────────────────────────
|
||||
|
||||
def _paper_layout(self, raw: str, operation: str):
|
||||
"""Resolve a paper-space layout by name, refusing Model and unknowns."""
|
||||
doc = self._require_doc()
|
||||
resolved = self._find_layout(raw)
|
||||
if resolved is None:
|
||||
available = [n for n in doc.layouts.names_in_taborder() if n != "Model"]
|
||||
raise ValueError(
|
||||
f"{operation}: layout {raw!r} not found (paper-space layouts: "
|
||||
f"{', '.join(available) or 'none'})"
|
||||
)
|
||||
if resolved == "Model":
|
||||
raise ValueError(
|
||||
f"{operation}: Model space has no sheet; plot it with "
|
||||
"drawing_export_pdf(layout=None) or apply the setup to a paper-space layout"
|
||||
)
|
||||
return doc.layouts.get(resolved)
|
||||
|
||||
@staticmethod
|
||||
def _page_setup_row(layout) -> dict:
|
||||
from engineering.standards.papers import (
|
||||
PLOT_TYPE_NAMES,
|
||||
dxf_scale_label,
|
||||
paper_from_size,
|
||||
scale_label,
|
||||
)
|
||||
|
||||
dxf = layout.dxf_layout.dxf
|
||||
flags = int(dxf.get("plot_layout_flags", dxf.get_default("plot_layout_flags")))
|
||||
# DXF stores every paper size and margin in millimetres, whatever
|
||||
# plot_paper_units says (ezdxf's acdb_plot_settings, verified).
|
||||
width, height = float(dxf.paper_width), float(dxf.paper_height)
|
||||
rotation = int(dxf.plot_rotation)
|
||||
if rotation in (1, 3):
|
||||
width, height = height, width
|
||||
media = str(dxf.paper_size)
|
||||
numerator, denominator = float(dxf.scale_numerator), float(dxf.scale_denominator)
|
||||
if flags & layout.USE_STANDARD_SCALE:
|
||||
scale = dxf_scale_label(int(dxf.standard_scale_type), numerator, denominator)
|
||||
else:
|
||||
scale = scale_label(numerator, denominator)
|
||||
return {
|
||||
"layout": layout.name,
|
||||
"paper": paper_from_size(width, height) or media,
|
||||
"canonical_media_name": media,
|
||||
"size_mm": [round(width, 2), round(height, 2)],
|
||||
"orientation": "landscape" if width >= height else "portrait",
|
||||
"plot_style": str(dxf.current_style_sheet),
|
||||
"scale": scale,
|
||||
"plot_area": PLOT_TYPE_NAMES.get(int(dxf.plot_type), str(dxf.plot_type)),
|
||||
"device": str(dxf.plot_configuration_file),
|
||||
"margins_mm": [
|
||||
round(float(dxf.top_margin), 3),
|
||||
round(float(dxf.bottom_margin), 3),
|
||||
round(float(dxf.left_margin), 3),
|
||||
round(float(dxf.right_margin), 3),
|
||||
],
|
||||
"center": bool(flags & layout.PLOT_CENTERED),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _paper_frame(layout) -> dict:
|
||||
"""The sheet as a matplotlib figure: size in inches plus the axes window.
|
||||
|
||||
Used by ``drawing_export_pdf`` so the PDF's /MediaBox *is* the paper.
|
||||
``plot_rotation`` 1/3 swaps the figure; the content is not rotated by
|
||||
the headless renderer (reported, not hidden).
|
||||
"""
|
||||
dxf = layout.dxf_layout.dxf
|
||||
width, height = float(dxf.paper_width), float(dxf.paper_height)
|
||||
rotation = int(dxf.plot_rotation)
|
||||
unit = 25.4 if int(dxf.plot_paper_units) == 0 else 1.0
|
||||
x0 = -(float(dxf.left_margin) + float(dxf.plot_origin_x_offset))
|
||||
y0 = -(float(dxf.bottom_margin) + float(dxf.plot_origin_y_offset))
|
||||
fig_w, fig_h = (height, width) if rotation in (1, 3) else (width, height)
|
||||
return {
|
||||
"size_mm": [round(fig_w, 2), round(fig_h, 2)],
|
||||
"figsize": (fig_w / 25.4, fig_h / 25.4),
|
||||
"xlim": (x0 / unit, (x0 + width) / unit),
|
||||
"ylim": (y0 / unit, (y0 + height) / unit),
|
||||
"rotation_applied": rotation == 0,
|
||||
}
|
||||
|
||||
async def page_setup_list(self, layout: str | None = None) -> list[dict]:
|
||||
def _sync():
|
||||
doc = self._require_doc()
|
||||
if layout is not None and self._layout_name(layout):
|
||||
return [self._page_setup_row(self._paper_layout(layout, "page_setup_list"))]
|
||||
return [
|
||||
self._page_setup_row(doc.layouts.get(name))
|
||||
for name in doc.layouts.names_in_taborder()
|
||||
if name != "Model"
|
||||
]
|
||||
|
||||
return await self._async(_sync)
|
||||
|
||||
async def page_setup_apply(self, layout: str, setup: dict) -> dict:
|
||||
from engineering.standards.papers import require_page_setup
|
||||
|
||||
resolved = require_page_setup(setup)
|
||||
|
||||
def _sync():
|
||||
from ezdxf.lldxf.const import STD_SCALES
|
||||
|
||||
target = self._paper_layout(layout, "page_setup_apply")
|
||||
before = self._page_setup_row(target)
|
||||
dxf = target.dxf_layout.dxf
|
||||
# Measured: a fresh layout has no stored plot_layout_flags, and
|
||||
# set_flag_state starts from 0 — toggling one bit would drop
|
||||
# plot-styles (32), lineweights (128) and viewports-first (512).
|
||||
if not dxf.hasattr("plot_layout_flags"):
|
||||
dxf.plot_layout_flags = dxf.get_default("plot_layout_flags")
|
||||
width, height = (float(v) for v in resolved["size_mm"])
|
||||
dxf.page_setup_name = ""
|
||||
dxf.plot_configuration_file = resolved["device"]
|
||||
dxf.paper_size = resolved["canonical_media_name"]
|
||||
dxf.paper_width = width
|
||||
dxf.paper_height = height
|
||||
dxf.plot_paper_units = 1 # millimetres
|
||||
dxf.plot_rotation = 0 # orientation rides on width/height, as AutoCAD names media
|
||||
if resolved["margins_mm"] is not None:
|
||||
top, bottom, left, right = (float(v) for v in resolved["margins_mm"])
|
||||
dxf.top_margin, dxf.bottom_margin = top, bottom
|
||||
dxf.left_margin, dxf.right_margin = left, right
|
||||
dxf.plot_origin_x_offset = 0.0
|
||||
dxf.plot_origin_y_offset = 0.0
|
||||
dxf.unit_factor = 1.0
|
||||
dxf.current_style_sheet = resolved["plot_style"]
|
||||
target.use_plot_styles(True)
|
||||
target.set_plot_type(int(resolved["plot_type"]))
|
||||
code = resolved["dxf_standard_scale_type"]
|
||||
if code is not None:
|
||||
dxf.standard_scale_type = int(code)
|
||||
numerator, denominator = STD_SCALES.get(int(code), (1.0, 1.0))
|
||||
dxf.scale_numerator, dxf.scale_denominator = float(numerator), float(denominator)
|
||||
target.use_standard_scale(True)
|
||||
else:
|
||||
numerator, denominator = (float(v) for v in resolved["scale_ratio"])
|
||||
dxf.standard_scale_type = 16
|
||||
dxf.scale_numerator, dxf.scale_denominator = numerator, denominator
|
||||
target.use_standard_scale(False)
|
||||
target.plot_centered(bool(resolved["center"]))
|
||||
# Limits follow the paper; viewports are deliberately left alone
|
||||
# (Paperspace.page_setup() would delete them).
|
||||
target.reset_paper_limits()
|
||||
self._mark_dirty()
|
||||
after = self._page_setup_row(target)
|
||||
changed = {
|
||||
key: [before[key], after[key]]
|
||||
for key in after
|
||||
if key != "layout" and before.get(key) != after[key]
|
||||
}
|
||||
return {
|
||||
"ok": True,
|
||||
"layout": target.name,
|
||||
"applied": dict(resolved),
|
||||
"changed": changed,
|
||||
"plot_style_known": bool(resolved["plot_style_known"]),
|
||||
"viewports_kept": True,
|
||||
"backend": "ezdxf",
|
||||
}
|
||||
|
||||
return await self._async(_sync)
|
||||
|
||||
async def plot_style_list(self) -> list[dict]:
|
||||
from engineering.standards.papers import CTB_CATALOG
|
||||
|
||||
return [{"name": name, "source": "catalog", "installed": None} for name in CTB_CATALOG]
|
||||
|
||||
async def drawing_template_save(self, path, name=None, description=None) -> dict:
|
||||
# Completed in Task 20; declared here so the ABC instantiates.
|
||||
raise UnsupportedCapabilityError(
|
||||
"dwt_write", "drawing_template_save: not implemented yet (Task 20)"
|
||||
)
|
||||
|
||||
# ── 3D solids (unsupported headlessly — honest capability boundary) ─────
|
||||
|
||||
@staticmethod
|
||||
|
||||
Reference in New Issue
Block a user