feat(settings): SYSVAR_CATALOG + system_variable_describe; system_set_variable refuses out-of-range values
90 authored rows (type, range/enum, default, meaning, saved-in, per-engine support, friendly key); every ezdxf claim is checked against the real backend and the registry-saved set is pinned to what the backend refuses. 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
f3ff703144
commit
125a8ed3b8
@@ -27,7 +27,7 @@ Live through COM on Windows, or headless through ezdxf anywhere — one typed co
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
> **v1.5 release snapshot:** 166 tools · 8 resources · 5 prompt templates · 2423 collected tests.
|
> **v1.5 release snapshot:** 167 tools · 8 resources · 5 prompt templates · 2423 collected tests.
|
||||||
> 166 is the **registered** count; a default install advertises 161 over `tools/list`,
|
> 166 is the **registered** count; a default install advertises 161 over `tools/list`,
|
||||||
> because `ENABLE_3D` is unset. `system_about` is the runtime authority.
|
> because `ENABLE_3D` is unset. `system_about` is the runtime authority.
|
||||||
|
|
||||||
|
|||||||
@@ -1313,6 +1313,18 @@ TOOL_ALIASES: dict[str, ToolAliases] = {
|
|||||||
"health check",
|
"health check",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
"system_variable_describe": ToolAliases(
|
||||||
|
acad=("SETVAR", "SYSVARMONITOR", "SYSVDLG"),
|
||||||
|
synonyms=(
|
||||||
|
"sysvar",
|
||||||
|
"what does ltscale do",
|
||||||
|
"explain a system variable",
|
||||||
|
"system variable reference",
|
||||||
|
"which variables are saved in the drawing",
|
||||||
|
"valid range of a system variable",
|
||||||
|
"list system variables",
|
||||||
|
),
|
||||||
|
),
|
||||||
# ── Transactions ────────────────────────────────────────────────────────
|
# ── Transactions ────────────────────────────────────────────────────────
|
||||||
"transaction_begin": ToolAliases(
|
"transaction_begin": ToolAliases(
|
||||||
synonyms=(
|
synonyms=(
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"schema_version": 1,
|
"schema_version": 1,
|
||||||
"package_version": "1.5.1",
|
"package_version": "1.5.1",
|
||||||
"totals": {
|
"totals": {
|
||||||
"tools": 166,
|
"tools": 167,
|
||||||
"resources": 8,
|
"resources": 8,
|
||||||
"prompts": 5,
|
"prompts": 5,
|
||||||
"groups": 20
|
"groups": 20
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
"pid": 9,
|
"pid": 9,
|
||||||
"premium": 12,
|
"premium": 12,
|
||||||
"solids": 5,
|
"solids": 5,
|
||||||
"system": 7,
|
"system": 8,
|
||||||
"templates": 2,
|
"templates": 2,
|
||||||
"transactions": 3,
|
"transactions": 3,
|
||||||
"validation": 1,
|
"validation": 1,
|
||||||
@@ -2099,6 +2099,20 @@
|
|||||||
"readOnlyHint": true
|
"readOnlyHint": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "system_variable_describe",
|
||||||
|
"group": "system",
|
||||||
|
"cost": "read",
|
||||||
|
"summary": "Explain a system variable: type, range, default, where it is saved, which engine honours it.",
|
||||||
|
"tags": [
|
||||||
|
"settings",
|
||||||
|
"system"
|
||||||
|
],
|
||||||
|
"annotations": {
|
||||||
|
"title": "Describe System Variable",
|
||||||
|
"readOnlyHint": true
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "template_apply_layers",
|
"name": "template_apply_layers",
|
||||||
"group": "templates",
|
"group": "templates",
|
||||||
|
|||||||
@@ -0,0 +1,772 @@
|
|||||||
|
"""AutoCAD system-variable catalogue: what each one is, where it lives, who honours it.
|
||||||
|
|
||||||
|
Authored data, pinned by ``tests/test_sysvar_catalog.py``. Every entry says
|
||||||
|
its type, range or enum, AutoCAD's default, a one-line meaning, where AutoCAD
|
||||||
|
saves it (``drawing`` / ``registry`` / ``not_saved``), whether each engine can
|
||||||
|
read *and write* it (``engines``), and the ``drawing_settings`` key that wraps
|
||||||
|
it when one exists (``friendly_key``).
|
||||||
|
|
||||||
|
``engines["ezdxf"]`` is a measured claim, not a hope: it is ``True`` only when
|
||||||
|
the headless backend has a real home for the value in the DXF file - a header
|
||||||
|
variable ezdxf will export, the active VPORT (grid/snap), the variable
|
||||||
|
dictionary (CANNOSCALE) or the model-space layout (limits). Registry-saved
|
||||||
|
and never-saved variables are ``False`` because a file has nowhere to keep
|
||||||
|
them, and ``EzdxfBackend.system_set_variable`` refuses them with
|
||||||
|
``capability: registry_sysvar``. A handful of drawing-saved variables are also
|
||||||
|
``False`` because ezdxf 1.4 has no header slot for them (CTAB, VIEWCTR,
|
||||||
|
VIEWSIZE, ISOLINES, FACETRES); that refusal is a plain ``ValueError``.
|
||||||
|
|
||||||
|
Sources: the AutoCAD 2026 System Variables reference (type / saved-in /
|
||||||
|
initial value columns), the DXF Reference HEADER and VPORT sections, and
|
||||||
|
``ezdxf.sections.headervars.HEADER_VAR_MAP`` for what the headless engine
|
||||||
|
exports.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import difflib
|
||||||
|
import math
|
||||||
|
from dataclasses import asdict, dataclass, field
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"SAVED_IN",
|
||||||
|
"SYSVAR_CATALOG",
|
||||||
|
"SYSVAR_TYPES",
|
||||||
|
"SysVar",
|
||||||
|
"check_sysvar_value",
|
||||||
|
"describe_sysvar",
|
||||||
|
"search_sysvars",
|
||||||
|
]
|
||||||
|
|
||||||
|
SYSVAR_TYPES = ("int", "float", "str", "bool", "point", "enum")
|
||||||
|
SAVED_IN = ("drawing", "registry", "not_saved")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class SysVar:
|
||||||
|
name: str
|
||||||
|
type: str
|
||||||
|
range: tuple[float, float] | None
|
||||||
|
enum: dict[int, str] | None
|
||||||
|
default: Any
|
||||||
|
meaning: str
|
||||||
|
saved_in: str
|
||||||
|
engines: dict[str, bool] = field(default_factory=lambda: {"ezdxf": True, "com": True})
|
||||||
|
friendly_key: str | None = None
|
||||||
|
read_only: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
_BOTH = {"ezdxf": True, "com": True}
|
||||||
|
_COM_ONLY = {"ezdxf": False, "com": True}
|
||||||
|
|
||||||
|
|
||||||
|
def _v(
|
||||||
|
name: str,
|
||||||
|
type_: str,
|
||||||
|
meaning: str,
|
||||||
|
*,
|
||||||
|
range_: tuple[float, float] | None = None,
|
||||||
|
enum: dict[int, str] | None = None,
|
||||||
|
default: Any = None,
|
||||||
|
saved_in: str = "drawing",
|
||||||
|
engines: dict[str, bool] | None = None,
|
||||||
|
friendly_key: str | None = None,
|
||||||
|
read_only: bool = False,
|
||||||
|
) -> SysVar:
|
||||||
|
if engines is None:
|
||||||
|
engines = _BOTH if saved_in == "drawing" else _COM_ONLY
|
||||||
|
return SysVar(
|
||||||
|
name=name,
|
||||||
|
type=type_,
|
||||||
|
range=range_,
|
||||||
|
enum=enum,
|
||||||
|
default=default,
|
||||||
|
meaning=meaning,
|
||||||
|
saved_in=saved_in,
|
||||||
|
engines=dict(engines),
|
||||||
|
friendly_key=friendly_key,
|
||||||
|
read_only=read_only,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
_POSITIVE = (1e-9, 1e9)
|
||||||
|
_NON_NEGATIVE = (0.0, 1e9)
|
||||||
|
_ACI = (0, 256)
|
||||||
|
_LINEWEIGHT = (-3, 211)
|
||||||
|
|
||||||
|
_ENTRIES: tuple[SysVar, ...] = (
|
||||||
|
# ── dimension variables (the dimstyle whitelist) ────────────────────────
|
||||||
|
_v(
|
||||||
|
"DIMTXT",
|
||||||
|
"float",
|
||||||
|
"dimension text height",
|
||||||
|
range_=_POSITIVE,
|
||||||
|
default=2.5,
|
||||||
|
friendly_key="dim_text_height",
|
||||||
|
),
|
||||||
|
_v(
|
||||||
|
"DIMASZ",
|
||||||
|
"float",
|
||||||
|
"arrowhead size",
|
||||||
|
range_=_POSITIVE,
|
||||||
|
default=2.5,
|
||||||
|
friendly_key="dim_arrow_size",
|
||||||
|
),
|
||||||
|
_v(
|
||||||
|
"DIMEXE",
|
||||||
|
"float",
|
||||||
|
"extension line extension beyond the dimension line",
|
||||||
|
range_=_POSITIVE,
|
||||||
|
default=1.25,
|
||||||
|
),
|
||||||
|
_v(
|
||||||
|
"DIMEXO",
|
||||||
|
"float",
|
||||||
|
"extension line offset from the origin points",
|
||||||
|
range_=_NON_NEGATIVE,
|
||||||
|
default=0.625,
|
||||||
|
),
|
||||||
|
_v(
|
||||||
|
"DIMGAP",
|
||||||
|
"float",
|
||||||
|
"gap between dimension line and text; a negative value draws a box around the text",
|
||||||
|
default=0.625,
|
||||||
|
),
|
||||||
|
_v(
|
||||||
|
"DIMTAD",
|
||||||
|
"enum",
|
||||||
|
"vertical text placement relative to the dimension line",
|
||||||
|
enum={0: "centered", 1: "above", 2: "outside", 3: "JIS", 4: "below"},
|
||||||
|
default=1,
|
||||||
|
),
|
||||||
|
_v(
|
||||||
|
"DIMTIH",
|
||||||
|
"bool",
|
||||||
|
"text inside the extension lines is horizontal (1) or aligned with the line (0)",
|
||||||
|
default=0,
|
||||||
|
),
|
||||||
|
_v(
|
||||||
|
"DIMTOH",
|
||||||
|
"bool",
|
||||||
|
"text outside the extension lines is horizontal (1) or aligned with the line (0)",
|
||||||
|
default=0,
|
||||||
|
),
|
||||||
|
_v(
|
||||||
|
"DIMDEC",
|
||||||
|
"int",
|
||||||
|
"decimal places of the primary dimension value",
|
||||||
|
range_=(0, 8),
|
||||||
|
default=2,
|
||||||
|
friendly_key="dim_decimals",
|
||||||
|
),
|
||||||
|
_v(
|
||||||
|
"DIMDSEP",
|
||||||
|
"str",
|
||||||
|
"decimal separator character for decimal dimensions (the headless engine stores its character code: 44 is ',')",
|
||||||
|
default=",",
|
||||||
|
friendly_key="decimal_separator",
|
||||||
|
),
|
||||||
|
_v(
|
||||||
|
"DIMLUNIT",
|
||||||
|
"enum",
|
||||||
|
"linear unit format for dimensions",
|
||||||
|
enum={
|
||||||
|
1: "scientific",
|
||||||
|
2: "decimal",
|
||||||
|
3: "engineering",
|
||||||
|
4: "architectural",
|
||||||
|
5: "fractional",
|
||||||
|
6: "windows desktop",
|
||||||
|
},
|
||||||
|
default=2,
|
||||||
|
),
|
||||||
|
_v(
|
||||||
|
"DIMZIN",
|
||||||
|
"int",
|
||||||
|
"zero suppression bitmask: 1 feet, 2 inches, 4 leading, 8 trailing",
|
||||||
|
range_=(0, 15),
|
||||||
|
default=8,
|
||||||
|
friendly_key="zero_suppression",
|
||||||
|
),
|
||||||
|
_v("DIMBLK", "str", "arrowhead block name; empty means closed filled", default=""),
|
||||||
|
_v("DIMTXSTY", "str", "text style used by dimension text", default="Standard"),
|
||||||
|
_v(
|
||||||
|
"DIMLWD",
|
||||||
|
"int",
|
||||||
|
"dimension line lineweight code (-3 default, -2 ByBlock, -1 ByLayer, else hundredths of mm)",
|
||||||
|
range_=_LINEWEIGHT,
|
||||||
|
default=-2,
|
||||||
|
),
|
||||||
|
_v(
|
||||||
|
"DIMLWE",
|
||||||
|
"int",
|
||||||
|
"extension line lineweight code (-3 default, -2 ByBlock, -1 ByLayer, else hundredths of mm)",
|
||||||
|
range_=_LINEWEIGHT,
|
||||||
|
default=-2,
|
||||||
|
),
|
||||||
|
_v(
|
||||||
|
"DIMSCALE",
|
||||||
|
"float",
|
||||||
|
"overall scale factor applied to dimension sizes; 0 scales to the viewport",
|
||||||
|
range_=(0.0, 1e6),
|
||||||
|
default=1.0,
|
||||||
|
friendly_key="dimscale",
|
||||||
|
),
|
||||||
|
_v("DIMTOL", "bool", "append tolerances to dimension text", default=0),
|
||||||
|
_v("DIMTP", "float", "upper tolerance limit", default=0.0),
|
||||||
|
_v("DIMTM", "float", "lower tolerance limit", default=0.0),
|
||||||
|
_v(
|
||||||
|
"DIMTOLJ",
|
||||||
|
"enum",
|
||||||
|
"vertical justification of tolerance text",
|
||||||
|
enum={0: "bottom", 1: "middle", 2: "top"},
|
||||||
|
default=1,
|
||||||
|
),
|
||||||
|
_v(
|
||||||
|
"DIMTFAC",
|
||||||
|
"float",
|
||||||
|
"tolerance text height as a factor of DIMTXT",
|
||||||
|
range_=_POSITIVE,
|
||||||
|
default=1.0,
|
||||||
|
),
|
||||||
|
_v(
|
||||||
|
"DIMLFAC",
|
||||||
|
"float",
|
||||||
|
"linear measurement scale factor; negative applies in paper space only",
|
||||||
|
default=1.0,
|
||||||
|
),
|
||||||
|
_v(
|
||||||
|
"DIMRND",
|
||||||
|
"float",
|
||||||
|
"rounding increment for linear dimensions; 0 rounds nothing",
|
||||||
|
range_=_NON_NEGATIVE,
|
||||||
|
default=0.0,
|
||||||
|
),
|
||||||
|
_v(
|
||||||
|
"DIMATFIT",
|
||||||
|
"enum",
|
||||||
|
"what moves outside when text and arrows do not fit",
|
||||||
|
enum={0: "text and arrows", 1: "arrows first", 2: "text first", 3: "best fit"},
|
||||||
|
default=3,
|
||||||
|
),
|
||||||
|
_v(
|
||||||
|
"DIMTMOVE",
|
||||||
|
"enum",
|
||||||
|
"how the dimension line behaves when text is moved",
|
||||||
|
enum={0: "move line with text", 1: "add a leader", 2: "text moves freely"},
|
||||||
|
default=0,
|
||||||
|
),
|
||||||
|
_v(
|
||||||
|
"DIMCLRD",
|
||||||
|
"int",
|
||||||
|
"dimension line colour (ACI: 0 ByBlock, 256 ByLayer)",
|
||||||
|
range_=_ACI,
|
||||||
|
default=0,
|
||||||
|
),
|
||||||
|
_v(
|
||||||
|
"DIMCLRE",
|
||||||
|
"int",
|
||||||
|
"extension line colour (ACI: 0 ByBlock, 256 ByLayer)",
|
||||||
|
range_=_ACI,
|
||||||
|
default=0,
|
||||||
|
),
|
||||||
|
_v(
|
||||||
|
"DIMCLRT",
|
||||||
|
"int",
|
||||||
|
"dimension text colour (ACI: 0 ByBlock, 256 ByLayer)",
|
||||||
|
range_=_ACI,
|
||||||
|
default=0,
|
||||||
|
),
|
||||||
|
_v("DIMSAH", "bool", "use DIMBLK1 / DIMBLK2 as separate arrowheads", default=0),
|
||||||
|
_v("DIMBLK1", "str", "first arrowhead block when DIMSAH is on", default=""),
|
||||||
|
_v("DIMBLK2", "str", "second arrowhead block when DIMSAH is on", default=""),
|
||||||
|
_v(
|
||||||
|
"DIMCEN",
|
||||||
|
"float",
|
||||||
|
"centre mark size for radial dimensions; 0 none, negative draws centre lines",
|
||||||
|
default=2.5,
|
||||||
|
),
|
||||||
|
# ── scales, units, precision ─────────────────────────────────────────────
|
||||||
|
_v(
|
||||||
|
"LTSCALE",
|
||||||
|
"float",
|
||||||
|
"global linetype scale factor",
|
||||||
|
range_=_POSITIVE,
|
||||||
|
default=1.0,
|
||||||
|
friendly_key="ltscale",
|
||||||
|
),
|
||||||
|
_v(
|
||||||
|
"PSLTSCALE",
|
||||||
|
"bool",
|
||||||
|
"scale linetypes by the viewport scale in paper space",
|
||||||
|
default=1,
|
||||||
|
friendly_key="psltscale",
|
||||||
|
),
|
||||||
|
_v(
|
||||||
|
"INSUNITS",
|
||||||
|
"enum",
|
||||||
|
"drawing units for inserted content and scaling",
|
||||||
|
enum={
|
||||||
|
0: "unitless",
|
||||||
|
1: "inches",
|
||||||
|
2: "feet",
|
||||||
|
3: "miles",
|
||||||
|
4: "millimeters",
|
||||||
|
5: "centimeters",
|
||||||
|
6: "meters",
|
||||||
|
7: "kilometers",
|
||||||
|
8: "microinches",
|
||||||
|
9: "mils",
|
||||||
|
10: "yards",
|
||||||
|
11: "angstroms",
|
||||||
|
12: "nanometers",
|
||||||
|
13: "microns",
|
||||||
|
14: "decimeters",
|
||||||
|
15: "decameters",
|
||||||
|
16: "hectometers",
|
||||||
|
17: "gigameters",
|
||||||
|
18: "astronomical units",
|
||||||
|
19: "light years",
|
||||||
|
20: "parsecs",
|
||||||
|
},
|
||||||
|
default=4,
|
||||||
|
friendly_key="units",
|
||||||
|
),
|
||||||
|
_v(
|
||||||
|
"LUNITS",
|
||||||
|
"enum",
|
||||||
|
"linear unit display format",
|
||||||
|
enum={1: "scientific", 2: "decimal", 3: "engineering", 4: "architectural", 5: "fractional"},
|
||||||
|
default=2,
|
||||||
|
friendly_key="linear_units",
|
||||||
|
),
|
||||||
|
_v(
|
||||||
|
"LUPREC",
|
||||||
|
"int",
|
||||||
|
"linear unit display precision (decimal places)",
|
||||||
|
range_=(0, 8),
|
||||||
|
default=4,
|
||||||
|
friendly_key="linear_precision",
|
||||||
|
),
|
||||||
|
_v(
|
||||||
|
"AUNITS",
|
||||||
|
"enum",
|
||||||
|
"angular unit display format",
|
||||||
|
enum={
|
||||||
|
0: "decimal degrees",
|
||||||
|
1: "degrees/minutes/seconds",
|
||||||
|
2: "grads",
|
||||||
|
3: "radians",
|
||||||
|
4: "surveyor",
|
||||||
|
},
|
||||||
|
default=0,
|
||||||
|
friendly_key="angular_units",
|
||||||
|
),
|
||||||
|
_v(
|
||||||
|
"AUPREC",
|
||||||
|
"int",
|
||||||
|
"angular unit display precision (decimal places)",
|
||||||
|
range_=(0, 8),
|
||||||
|
default=0,
|
||||||
|
friendly_key="angular_precision",
|
||||||
|
),
|
||||||
|
_v(
|
||||||
|
"CANNOSCALE",
|
||||||
|
"str",
|
||||||
|
"current annotation scale name, e.g. '1:50' (a DICTIONARYVAR in AcDbVariableDictionary, not a header variable)",
|
||||||
|
default="1:1",
|
||||||
|
friendly_key="annotation_scale",
|
||||||
|
),
|
||||||
|
_v(
|
||||||
|
"CANNOSCALEVALUE",
|
||||||
|
"float",
|
||||||
|
"current annotation scale as a number (paper / drawing), derived from CANNOSCALE",
|
||||||
|
default=1.0,
|
||||||
|
read_only=True,
|
||||||
|
),
|
||||||
|
# ── limits, grid, snap, tracking ─────────────────────────────────────────
|
||||||
|
_v(
|
||||||
|
"LIMMIN",
|
||||||
|
"point",
|
||||||
|
"lower-left corner of the drawing limits (LIMITS)",
|
||||||
|
default=(0.0, 0.0),
|
||||||
|
friendly_key="limits",
|
||||||
|
),
|
||||||
|
_v(
|
||||||
|
"LIMMAX",
|
||||||
|
"point",
|
||||||
|
"upper-right corner of the drawing limits (LIMITS)",
|
||||||
|
default=(420.0, 297.0),
|
||||||
|
friendly_key="limits",
|
||||||
|
),
|
||||||
|
_v(
|
||||||
|
"GRIDMODE",
|
||||||
|
"bool",
|
||||||
|
"grid display on/off (stored on the active VPORT, not in the header)",
|
||||||
|
default=0,
|
||||||
|
friendly_key="grid",
|
||||||
|
),
|
||||||
|
_v(
|
||||||
|
"GRIDUNIT",
|
||||||
|
"point",
|
||||||
|
"grid spacing [x, y] (stored on the active VPORT)",
|
||||||
|
default=(10.0, 10.0),
|
||||||
|
friendly_key="grid_spacing",
|
||||||
|
),
|
||||||
|
_v(
|
||||||
|
"SNAPMODE",
|
||||||
|
"bool",
|
||||||
|
"snap on/off (stored on the active VPORT)",
|
||||||
|
default=0,
|
||||||
|
friendly_key="snap",
|
||||||
|
),
|
||||||
|
_v(
|
||||||
|
"SNAPUNIT",
|
||||||
|
"point",
|
||||||
|
"snap spacing [x, y] (stored on the active VPORT)",
|
||||||
|
default=(10.0, 10.0),
|
||||||
|
friendly_key="snap_spacing",
|
||||||
|
),
|
||||||
|
_v(
|
||||||
|
"ORTHOMODE",
|
||||||
|
"bool",
|
||||||
|
"ortho mode: cursor movement constrained to horizontal/vertical",
|
||||||
|
default=0,
|
||||||
|
friendly_key="ortho",
|
||||||
|
),
|
||||||
|
_v(
|
||||||
|
"POLARMODE",
|
||||||
|
"int",
|
||||||
|
"polar tracking bitmask: 1 relative angles, 2 polar in osnap tracking, 4 additional angles, 8 tooltips (polar on/off itself is AUTOSNAP bit 8)",
|
||||||
|
range_=(0, 15),
|
||||||
|
default=0,
|
||||||
|
saved_in="registry",
|
||||||
|
),
|
||||||
|
_v(
|
||||||
|
"POLARANG",
|
||||||
|
"float",
|
||||||
|
"polar angle increment in radians (90 degrees = 1.5708)",
|
||||||
|
range_=(1e-9, 2 * math.pi),
|
||||||
|
default=math.pi / 2,
|
||||||
|
saved_in="registry",
|
||||||
|
friendly_key="polar_angle",
|
||||||
|
),
|
||||||
|
_v(
|
||||||
|
"AUTOSNAP",
|
||||||
|
"int",
|
||||||
|
"AutoSnap bitmask: 1 marker, 2 magnet, 4 tooltip, 8 polar tracking on, 16 object snap tracking, 32 tracking tooltips, 64 dynamic input aperture",
|
||||||
|
range_=(0, 127),
|
||||||
|
default=63,
|
||||||
|
saved_in="registry",
|
||||||
|
friendly_key="polar",
|
||||||
|
),
|
||||||
|
_v(
|
||||||
|
"OSMODE",
|
||||||
|
"int",
|
||||||
|
"running object snap bitmask (1 end, 2 mid, 4 cen, 8 nod, 16 qua, 32 int, 64 ins, 128 per, 256 tan, 512 nea, 1024 quick, 2048 app, 4096 ext, 8192 par, 16384 off); headlessly held in memory only, DXF R2000+ does not carry it",
|
||||||
|
range_=(0, 32767),
|
||||||
|
default=4133,
|
||||||
|
friendly_key="osmode",
|
||||||
|
),
|
||||||
|
# ── current properties ───────────────────────────────────────────────────
|
||||||
|
_v(
|
||||||
|
"TEXTSIZE",
|
||||||
|
"float",
|
||||||
|
"default height for new TEXT entities (not dimension text)",
|
||||||
|
range_=_POSITIVE,
|
||||||
|
default=2.5,
|
||||||
|
friendly_key="text_size",
|
||||||
|
),
|
||||||
|
_v("TEXTSTYLE", "str", "current text style name", default="Standard", friendly_key="textstyle"),
|
||||||
|
_v(
|
||||||
|
"DIMSTYLE",
|
||||||
|
"str",
|
||||||
|
"current dimension style name (read-only: change it with dimstyle_set_current or drawing_settings {'dimstyle': ...})",
|
||||||
|
default="ISO-25",
|
||||||
|
friendly_key="dimstyle",
|
||||||
|
read_only=True,
|
||||||
|
),
|
||||||
|
_v("CLAYER", "str", "current layer name", default="0"),
|
||||||
|
_v(
|
||||||
|
"CELTYPE",
|
||||||
|
"str",
|
||||||
|
"current entity linetype (ByLayer, ByBlock or a loaded linetype)",
|
||||||
|
default="ByLayer",
|
||||||
|
),
|
||||||
|
_v(
|
||||||
|
"CECOLOR",
|
||||||
|
"str",
|
||||||
|
"current entity colour ('BYLAYER', 'BYBLOCK' or an ACI number; the headless engine stores the ACI code, 256 is ByLayer)",
|
||||||
|
default="BYLAYER",
|
||||||
|
),
|
||||||
|
_v(
|
||||||
|
"CELWEIGHT",
|
||||||
|
"int",
|
||||||
|
"current entity lineweight code (-1 ByLayer, -2 ByBlock, -3 default, else hundredths of mm)",
|
||||||
|
range_=_LINEWEIGHT,
|
||||||
|
default=-1,
|
||||||
|
),
|
||||||
|
_v(
|
||||||
|
"PDMODE",
|
||||||
|
"int",
|
||||||
|
"point display mode (0-4, or those plus 32 / 64 / 96)",
|
||||||
|
range_=(0, 100),
|
||||||
|
default=0,
|
||||||
|
friendly_key="point_mode",
|
||||||
|
),
|
||||||
|
_v(
|
||||||
|
"PDSIZE",
|
||||||
|
"float",
|
||||||
|
"point display size; 0 is 5 percent of the view, negative is a percentage of the viewport",
|
||||||
|
default=0.0,
|
||||||
|
friendly_key="point_size",
|
||||||
|
),
|
||||||
|
_v(
|
||||||
|
"FILLETRAD",
|
||||||
|
"float",
|
||||||
|
"current fillet radius",
|
||||||
|
range_=_NON_NEGATIVE,
|
||||||
|
default=0.0,
|
||||||
|
friendly_key="fillet_radius",
|
||||||
|
),
|
||||||
|
_v("MIRRTEXT", "bool", "mirror text with MIRROR (1) or keep it readable (0)", default=0),
|
||||||
|
_v("ATTREQ", "bool", "prompt for attribute values on INSERT", default=1, saved_in="registry"),
|
||||||
|
_v(
|
||||||
|
"ATTDIA",
|
||||||
|
"bool",
|
||||||
|
"use a dialog for attribute values on INSERT",
|
||||||
|
default=0,
|
||||||
|
saved_in="registry",
|
||||||
|
),
|
||||||
|
_v("PLINEWID", "float", "default width for new polylines", range_=_NON_NEGATIVE, default=0.0),
|
||||||
|
_v(
|
||||||
|
"ANGBASE",
|
||||||
|
"float",
|
||||||
|
"direction of angle 0 relative to the current UCS, in radians",
|
||||||
|
default=0.0,
|
||||||
|
),
|
||||||
|
_v("ANGDIR", "bool", "positive angle direction: 0 counter-clockwise, 1 clockwise", default=0),
|
||||||
|
# ── layouts, views, UCS ──────────────────────────────────────────────────
|
||||||
|
_v("TILEMODE", "bool", "1 model tab active, 0 a layout tab active", default=1),
|
||||||
|
_v(
|
||||||
|
"CTAB",
|
||||||
|
"str",
|
||||||
|
"name of the current tab (Model or a layout); headlessly use layout_list / layout_set_current",
|
||||||
|
default="Model",
|
||||||
|
engines=_COM_ONLY,
|
||||||
|
),
|
||||||
|
_v(
|
||||||
|
"UCSNAME",
|
||||||
|
"str",
|
||||||
|
"name of the current UCS; empty when unnamed or world",
|
||||||
|
default="",
|
||||||
|
read_only=True,
|
||||||
|
),
|
||||||
|
_v(
|
||||||
|
"VIEWCTR",
|
||||||
|
"point",
|
||||||
|
"centre of the current view (read-only; VPORT centre in a file)",
|
||||||
|
default=(0.0, 0.0),
|
||||||
|
engines=_COM_ONLY,
|
||||||
|
read_only=True,
|
||||||
|
),
|
||||||
|
_v(
|
||||||
|
"VIEWSIZE",
|
||||||
|
"float",
|
||||||
|
"height of the current view in drawing units (read-only)",
|
||||||
|
default=100.0,
|
||||||
|
engines=_COM_ONLY,
|
||||||
|
read_only=True,
|
||||||
|
),
|
||||||
|
_v("REGENMODE", "bool", "automatic regeneration on", default=1),
|
||||||
|
_v("LWDISPLAY", "bool", "display lineweights on screen", default=0),
|
||||||
|
# ── file, session ────────────────────────────────────────────────────────
|
||||||
|
_v(
|
||||||
|
"DWGNAME",
|
||||||
|
"str",
|
||||||
|
"name of the current drawing file (read-only)",
|
||||||
|
default="Drawing1.dwg",
|
||||||
|
saved_in="not_saved",
|
||||||
|
read_only=True,
|
||||||
|
),
|
||||||
|
_v(
|
||||||
|
"DWGPREFIX",
|
||||||
|
"str",
|
||||||
|
"folder of the current drawing file (read-only)",
|
||||||
|
default="",
|
||||||
|
saved_in="not_saved",
|
||||||
|
read_only=True,
|
||||||
|
),
|
||||||
|
_v(
|
||||||
|
"SAVETIME",
|
||||||
|
"int",
|
||||||
|
"automatic save interval in minutes; 0 disables",
|
||||||
|
range_=(0, 600),
|
||||||
|
default=10,
|
||||||
|
saved_in="registry",
|
||||||
|
),
|
||||||
|
_v(
|
||||||
|
"FILEDIA",
|
||||||
|
"bool",
|
||||||
|
"show file dialogs (1) or take file names from the command line (0)",
|
||||||
|
default=1,
|
||||||
|
saved_in="registry",
|
||||||
|
),
|
||||||
|
_v(
|
||||||
|
"CMDECHO",
|
||||||
|
"bool",
|
||||||
|
"echo prompts and input during AutoLISP command execution",
|
||||||
|
default=1,
|
||||||
|
saved_in="not_saved",
|
||||||
|
),
|
||||||
|
_v("HIGHLIGHT", "bool", "highlight selected objects", default=1, saved_in="not_saved"),
|
||||||
|
_v(
|
||||||
|
"XREFCTL",
|
||||||
|
"bool",
|
||||||
|
"write external reference log (.xlg) files",
|
||||||
|
default=0,
|
||||||
|
saved_in="registry",
|
||||||
|
),
|
||||||
|
_v(
|
||||||
|
"PROXYSHOW",
|
||||||
|
"enum",
|
||||||
|
"how proxy objects are displayed",
|
||||||
|
enum={0: "not shown", 1: "shown", 2: "bounding box"},
|
||||||
|
default=1,
|
||||||
|
saved_in="registry",
|
||||||
|
),
|
||||||
|
# ── hatch ────────────────────────────────────────────────────────────────
|
||||||
|
_v(
|
||||||
|
"HPNAME",
|
||||||
|
"str",
|
||||||
|
"default hatch pattern name for HATCH",
|
||||||
|
default="ANSI31",
|
||||||
|
saved_in="not_saved",
|
||||||
|
),
|
||||||
|
_v(
|
||||||
|
"HPSCALE",
|
||||||
|
"float",
|
||||||
|
"default hatch pattern scale",
|
||||||
|
range_=_POSITIVE,
|
||||||
|
default=1.0,
|
||||||
|
saved_in="not_saved",
|
||||||
|
),
|
||||||
|
_v(
|
||||||
|
"HPANG",
|
||||||
|
"float",
|
||||||
|
"default hatch pattern angle in degrees",
|
||||||
|
default=0.0,
|
||||||
|
saved_in="not_saved",
|
||||||
|
),
|
||||||
|
# ── 3D display ───────────────────────────────────────────────────────────
|
||||||
|
_v(
|
||||||
|
"ISOLINES",
|
||||||
|
"int",
|
||||||
|
"contour lines per surface on 3D solids",
|
||||||
|
range_=(0, 2047),
|
||||||
|
default=4,
|
||||||
|
engines=_COM_ONLY,
|
||||||
|
),
|
||||||
|
_v(
|
||||||
|
"FACETRES",
|
||||||
|
"float",
|
||||||
|
"smoothness of shaded and rendered curved solids",
|
||||||
|
range_=(0.01, 10.0),
|
||||||
|
default=0.5,
|
||||||
|
engines=_COM_ONLY,
|
||||||
|
),
|
||||||
|
_v("DISPSILH", "bool", "display silhouette edges of 3D solids in wireframe", default=0),
|
||||||
|
)
|
||||||
|
|
||||||
|
SYSVAR_CATALOG: dict[str, SysVar] = {entry.name: entry for entry in _ENTRIES}
|
||||||
|
|
||||||
|
|
||||||
|
def _as_dict(var: SysVar) -> dict:
|
||||||
|
out = asdict(var)
|
||||||
|
out["known"] = True
|
||||||
|
out["range"] = list(var.range) if var.range is not None else None
|
||||||
|
out["default"] = list(var.default) if isinstance(var.default, tuple) else var.default
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def describe_sysvar(name: str) -> dict:
|
||||||
|
"""The catalogue row for ``name`` (case-insensitive), or ``known: False`` plus the nearest names."""
|
||||||
|
key = str(name).strip().upper().lstrip("$")
|
||||||
|
var = SYSVAR_CATALOG.get(key)
|
||||||
|
if var is not None:
|
||||||
|
return _as_dict(var)
|
||||||
|
names = sorted(SYSVAR_CATALOG)
|
||||||
|
nearest = difflib.get_close_matches(key, names, n=5, cutoff=0.6)
|
||||||
|
for candidate in names:
|
||||||
|
if len(nearest) >= 5:
|
||||||
|
break
|
||||||
|
if key and candidate.startswith(key[:3]) and candidate not in nearest:
|
||||||
|
nearest.append(candidate)
|
||||||
|
return {"known": False, "name": key, "nearest": nearest}
|
||||||
|
|
||||||
|
|
||||||
|
def search_sysvars(text: str) -> list[dict]:
|
||||||
|
"""Catalogue rows whose name, meaning or friendly key contains every word of ``text``."""
|
||||||
|
words = [word for word in str(text).lower().split() if word]
|
||||||
|
if not words:
|
||||||
|
return []
|
||||||
|
hits = []
|
||||||
|
for var in SYSVAR_CATALOG.values():
|
||||||
|
haystack = " ".join((var.name, var.meaning, var.friendly_key or "")).lower()
|
||||||
|
if all(word in haystack for word in words):
|
||||||
|
hits.append(_as_dict(var))
|
||||||
|
return sorted(hits, key=lambda row: row["name"])
|
||||||
|
|
||||||
|
|
||||||
|
_TRUTHY = {"0", "1", "on", "off", "true", "false"}
|
||||||
|
|
||||||
|
|
||||||
|
def check_sysvar_value(name: str, value: Any) -> str | None:
|
||||||
|
"""A refusal message when ``value`` cannot be ``name``'s value; ``None`` when fine or unknown.
|
||||||
|
|
||||||
|
Soft by design: a variable the catalogue has not heard of is passed
|
||||||
|
through untouched, so ``system_set_variable`` keeps working for the
|
||||||
|
hundreds of variables nobody has authored a row for.
|
||||||
|
"""
|
||||||
|
var = SYSVAR_CATALOG.get(str(name).strip().upper().lstrip("$"))
|
||||||
|
if var is None:
|
||||||
|
return None
|
||||||
|
if var.read_only:
|
||||||
|
return f"{var.name} is read-only: {var.meaning}"
|
||||||
|
if var.type in ("int", "float"):
|
||||||
|
if isinstance(value, bool) or not isinstance(value, (int, float, str)):
|
||||||
|
return f"{var.name}: expected a number, got {value!r}"
|
||||||
|
try:
|
||||||
|
number = float(value)
|
||||||
|
except ValueError:
|
||||||
|
return f"{var.name}: expected a number, got {value!r}"
|
||||||
|
if not math.isfinite(number):
|
||||||
|
return f"{var.name}: must be a finite number"
|
||||||
|
if var.type == "int" and number != int(number):
|
||||||
|
return f"{var.name}: expected an integer, got {value!r}"
|
||||||
|
if var.range is not None:
|
||||||
|
low, high = var.range
|
||||||
|
if not low <= number <= high:
|
||||||
|
return f"{var.name}: {number:g} is out of range {low:g}..{high:g} - {var.meaning}"
|
||||||
|
return None
|
||||||
|
if var.type == "bool":
|
||||||
|
if value in (0, 1) or str(value).strip().lower() in _TRUTHY:
|
||||||
|
return None
|
||||||
|
return f"{var.name}: expected 0 or 1 (on/off), got {value!r}"
|
||||||
|
if var.type == "enum":
|
||||||
|
try:
|
||||||
|
code = int(value)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return f"{var.name}: expected one of {sorted(var.enum)}, got {value!r}"
|
||||||
|
if code not in var.enum:
|
||||||
|
choices = ", ".join(f"{k}={v}" for k, v in var.enum.items())
|
||||||
|
return f"{var.name}: {code} is not a valid code ({choices})"
|
||||||
|
return None
|
||||||
|
if var.type == "point":
|
||||||
|
try:
|
||||||
|
float(value[0]), float(value[1])
|
||||||
|
except (TypeError, IndexError, ValueError):
|
||||||
|
return f"{var.name}: expected an [x, y] point, got {value!r}"
|
||||||
|
return None
|
||||||
|
return None
|
||||||
@@ -5024,7 +5024,22 @@ async def system_set_variable(
|
|||||||
value: Annotated[Any, "New variable value"],
|
value: Annotated[Any, "New variable value"],
|
||||||
ctx: Context = None,
|
ctx: Context = None,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""Set an AutoCAD system variable (e.g. DIMSCALE, LTSCALE, MEASUREMENT)."""
|
"""Set an AutoCAD system variable (e.g. DIMSCALE, LTSCALE, MEASUREMENT).
|
||||||
|
|
||||||
|
Refusals, before anything is written: a value outside the catalogued
|
||||||
|
range or enum of a known variable, or a write to a read-only variable
|
||||||
|
(DIMSTYLE, CANNOSCALEVALUE, DWGNAME, …), is refused with the catalogue's
|
||||||
|
message — `system_variable_describe(name)` shows the range. A variable the
|
||||||
|
catalogue does not know is passed through unchanged. The headless engine
|
||||||
|
additionally refuses registry-saved variables (`capability:
|
||||||
|
registry_sysvar`) because a file has nowhere to keep them, and names that
|
||||||
|
ezdxf has no header slot for (a `ValueError` naming the variable).
|
||||||
|
"""
|
||||||
|
from engineering.standards.sysvars import check_sysvar_value
|
||||||
|
|
||||||
|
message = check_sysvar_value(name, value)
|
||||||
|
if message:
|
||||||
|
raise ToolError(f"system_set_variable refused: {message}")
|
||||||
return await _backend(ctx).system_set_variable(name, value)
|
return await _backend(ctx).system_set_variable(name, value)
|
||||||
|
|
||||||
|
|
||||||
@@ -6836,6 +6851,66 @@ async def pid_tag_parse(
|
|||||||
return parse_tag(tag, kind, equipment_prefixes)
|
return parse_tag(tag, kind, equipment_prefixes)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# ── SECTION 20: Environment (1 tool) ────────────────────────────────────────
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Group C (settings) opens this section with `system_variable_describe` and,
|
||||||
|
# in Task 16, the two `drawing_properties_*` tools; group V's merge extends the
|
||||||
|
# header count to the full environment surface (documents, layer states,
|
||||||
|
# named views, UCS, launch, preferences, interactive prompts).
|
||||||
|
|
||||||
|
|
||||||
|
@cad_tool(
|
||||||
|
summary="Explain a system variable: type, range, default, where it is saved, which engine honours it.",
|
||||||
|
cost="read",
|
||||||
|
)
|
||||||
|
@mcp.tool(
|
||||||
|
annotations={"title": "Describe System Variable", "readOnlyHint": True},
|
||||||
|
tags={"system", "settings"},
|
||||||
|
)
|
||||||
|
async def system_variable_describe(
|
||||||
|
name: Annotated[
|
||||||
|
str | None, "System variable name, e.g. LTSCALE (case-insensitive, '$' optional)"
|
||||||
|
] = None,
|
||||||
|
search: Annotated[
|
||||||
|
str | None, "Free text matched against names and meanings, e.g. 'decimal separator'"
|
||||||
|
] = None,
|
||||||
|
ctx: Context = None,
|
||||||
|
) -> dict:
|
||||||
|
"""What an AutoCAD system variable means, from an authored catalogue of 90.
|
||||||
|
|
||||||
|
With `name`: `{known, name, type, range|enum, default, meaning, saved_in
|
||||||
|
(drawing|registry|not_saved), engines: {ezdxf, com}, friendly_key,
|
||||||
|
read_only}` plus `current` when the active backend can read it (omitted,
|
||||||
|
with `current_unavailable`, when it cannot — no document open, or a
|
||||||
|
registry variable headlessly). An unknown name is never an error: `known:
|
||||||
|
false` and the nearest catalogue names come back. With `search`: every row
|
||||||
|
whose name or meaning contains all the words. With neither: the index of
|
||||||
|
names. `friendly_key` names the `drawing_settings` key that wraps the
|
||||||
|
variable, which is the preferred way to set it; `engines.ezdxf: false`
|
||||||
|
means the headless engine refuses a write (registry-saved, or no header
|
||||||
|
slot in ezdxf).
|
||||||
|
"""
|
||||||
|
from engineering.standards.sysvars import SYSVAR_CATALOG, describe_sysvar, search_sysvars
|
||||||
|
|
||||||
|
if name:
|
||||||
|
row = describe_sysvar(name)
|
||||||
|
backend = ctx.lifespan_context.get("backend") if ctx is not None else None
|
||||||
|
if row["known"] and backend is not None:
|
||||||
|
try:
|
||||||
|
row["current"] = await backend.system_get_variable(row["name"])
|
||||||
|
except Exception as exc: # no document, or the engine cannot hold it
|
||||||
|
row["current_unavailable"] = str(exc)
|
||||||
|
return row
|
||||||
|
if search:
|
||||||
|
return {"query": search, "matches": search_sysvars(search)}
|
||||||
|
return {
|
||||||
|
"count": len(SYSVAR_CATALOG),
|
||||||
|
"names": sorted(SYSVAR_CATALOG),
|
||||||
|
"hint": "pass name=<VARIABLE> for one row or search=<words> to filter",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# ── RESOURCES ───────────────────────────────────────────────────────────────
|
# ── RESOURCES ───────────────────────────────────────────────────────────────
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -0,0 +1,315 @@
|
|||||||
|
"""SYSVAR_CATALOG: authored facts about AutoCAD system variables, pinned to the engines.
|
||||||
|
|
||||||
|
Every `engines["ezdxf"]` claim is checked against the real headless backend
|
||||||
|
(read, and write of the default where the variable is writable); every
|
||||||
|
`saved_in` that is not "drawing" is checked to be exactly the set the backend
|
||||||
|
refuses with `capability: registry_sysvar`. The catalogue cannot say a thing
|
||||||
|
the backend does not do.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from backends.base import UnsupportedCapabilityError
|
||||||
|
from backends.contracts.settings import _SETTING_MAP
|
||||||
|
from backends.ezdxf_backend import _UNSAVED_SYSVARS
|
||||||
|
from engineering.standards.sysvars import (
|
||||||
|
SAVED_IN,
|
||||||
|
SYSVAR_CATALOG,
|
||||||
|
SYSVAR_TYPES,
|
||||||
|
SysVar,
|
||||||
|
check_sysvar_value,
|
||||||
|
describe_sysvar,
|
||||||
|
search_sysvars,
|
||||||
|
)
|
||||||
|
|
||||||
|
asyncio_test = pytest.mark.asyncio
|
||||||
|
|
||||||
|
DIM_WHITELIST = (
|
||||||
|
"DIMTXT",
|
||||||
|
"DIMASZ",
|
||||||
|
"DIMEXE",
|
||||||
|
"DIMEXO",
|
||||||
|
"DIMGAP",
|
||||||
|
"DIMTAD",
|
||||||
|
"DIMTIH",
|
||||||
|
"DIMTOH",
|
||||||
|
"DIMDEC",
|
||||||
|
"DIMDSEP",
|
||||||
|
"DIMLUNIT",
|
||||||
|
"DIMZIN",
|
||||||
|
"DIMBLK",
|
||||||
|
"DIMTXSTY",
|
||||||
|
"DIMLWD",
|
||||||
|
"DIMLWE",
|
||||||
|
"DIMSCALE",
|
||||||
|
"DIMTOL",
|
||||||
|
"DIMTP",
|
||||||
|
"DIMTM",
|
||||||
|
"DIMTOLJ",
|
||||||
|
"DIMTFAC",
|
||||||
|
"DIMLFAC",
|
||||||
|
"DIMRND",
|
||||||
|
"DIMATFIT",
|
||||||
|
"DIMTMOVE",
|
||||||
|
"DIMCLRD",
|
||||||
|
"DIMCLRE",
|
||||||
|
"DIMCLRT",
|
||||||
|
"DIMSAH",
|
||||||
|
"DIMBLK1",
|
||||||
|
"DIMBLK2",
|
||||||
|
"DIMCEN",
|
||||||
|
)
|
||||||
|
|
||||||
|
SPEC_NAMES = (
|
||||||
|
"LTSCALE PSLTSCALE INSUNITS LUNITS LUPREC AUNITS AUPREC LIMMIN LIMMAX GRIDMODE GRIDUNIT "
|
||||||
|
"SNAPMODE SNAPUNIT ORTHOMODE POLARMODE POLARANG OSMODE CANNOSCALE TEXTSIZE TEXTSTYLE "
|
||||||
|
"DIMSTYLE CLAYER CELTYPE CECOLOR CELWEIGHT PDMODE PDSIZE FILLETRAD MIRRTEXT ATTREQ ATTDIA "
|
||||||
|
"TILEMODE CTAB DWGNAME DWGPREFIX SAVETIME FILEDIA CMDECHO HIGHLIGHT REGENMODE UCSNAME "
|
||||||
|
"VIEWCTR VIEWSIZE ANGBASE ANGDIR PLINEWID HPNAME HPSCALE HPANG LWDISPLAY XREFCTL PROXYSHOW "
|
||||||
|
"ISOLINES FACETRES DISPSILH"
|
||||||
|
).split()
|
||||||
|
|
||||||
|
#: The two engines legitimately represent these differently (a character vs.
|
||||||
|
#: its code; a colour name vs. its ACI number), so their default cannot be
|
||||||
|
#: written verbatim headlessly. Everything else round-trips its default.
|
||||||
|
_REPRESENTATION_DIFFERS = {"DIMDSEP", "CECOLOR"}
|
||||||
|
|
||||||
|
|
||||||
|
# ── the data is complete ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
def test_catalogue_is_authored_not_sparse():
|
||||||
|
assert len(SYSVAR_CATALOG) >= 60
|
||||||
|
|
||||||
|
|
||||||
|
def test_every_dim_whitelist_variable_and_every_spec_name_is_present():
|
||||||
|
missing = [n for n in (*DIM_WHITELIST, *SPEC_NAMES) if n not in SYSVAR_CATALOG]
|
||||||
|
assert missing == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_every_entry_is_complete_and_well_typed():
|
||||||
|
for name, var in SYSVAR_CATALOG.items():
|
||||||
|
assert isinstance(var, SysVar) and var.name == name
|
||||||
|
assert var.type in SYSVAR_TYPES, name
|
||||||
|
assert var.meaning.strip(), name
|
||||||
|
assert var.saved_in in SAVED_IN, name
|
||||||
|
assert set(var.engines) == {"ezdxf", "com"}, name
|
||||||
|
assert var.friendly_key is None or var.friendly_key in _SETTING_MAP, name
|
||||||
|
if var.type == "enum":
|
||||||
|
assert var.enum and all(isinstance(k, int) for k in var.enum), name
|
||||||
|
assert var.default in var.enum, name
|
||||||
|
else:
|
||||||
|
assert var.enum is None, name
|
||||||
|
if var.range is not None:
|
||||||
|
low, high = var.range
|
||||||
|
assert low < high, name
|
||||||
|
|
||||||
|
|
||||||
|
def test_friendly_keys_cover_the_whole_facade():
|
||||||
|
covered = {var.friendly_key for var in SYSVAR_CATALOG.values() if var.friendly_key}
|
||||||
|
assert covered == set(_SETTING_MAP)
|
||||||
|
for key, (variable, _kind) in _SETTING_MAP.items():
|
||||||
|
assert SYSVAR_CATALOG[variable].friendly_key == key, (key, variable)
|
||||||
|
|
||||||
|
|
||||||
|
def test_non_drawing_variables_are_exactly_the_ones_the_headless_engine_refuses():
|
||||||
|
unsaved = {n for n, v in SYSVAR_CATALOG.items() if v.saved_in != "drawing"}
|
||||||
|
assert unsaved == set(_UNSAVED_SYSVARS)
|
||||||
|
for name in unsaved:
|
||||||
|
assert SYSVAR_CATALOG[name].engines["ezdxf"] is False, name
|
||||||
|
|
||||||
|
|
||||||
|
# ── describe / search / check ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
def test_describe_is_case_insensitive_and_complete():
|
||||||
|
row = describe_sysvar("ltscale")
|
||||||
|
assert row["known"] is True and row["name"] == "LTSCALE"
|
||||||
|
assert row["type"] == "float" and row["range"][0] > 0
|
||||||
|
assert row["saved_in"] == "drawing"
|
||||||
|
assert row["engines"] == {"ezdxf": True, "com": True}
|
||||||
|
assert row["friendly_key"] == "ltscale"
|
||||||
|
assert describe_sysvar("$LIMMAX")["default"] == [420.0, 297.0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_describe_unknown_names_the_nearest_without_raising():
|
||||||
|
row = describe_sysvar("LTSCAL")
|
||||||
|
assert row["known"] is False and row["name"] == "LTSCAL"
|
||||||
|
assert "LTSCALE" in row["nearest"]
|
||||||
|
assert describe_sysvar("")["known"] is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_search_matches_names_and_meanings():
|
||||||
|
names = [row["name"] for row in search_sysvars("scale")]
|
||||||
|
for expected in ("LTSCALE", "DIMSCALE", "CANNOSCALE", "PSLTSCALE", "HPSCALE"):
|
||||||
|
assert expected in names
|
||||||
|
assert [row["name"] for row in search_sysvars("decimal separator")] == ["DIMDSEP"]
|
||||||
|
assert search_sysvars(" ") == []
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"name, value, fragment",
|
||||||
|
[
|
||||||
|
("DIMDEC", 9, "0..8"),
|
||||||
|
("dimdec", -1, "0..8"),
|
||||||
|
("DIMDEC", 2.5, "integer"),
|
||||||
|
("DIMTXT", "big", "number"),
|
||||||
|
("DIMTXT", 0, "out of range"),
|
||||||
|
("LUNITS", 7, "architectural"),
|
||||||
|
("ORTHOMODE", 2, "0 or 1"),
|
||||||
|
("LIMMAX", 5, "[x, y]"),
|
||||||
|
("DIMSTYLE", "ANSI", "read-only"),
|
||||||
|
("CANNOSCALEVALUE", 0.5, "read-only"),
|
||||||
|
("POLARANG", 0, "out of range"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_check_refuses_with_a_message_naming_the_variable(name, value, fragment):
|
||||||
|
message = check_sysvar_value(name, value)
|
||||||
|
assert message is not None
|
||||||
|
assert name.upper() in message and fragment in message
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"name, value",
|
||||||
|
[
|
||||||
|
("DIMDEC", 3),
|
||||||
|
("DIMTXT", "3.5"),
|
||||||
|
("LUNITS", 4),
|
||||||
|
("ORTHOMODE", "on"),
|
||||||
|
("LIMMAX", [297, 210]),
|
||||||
|
("DIMDSEP", ","),
|
||||||
|
("NOSUCHVARIABLE", 12345),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_check_passes_good_values_and_unknown_names(name, value):
|
||||||
|
assert check_sysvar_value(name, value) is None
|
||||||
|
|
||||||
|
|
||||||
|
# ── the engine claims are true ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
@asyncio_test
|
||||||
|
async def test_every_ezdxf_true_entry_reads_on_a_fresh_document(backend):
|
||||||
|
unreadable = []
|
||||||
|
for name, var in SYSVAR_CATALOG.items():
|
||||||
|
if var.engines["ezdxf"] and await backend.system_get_variable(name) is None:
|
||||||
|
unreadable.append(name)
|
||||||
|
assert unreadable == []
|
||||||
|
|
||||||
|
|
||||||
|
@asyncio_test
|
||||||
|
async def test_every_writable_ezdxf_true_entry_round_trips_its_default(backend):
|
||||||
|
for name, var in SYSVAR_CATALOG.items():
|
||||||
|
if not var.engines["ezdxf"] or var.read_only or name in _REPRESENTATION_DIFFERS:
|
||||||
|
continue
|
||||||
|
await backend.system_set_variable(name, var.default)
|
||||||
|
back = await backend.system_get_variable(name)
|
||||||
|
if var.type == "point":
|
||||||
|
assert tuple(float(v) for v in back[:2]) == tuple(float(v) for v in var.default), name
|
||||||
|
elif var.type in ("bool", "int", "enum"):
|
||||||
|
assert int(back) == int(var.default), name
|
||||||
|
elif var.type == "float":
|
||||||
|
assert float(back) == pytest.approx(float(var.default)), name
|
||||||
|
else:
|
||||||
|
assert back == var.default, name
|
||||||
|
|
||||||
|
|
||||||
|
@asyncio_test
|
||||||
|
async def test_every_ezdxf_false_entry_is_refused_before_any_write(backend):
|
||||||
|
for name, var in SYSVAR_CATALOG.items():
|
||||||
|
if var.engines["ezdxf"]:
|
||||||
|
continue
|
||||||
|
if var.saved_in == "drawing":
|
||||||
|
with pytest.raises(ValueError, match=name): # no header slot in ezdxf
|
||||||
|
await backend.system_set_variable(name, var.default)
|
||||||
|
else:
|
||||||
|
with pytest.raises(UnsupportedCapabilityError) as excinfo:
|
||||||
|
await backend.system_set_variable(name, var.default)
|
||||||
|
assert excinfo.value.capability == "registry_sysvar", name
|
||||||
|
|
||||||
|
|
||||||
|
# ── server tools ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
class _FakeCtx:
|
||||||
|
def __init__(self, backend):
|
||||||
|
self.lifespan_context = {"backend": backend}
|
||||||
|
|
||||||
|
async def info(self, *a, **k):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@asyncio_test
|
||||||
|
async def test_set_variable_tool_refuses_out_of_range_before_the_backend_sees_it(backend):
|
||||||
|
from fastmcp.exceptions import ToolError
|
||||||
|
|
||||||
|
import server
|
||||||
|
|
||||||
|
ctx = _FakeCtx(backend)
|
||||||
|
with pytest.raises(ToolError, match="DIMDEC"):
|
||||||
|
await server.system_set_variable(name="DIMDEC", value=99, ctx=ctx)
|
||||||
|
assert await backend.system_get_variable("DIMDEC") == 2
|
||||||
|
with pytest.raises(ToolError, match="read-only"):
|
||||||
|
await server.system_set_variable(name="DIMSTYLE", value="ANSI", ctx=ctx)
|
||||||
|
# a name the catalogue does not know still passes straight through
|
||||||
|
out = await server.system_set_variable(name="MEASUREMENT", value=0, ctx=ctx)
|
||||||
|
assert out["ok"] is True
|
||||||
|
assert await backend.system_get_variable("MEASUREMENT") == 0
|
||||||
|
|
||||||
|
|
||||||
|
@asyncio_test
|
||||||
|
async def test_set_variable_tool_refuses_before_a_live_backend_is_called(monkeypatch):
|
||||||
|
pytest.importorskip("win32com.client", reason="pywin32 not installed")
|
||||||
|
from fastmcp.exceptions import ToolError
|
||||||
|
|
||||||
|
import server
|
||||||
|
from backends import com_backend as module
|
||||||
|
|
||||||
|
calls: list[tuple] = []
|
||||||
|
|
||||||
|
class _App:
|
||||||
|
def GetVariable(self, name):
|
||||||
|
calls.append(("GetVariable", name))
|
||||||
|
return 2
|
||||||
|
|
||||||
|
def SetVariable(self, name, value):
|
||||||
|
calls.append(("SetVariable", name, value))
|
||||||
|
|
||||||
|
monkeypatch.setattr(module, "_acad_app", lambda: _App())
|
||||||
|
backend = module.ComBackend()
|
||||||
|
|
||||||
|
async def _run_inline(func, *args, **kwargs):
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
|
||||||
|
monkeypatch.setattr(backend, "_run", _run_inline)
|
||||||
|
with pytest.raises(ToolError, match="DIMDEC"):
|
||||||
|
await server.system_set_variable(name="DIMDEC", value=40, ctx=_FakeCtx(backend))
|
||||||
|
assert calls == []
|
||||||
|
|
||||||
|
|
||||||
|
@asyncio_test
|
||||||
|
async def test_describe_tool_returns_the_row_with_the_current_value(backend):
|
||||||
|
import server
|
||||||
|
|
||||||
|
ctx = _FakeCtx(backend)
|
||||||
|
row = await server.system_variable_describe(name="dimdec", ctx=ctx)
|
||||||
|
assert row["known"] is True and row["name"] == "DIMDEC" and row["current"] == 2
|
||||||
|
unknown = await server.system_variable_describe(name="DIMDEK", ctx=ctx)
|
||||||
|
assert unknown["known"] is False and "DIMDEC" in unknown["nearest"]
|
||||||
|
found = await server.system_variable_describe(search="decimal separator", ctx=ctx)
|
||||||
|
assert [row["name"] for row in found["matches"]] == ["DIMDSEP"]
|
||||||
|
index = await server.system_variable_describe(ctx=ctx)
|
||||||
|
assert index["count"] == len(SYSVAR_CATALOG) and "LTSCALE" in index["names"]
|
||||||
|
|
||||||
|
|
||||||
|
@asyncio_test
|
||||||
|
async def test_describe_tool_works_without_a_backend():
|
||||||
|
import server
|
||||||
|
|
||||||
|
class _NoBackendCtx:
|
||||||
|
lifespan_context = {"backend": None}
|
||||||
|
|
||||||
|
row = await server.system_variable_describe(name="LTSCALE", ctx=_NoBackendCtx())
|
||||||
|
assert row["known"] is True and "current" not in row
|
||||||
@@ -517,8 +517,10 @@ async def test_tool_group_sizes_are_unchanged():
|
|||||||
when `pid_tag_parse` (the ISA-5.1 tag grammar as a read-only tool)
|
when `pid_tag_parse` (the ISA-5.1 tag grammar as a read-only tool)
|
||||||
joined; the `pid`
|
joined; the `pid`
|
||||||
tag is ranked first in `_GROUP_TAG_PRIORITY` so those tools file under
|
tag is ranked first in `_GROUP_TAG_PRIORITY` so those tools file under
|
||||||
`pid` rather than under their secondary `query` / `create` tags. Every
|
`pid` rather than under their secondary `query` / `create` tags. `system`
|
||||||
other number here has been unchanged since the snapshot was taken.
|
moved 7 -> 8 when v1.6's `system_variable_describe` (the sysvar catalogue
|
||||||
|
as a read-only tool) opened SECTION 20. Every other number here has been
|
||||||
|
unchanged since the snapshot was taken.
|
||||||
"""
|
"""
|
||||||
sizes = {label: len(names) for label, names in (await server._tool_groups()).items()}
|
sizes = {label: len(names) for label, names in (await server._tool_groups()).items()}
|
||||||
assert sizes == {
|
assert sizes == {
|
||||||
@@ -537,10 +539,10 @@ async def test_tool_group_sizes_are_unchanged():
|
|||||||
"pid": 9,
|
"pid": 9,
|
||||||
"premium": 12,
|
"premium": 12,
|
||||||
"solids": 5,
|
"solids": 5,
|
||||||
"system": 7,
|
"system": 8,
|
||||||
"templates": 2,
|
"templates": 2,
|
||||||
"transactions": 3,
|
"transactions": 3,
|
||||||
"validation": 1,
|
"validation": 1,
|
||||||
"view": 4,
|
"view": 4,
|
||||||
}
|
}
|
||||||
assert sum(sizes.values()) == 166
|
assert sum(sizes.values()) == 167
|
||||||
|
|||||||
Reference in New Issue
Block a user