Files
AI_portal/dwg-counting/blocks.py
2026-07-07 13:16:26 +02:00

125 lines
4.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Exact symbol counting from CAD vector data.
CAD drawings place repeated symbols as *block references* (DXF INSERT
entities). The file therefore contains the exact count and exact location of
every symbol — no raster matching, no threshold, no false positives. This
module reads that structure.
count_blocks() → list of named blocks with exact counts + every instance's
model coordinates. render_block_thumbnail() draws one block's geometry so the
user can visually identify which block is the symbol they want.
"""
from __future__ import annotations
import logging
import re
from pathlib import Path
import cairosvg
import ezdxf
from ezdxf.addons.drawing import Frontend, RenderContext, layout
from ezdxf.addons.drawing.config import (
BackgroundPolicy, ColorPolicy, Configuration,
)
from ezdxf.addons.drawing.svg import SVGBackend
logger = logging.getLogger(__name__)
# CAD-bookkeeping / paste-artifact block names users never want to count.
_NOISE_PREFIX = re.compile(r"^(\*|A\$C|Clipboard|DWG\d|_|\$|ACAD_)", re.I)
def _is_noise(name: str) -> bool:
if not name or _NOISE_PREFIX.match(name):
return True
if name.isdigit(): # purely numeric auto-names
return True
if len(name.strip()) <= 1:
return True
return False
def count_blocks(dxf_path: Path) -> dict:
"""Parse modelspace block references.
Returns:
{
"blocks": [
{"name", "count", "noise": bool,
"instances": [{"x","y","rot"}, ...]},
... sorted: meaningful first, then by count desc
],
"doc": <ezdxf doc> # reused for thumbnail rendering
}
"""
doc = ezdxf.readfile(str(dxf_path))
msp = doc.modelspace()
blocks: dict[str, dict] = {}
for ins in msp.query("INSERT"):
name = ins.dxf.name
rec = blocks.setdefault(name, {"name": name, "instances": []})
# MINSERT (array insert) places row_count × column_count copies.
rows = int(getattr(ins.dxf, "row_count", 1) or 1)
cols = int(getattr(ins.dxf, "column_count", 1) or 1)
rsp = float(getattr(ins.dxf, "row_spacing", 0) or 0)
csp = float(getattr(ins.dxf, "column_spacing", 0) or 0)
ip = ins.dxf.insert
rot = float(getattr(ins.dxf, "rotation", 0) or 0)
for r in range(max(rows, 1)):
for c in range(max(cols, 1)):
rec["instances"].append({
"x": float(ip.x) + c * csp,
"y": float(ip.y) + r * rsp,
"rot": rot,
})
out = []
for name, rec in blocks.items():
rec["count"] = len(rec["instances"])
rec["noise"] = _is_noise(name)
out.append(rec)
# Meaningful named blocks first, each sorted by count desc.
out.sort(key=lambda b: (b["noise"], -b["count"]))
total = sum(b["count"] for b in out)
logger.info("Block scan: %d instances, %d distinct blocks (%d meaningful)",
total, len(out), sum(1 for b in out if not b["noise"]))
return {"blocks": out, "doc": doc}
def render_block_thumbnail(src_doc, name: str, out_path: Path,
px: int = 200) -> Path:
"""Render one block definition's geometry to a square PNG thumbnail."""
src_block = src_doc.blocks.get(name)
if src_block is None:
raise ValueError(f"Block not found: {name}")
nd = ezdxf.new()
nb = nd.blocks.new(name=name) if name not in nd.blocks \
else nd.blocks.get(name)
for e in src_block:
try:
nb.add_foreign_entity(e)
except Exception:
# Some entity types can't be copied across docs — skip; the
# remaining geometry is still enough to identify the symbol.
pass
msp = nd.modelspace()
msp.add_blockref(name, (0, 0))
cfg = Configuration(
background_policy=BackgroundPolicy.WHITE,
color_policy=ColorPolicy.BLACK,
)
backend = SVGBackend()
page = layout.Page(120, 120, layout.Units.mm, layout.Margins.all(5))
Frontend(RenderContext(nd), backend, config=cfg).draw_layout(
msp, finalize=True)
svg = backend.get_string(page)
png = cairosvg.svg2png(bytestring=svg.encode("utf-8"),
output_width=px, output_height=px)
Path(out_path).write_bytes(png)
return out_path