feat(blocks): block_define — typed primitives + ATTDEFs on both engines
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
94840042a5
commit
a84cae2641
@@ -3136,6 +3136,128 @@ class ComBackend(AutoCADBackend):
|
||||
|
||||
return await self._run(_sync)
|
||||
|
||||
async def block_define(
|
||||
self,
|
||||
name,
|
||||
entities,
|
||||
attdefs=None,
|
||||
base_x=0.0,
|
||||
base_y=0.0,
|
||||
overwrite=False,
|
||||
) -> dict:
|
||||
"""A block definition with ATTDEFs from typed specs, through ActiveX.
|
||||
|
||||
``Blocks.Add`` creates the definition; each primitive is one ``Add*``
|
||||
call on the Block object, each ATTDEF one ``AddAttribute``. Overwrite
|
||||
deletes the existing definition's members and re-adds — the name and
|
||||
its INSERTs survive. Unit-tested against a fake ActiveX surface;
|
||||
executed live by ``scripts/smoke_pid_com.py``.
|
||||
"""
|
||||
from backends.block_specs import (
|
||||
solid_vertices,
|
||||
validate_attdef_specs,
|
||||
validate_entity_specs,
|
||||
)
|
||||
|
||||
# The anonymous-name rule comes first: ``*`` is also a forbidden DXF
|
||||
# symbol character, so ``sanitize_symbol_name`` would refuse ``*U9``
|
||||
# with the generic message before this one could name the real reason.
|
||||
if (name or "").strip().startswith("*"):
|
||||
raise ValueError("block_define: anonymous block names (*...) are refused")
|
||||
clean_name = sanitize_symbol_name(name, kind="block")
|
||||
ents = validate_entity_specs(entities)
|
||||
atts = validate_attdef_specs(attdefs or [])
|
||||
# acAlignmentLeft / Center / Right / MiddleCenter
|
||||
align_map = {"left": 0, "center": 1, "right": 2, "middle_center": 10}
|
||||
|
||||
def _style(obj, layer: str) -> None:
|
||||
obj.Layer = layer
|
||||
obj.Color = 0 # acByBlock
|
||||
obj.Linetype = "ByBlock"
|
||||
|
||||
def _sync():
|
||||
doc = _acad_doc()
|
||||
try:
|
||||
existing = doc.Blocks.Item(clean_name)
|
||||
except Exception:
|
||||
existing = None
|
||||
if existing is not None and not overwrite:
|
||||
raise ValueError(
|
||||
f"block_define: block {clean_name!r} already exists; "
|
||||
"pass overwrite=true to replace its contents"
|
||||
)
|
||||
replaced = existing is not None
|
||||
base = _apoint(float(base_x), float(base_y), 0.0)
|
||||
if existing is None:
|
||||
block = doc.Blocks.Add(base, clean_name)
|
||||
else:
|
||||
block = existing
|
||||
members = [block.Item(i) for i in range(block.Count)]
|
||||
for member in members:
|
||||
member.Delete()
|
||||
block.Origin = base
|
||||
for spec in ents:
|
||||
kind = spec["type"]
|
||||
if kind == "line":
|
||||
obj = block.AddLine(
|
||||
_apoint(spec["x1"], spec["y1"]), _apoint(spec["x2"], spec["y2"])
|
||||
)
|
||||
elif kind == "circle":
|
||||
obj = block.AddCircle(_apoint(spec["cx"], spec["cy"]), float(spec["r"]))
|
||||
elif kind == "arc":
|
||||
obj = block.AddArc(
|
||||
_apoint(spec["cx"], spec["cy"]),
|
||||
float(spec["r"]),
|
||||
deg2rad(spec["start_deg"]),
|
||||
deg2rad(spec["end_deg"]),
|
||||
)
|
||||
elif kind == "polyline":
|
||||
flat = [coord for pt in spec["points"] for coord in pt]
|
||||
obj = block.AddLightWeightPolyline(_av(flat))
|
||||
obj.Closed = bool(spec["closed"])
|
||||
for index, bulge in enumerate(spec["bulges"] or []):
|
||||
if bulge:
|
||||
obj.SetBulge(index, float(bulge))
|
||||
elif kind == "text":
|
||||
obj = block.AddText(
|
||||
spec["text"], _apoint(spec["x"], spec["y"]), float(spec["height"])
|
||||
)
|
||||
obj.Rotation = deg2rad(spec["rotation_deg"])
|
||||
if spec["align"] != "left":
|
||||
obj.Alignment = align_map[spec["align"]]
|
||||
obj.TextAlignmentPoint = _apoint(spec["x"], spec["y"])
|
||||
elif kind == "solid":
|
||||
verts = solid_vertices(spec["points"])
|
||||
verts = verts + [verts[-1]] * (4 - len(verts))
|
||||
obj = block.AddSolid(*[_apoint(x, y) for x, y in verts])
|
||||
_style(obj, spec["layer"])
|
||||
for att in atts:
|
||||
mode = 1 if att["invisible"] else 0 # acAttributeModeInvisible / Normal
|
||||
obj = block.AddAttribute(
|
||||
float(att["height"]),
|
||||
mode,
|
||||
att["prompt"],
|
||||
_apoint(att["x"], att["y"]),
|
||||
att["tag"],
|
||||
att["default"],
|
||||
)
|
||||
obj.Rotation = deg2rad(att["rotation_deg"])
|
||||
if att["align"] != "left":
|
||||
obj.Alignment = align_map[att["align"]]
|
||||
obj.TextAlignmentPoint = _apoint(att["x"], att["y"])
|
||||
_style(obj, "0")
|
||||
_regen()
|
||||
return {
|
||||
"ok": True,
|
||||
"name": clean_name,
|
||||
"entity_count": len(ents),
|
||||
"attdef_count": len(atts),
|
||||
"replaced": replaced,
|
||||
"backend": "com",
|
||||
}
|
||||
|
||||
return await self._run(_sync)
|
||||
|
||||
# ── analysis / query ──────────────────────────────────────────────────────
|
||||
|
||||
async def analysis_stats(self) -> dict:
|
||||
|
||||
@@ -48,3 +48,19 @@ class BlockContract(ABC):
|
||||
base_x: float = 0.0,
|
||||
base_y: float = 0.0,
|
||||
) -> dict: ...
|
||||
|
||||
@abstractmethod
|
||||
async def block_define(
|
||||
self,
|
||||
name: str,
|
||||
entities: list[dict],
|
||||
attdefs: list[dict] | None = None,
|
||||
base_x: float = 0.0,
|
||||
base_y: float = 0.0,
|
||||
overwrite: bool = False,
|
||||
) -> dict:
|
||||
"""Create (or with ``overwrite`` replace the contents of) a block
|
||||
definition from typed primitive specs and ATTDEF specs — see
|
||||
``backends/block_specs.py`` for the spec vocabulary. Returns
|
||||
``{ok, name, entity_count, attdef_count, replaced, backend}``."""
|
||||
...
|
||||
|
||||
@@ -4560,6 +4560,128 @@ class EzdxfBackend(AutoCADBackend):
|
||||
|
||||
return await self._async(_sync)
|
||||
|
||||
async def block_define(
|
||||
self,
|
||||
name,
|
||||
entities,
|
||||
attdefs=None,
|
||||
base_x=0.0,
|
||||
base_y=0.0,
|
||||
overwrite=False,
|
||||
) -> dict:
|
||||
"""A block definition with ATTDEFs from typed specs.
|
||||
|
||||
Validation runs before any write, so a malformed entry leaves no
|
||||
definition behind. ``overwrite`` replaces the *contents* of an existing
|
||||
definition rather than deleting it: existing INSERTs keep pointing at
|
||||
the name and show the new geometry (deleting a referenced block is
|
||||
refused by both engines; replacing contents works on both).
|
||||
"""
|
||||
from ezdxf.enums import TextEntityAlignment
|
||||
|
||||
from backends.block_specs import (
|
||||
solid_vertices,
|
||||
validate_attdef_specs,
|
||||
validate_entity_specs,
|
||||
)
|
||||
from security import sanitize_symbol_name
|
||||
|
||||
# The anonymous-name rule comes first: ``*`` is also a forbidden DXF
|
||||
# symbol character, so ``sanitize_symbol_name`` would refuse ``*U9``
|
||||
# with the generic message before this one could name the real reason.
|
||||
if (name or "").strip().startswith("*"):
|
||||
raise ValueError("block_define: anonymous block names (*...) are refused")
|
||||
clean_name = sanitize_symbol_name(name, kind="block")
|
||||
ents = validate_entity_specs(entities)
|
||||
atts = validate_attdef_specs(attdefs or [])
|
||||
align_map = {
|
||||
"left": TextEntityAlignment.LEFT,
|
||||
"center": TextEntityAlignment.CENTER,
|
||||
"right": TextEntityAlignment.RIGHT,
|
||||
"middle_center": TextEntityAlignment.MIDDLE_CENTER,
|
||||
}
|
||||
|
||||
def _sync():
|
||||
doc = self._require_doc()
|
||||
existing = doc.blocks.get(clean_name) if clean_name in doc.blocks else None
|
||||
if existing is not None and not overwrite:
|
||||
raise ValueError(
|
||||
f"block_define: block {clean_name!r} already exists; "
|
||||
"pass overwrite=true to replace its contents"
|
||||
)
|
||||
replaced = existing is not None
|
||||
base = (float(base_x), float(base_y), 0.0)
|
||||
if existing is None:
|
||||
blk = doc.blocks.new(name=clean_name, base_point=base)
|
||||
else:
|
||||
blk = existing
|
||||
blk.delete_all_entities()
|
||||
blk.block.dxf.base_point = base
|
||||
for spec in ents:
|
||||
attribs = {"layer": spec["layer"], "color": 0, "linetype": "ByBlock"}
|
||||
kind = spec["type"]
|
||||
if kind == "line":
|
||||
blk.add_line(
|
||||
(spec["x1"], spec["y1"]), (spec["x2"], spec["y2"]), dxfattribs=attribs
|
||||
)
|
||||
elif kind == "circle":
|
||||
blk.add_circle((spec["cx"], spec["cy"]), spec["r"], dxfattribs=attribs)
|
||||
elif kind == "arc":
|
||||
blk.add_arc(
|
||||
(spec["cx"], spec["cy"]),
|
||||
spec["r"],
|
||||
spec["start_deg"],
|
||||
spec["end_deg"],
|
||||
dxfattribs=attribs,
|
||||
)
|
||||
elif kind == "polyline":
|
||||
bulges = spec["bulges"] or [0.0] * len(spec["points"])
|
||||
blk.add_lwpolyline(
|
||||
[(x, y, b) for (x, y), b in zip(spec["points"], bulges, strict=True)],
|
||||
format="xyb",
|
||||
close=spec["closed"],
|
||||
dxfattribs=attribs,
|
||||
)
|
||||
elif kind == "text":
|
||||
txt = blk.add_text(
|
||||
spec["text"],
|
||||
dxfattribs={
|
||||
**attribs,
|
||||
"height": spec["height"],
|
||||
"rotation": spec["rotation_deg"],
|
||||
},
|
||||
)
|
||||
txt.set_placement((spec["x"], spec["y"]), align=align_map[spec["align"]])
|
||||
elif kind == "solid":
|
||||
blk.add_solid(solid_vertices(spec["points"]), dxfattribs=attribs)
|
||||
for att in atts:
|
||||
attdef = blk.add_attdef(
|
||||
att["tag"],
|
||||
insert=(att["x"], att["y"]),
|
||||
text=att["default"],
|
||||
dxfattribs={
|
||||
"layer": "0",
|
||||
"color": 0,
|
||||
"height": att["height"],
|
||||
"rotation": att["rotation_deg"],
|
||||
"prompt": att["prompt"],
|
||||
},
|
||||
)
|
||||
attdef.set_placement((att["x"], att["y"]), align=align_map[att["align"]])
|
||||
if att["invisible"]:
|
||||
attdef.is_invisible = True
|
||||
self._mark_dirty()
|
||||
return {
|
||||
"ok": True,
|
||||
"name": clean_name,
|
||||
"entity_count": len(ents),
|
||||
"attdef_count": len(atts),
|
||||
"replaced": replaced,
|
||||
"backend": "ezdxf",
|
||||
}
|
||||
|
||||
return await self._async(_sync)
|
||||
|
||||
# ── analysis / query ──────────────────────────────────────────────────────
|
||||
|
||||
async def analysis_stats(self) -> dict:
|
||||
|
||||
Reference in New Issue
Block a user