Files
autocad-mcp-pro/backends/xdata_specs.py
T

103 lines
3.9 KiB
Python

"""XDATA value typing and limits, shared by both engines.
DXF XDATA is a list of (group code, value) pairs under a registered APPID.
This module is the one place that decides how a JSON value becomes a group
code and enforces the two limits AutoCAD enforces silently: 255 characters
per string and 16 KB per entity.
"""
from __future__ import annotations
import re
APP_NAME_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]{0,30}$")
RESERVED_APPS = {"ACAD"}
MAX_STRING = 255
MAX_BYTES = 16 * 1024
INT_MIN, INT_MAX = -(2**31), 2**31 - 1
def validate_app_name(app) -> str:
if not isinstance(app, str) or not APP_NAME_RE.match(app):
raise ValueError(f"xdata: app name must match {APP_NAME_RE.pattern}, got {app!r}")
if app.upper() in RESERVED_APPS:
raise ValueError(f"xdata: app name {app!r} is reserved")
return app
def encode_values(values) -> list[tuple[int, object]]:
"""JSON values → (group code, value). Raises TypeError/ValueError; writes nothing."""
if not isinstance(values, (list, tuple)):
raise TypeError("xdata: values must be a list")
tags: list[tuple[int, object]] = []
size = 0
for i, value in enumerate(values):
where = f"values[{i}]"
if isinstance(value, bool):
raise TypeError(f"xdata: {where} booleans have no XDATA group code")
if isinstance(value, str):
if len(value) > MAX_STRING:
raise ValueError(
f"xdata: {where} is {len(value)} characters; the limit is {MAX_STRING}"
)
tags.append((1000, value))
size += len(value.encode("utf-8")) + 2
elif isinstance(value, int):
if not INT_MIN <= value <= INT_MAX:
raise ValueError(f"xdata: {where} is outside the 32-bit integer range")
tags.append((1071, int(value)))
size += 6
elif isinstance(value, float):
tags.append((1040, float(value)))
size += 10
elif isinstance(value, (list, tuple)) and len(value) in (2, 3):
if any(isinstance(c, bool) for c in value):
raise TypeError(f"xdata: {where} point coordinates must be numbers")
try:
coords = tuple(float(c) for c in value)
except (TypeError, ValueError) as exc:
raise TypeError(f"xdata: {where} point coordinates must be numbers") from exc
if len(coords) == 2:
coords = (coords[0], coords[1], 0.0)
tags.append((1010, coords))
size += 26
else:
raise TypeError(f"xdata: {where} must be a string, int, float or [x, y(, z)] point")
if size > MAX_BYTES:
raise ValueError(f"xdata: encoded size {size} bytes exceeds the 16 KB per-entity limit")
return tags
def decode_values(tags) -> list:
"""(group code, value) → JSON values. Skips the 1001 app marker and 1002 braces."""
out: list = []
for code, value in tags:
if code in (1001, 1002):
continue
if code == 1010 or code in (1011, 1012, 1013):
out.append([float(c) for c in value])
elif code in (1040, 1041, 1042):
out.append(float(value))
elif code in (1070, 1071):
out.append(int(value))
else:
out.append(str(value))
return out
def split_by_app(codes, values) -> dict[str, list]:
"""A flat ActiveX GetXData stream (1001 markers inline) → {app: values}."""
out: dict[str, list] = {}
current: str | None = None
pending: list[tuple[int, object]] = []
for code, value in zip(codes, values, strict=True):
if code == 1001:
if current is not None:
out[current] = decode_values(pending)
current, pending = str(value), []
else:
pending.append((code, value))
if current is not None:
out[current] = decode_values(pending)
return out