Resolving Nested Block References with ezdxf
An INSERT entity places a named block definition with a scale, a rotation and an insertion point, and blocks routinely contain further INSERTs. Flattening them means recursing, composing the placement transforms as you descend, guarding against circular definitions, and keeping the chain of block names so a resolved entity can be traced back. A single-level expansion leaves the nested geometry sitting at the wrong place, at the wrong size, silently. This page is part of DXF Entity Structure Breakdown.
How Placement Composes
Each INSERT carries four things that place its block: an insertion point, X, Y and Z scale factors, a rotation about the Z axis, and an extrusion vector defining the plane it all happens in. Together they form a transform from the block’s own coordinate space into the space of whatever contains the INSERT.
For an INSERT in modelspace, that containing space is the world. For an INSERT inside a block definition, it is the parent block’s space — so the geometry’s world position is the product of the transforms of every INSERT on the path from modelspace down to it. The order matters: composing parent-then-child gives a different result from child-then-parent, and both produce geometry that looks plausible.
The library’s virtual-entity expansion does one level of this correctly. Ask a modelspace INSERT for its virtual entities and you get its block’s contents transformed into world space — including any nested INSERT, itself correctly placed, still unexpanded. Recursion is the caller’s job, and so is the cycle guard: a block that references itself, directly or through a chain, is invalid and does occur in files that have been through several rounds of editing.
Production-Ready Script
# ezdxf>=1.1.0, Python 3.9+
from __future__ import annotations
from dataclasses import dataclass
from typing import Iterator
import ezdxf
from ezdxf.entities import Insert
class CircularBlockError(ValueError):
pass
@dataclass(frozen=True)
class PlacedEntity:
entity: object # the transformed DXF entity
block_path: tuple[str, ...] # outermost block first; empty for modelspace
depth: int
def flatten(layout, *, max_depth: int = 64) -> Iterator[PlacedEntity]:
"""Yield every entity in world coordinates, expanding nested INSERTs."""
yield from _walk(layout, path=(), depth=0, max_depth=max_depth)
def _walk(container, *, path: tuple[str, ...], depth: int, max_depth: int
) -> Iterator[PlacedEntity]:
if depth > max_depth:
raise CircularBlockError(
f"block nesting exceeded {max_depth} via {' > '.join(path)}"
)
for entity in container:
if isinstance(entity, Insert):
name = entity.dxf.name
if name in path:
raise CircularBlockError(
f"block {name!r} references itself via {' > '.join(path)}"
)
# virtual_entities() applies THIS insert's transform to the block
# contents; nested inserts come back placed but unexpanded.
yield from _walk(entity.virtual_entities(),
path=path + (name,), depth=depth + 1, max_depth=max_depth)
else:
yield PlacedEntity(entity=entity, block_path=path, depth=depth)
def flatten_document(dxf_path: str) -> list[PlacedEntity]:
doc = ezdxf.readfile(dxf_path)
return list(flatten(doc.modelspace()))
def summarise(placed: list[PlacedEntity]) -> dict:
from collections import Counter
by_depth = Counter(p.depth for p in placed)
by_type = Counter(p.entity.dxftype() for p in placed)
return {
"entities": len(placed),
"max_depth": max(by_depth) if by_depth else 0,
"at_each_depth": dict(sorted(by_depth.items())),
"top_types": dict(by_type.most_common(5)),
}
if __name__ == "__main__":
placed = flatten_document("federated.dxf")
print(summarise(placed))
deep = [p for p in placed if p.depth >= 2]
if deep:
print("example nested:", deep[0].entity.dxftype(), "via", " > ".join(deep[0].block_path))
Key implementation notes:
- The cycle guard tests membership in the current path, not in a global visited set. A block legitimately used twice in different branches is fine; a block that contains itself is not, and only the path distinguishes them.
- The depth limit is a second backstop with a different message, so an unusual-but-legal deep nesting is distinguishable from a cycle.
virtual_entities()does the transform composition, which is why none appears explicitly here — reimplementing the matrix arithmetic is a common and unnecessary source of placement bugs.block_pathtravels with every entity. When a feature turns out to be in the wrong place, the path names the block to look at.- The summary reports entities per depth. A drawing where everything sits at depth 0 has no nesting, and one where most entities are at depth 3 explains why a single-level expansion looked empty.
Compatibility Matrix
| Component | Supported range | Notes |
|---|---|---|
ezdxf |
>=1.1.0 |
virtual_entities on INSERT |
| DXF revision | R12 – R2018 | block table present throughout |
| Nesting depth | bounded by max_depth |
64 is generous for real drawings |
| Non-uniform scale | supported | mirrored inserts have a negative scale |
| External references | expanded when bound | unbound references have empty definitions |
Fallback Strategies
1. Nothing comes back for a block. Its definition is empty, which is what an unresolved external reference looks like. Check whether the reference was bound before treating it as a data problem.
2. Geometry is mirrored. A negative scale factor on an INSERT is a legitimate mirror. It propagates correctly through the transform; what it also does is reverse ring winding, so downstream polygon orientation needs normalising.
3. Runaway recursion. The cycle guard names the block. Repair the drawing rather than raising the depth limit.
4. Attributes are missing after flattening. Flattening yields geometry; attribute values live on the INSERT itself, which the walk descends past. Collect attributes at the INSERT before recursing, as the block attribute extraction guide describes.
5. Memory grows on a large drawing. flatten is a generator; the list materialisation in flatten_document is what allocates. Consume the generator directly for a large file.
FAQ
Does virtual_entities handle nesting?
It expands one level. A block containing another block reference yields that nested INSERT as one of its virtual entities, already transformed, but not the geometry inside it. For a fully flattened result you have to recurse on any INSERT that comes back, composing the transform as you go.
What order do the placement components apply in?
Scale, then rotation, then translation — and the scales are applied about the block base point rather than about the world origin. Composing them in a different order produces geometry that is the right shape in the wrong place, which looks like a coordinate system problem rather than a matrix one.
How do I detect a circular block definition?
Track the block names on the current recursion path in a set and refuse to enter one already on it. A depth limit alone is a blunter instrument: it stops the runaway but reports a depth error rather than naming the cycle, and legitimate deep nesting then looks like corruption.
Related Pages
- DXF Entity Structure Breakdown — parent reference on sections, group codes and the block table
- Extracting Block Attributes from CAD Files with ezdxf — the attribute side of the same INSERT traversal
- Extracting LWPOLYLINE Vertices with ezdxf — what to do with the geometry once it is placed