engineering/understand/diff.py reads each revision once into entity records and matches them in three stages: exact signature (type, layer, geometry rounded to tol, text, block, rotation, scale, attributes; handles ignored), then handle while at least half of the exact matches kept theirs, then type + layer + nearest anchor within move_tol (default 2 % of the 1st-99th percentile diagonal). The report counts changes by layer and type, says what changed per entity (moved by, text, attribute, radius ... from -> to), compares named block definitions by content and groups the changes into clusters. drawing_diff opens group D's copy of SECTION 26. It is read-only unless markup=True, which files revision clouds around the model-space clusters on the current document through add_revision - refused before any read with new_path, without a valid rev, or on an engine without revcloud. A revision path that is neither .dxf nor .dwg is refused naming the parameter (old_path: / new_path:), like every other path refusal. Tests: the synthetic plant P&ID edited five ways (a move, a text, an attribute, an add, a delete) under renumbered handles reports exactly those five edits; renumbering alone reports nothing. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
97 lines
3.6 KiB
Python
97 lines
3.6 KiB
Python
"""Two revisions of the synthetic plant P&ID with known edits, for ``drawing_diff``.
|
|
|
|
Built from ``build_plant_pair`` (``tests/fixtures/plant_pair.py``) with
|
|
ezdxf. The older revision is the P&ID plus one tagged block reference; the
|
|
newer one moves a line by half the move tolerance, rewrites a text, changes
|
|
the block's attribute, deletes one line, adds a circle - and then gives every
|
|
model-space entity a new handle, the way a save through another program
|
|
renumbers them. What was edited is
|
|
picked from the older revision's own records (entities whose signature is
|
|
unique, so a double-drawn line can never be the one edited) and returned with
|
|
the paths: the expected answer is read off the fixture, never typed in.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections import Counter
|
|
from pathlib import Path
|
|
|
|
import ezdxf
|
|
|
|
from engineering.understand.diff import diff_snapshots, read_revision, robust_box, signature
|
|
from engineering.understand.snapshot import records_from_doc
|
|
from tests.fixtures.plant_pair import build_plant_pair
|
|
|
|
TAG_BLOCK = "DIFF_TAG"
|
|
TAG_LAYER = "DIFF-TAGS"
|
|
ADDED_LAYER = "DIFF-ADDED"
|
|
NEW_TEXT = "REV-B NOTE"
|
|
TAG_BEFORE = "X-1"
|
|
TAG_AFTER = "X-2"
|
|
|
|
|
|
def renumber_modelspace(doc) -> None:
|
|
"""Every model-space entity replaced by a copy under a fresh handle."""
|
|
msp = doc.modelspace()
|
|
for entity in list(msp):
|
|
clone = entity.copy()
|
|
msp.add_entity(clone)
|
|
msp.delete_entity(entity)
|
|
|
|
|
|
def build_revision_pair(directory, *, edits: bool = True) -> dict:
|
|
directory = Path(directory)
|
|
truth = build_plant_pair(directory / "plant")
|
|
base = ezdxf.readfile(truth["pid"])
|
|
box = robust_box(records_from_doc(base, source="base"))
|
|
centre = ((box[0] + box[2]) / 2.0, (box[1] + box[3]) / 2.0)
|
|
block = base.blocks.new(TAG_BLOCK)
|
|
block.add_circle((0.0, 0.0), 5.0)
|
|
block.add_attdef("TAG", (0.0, -8.0), dxfattribs={"height": 2.5})
|
|
base.layers.add(TAG_LAYER)
|
|
tag = base.modelspace().add_blockref(TAG_BLOCK, centre, dxfattribs={"layer": TAG_LAYER})
|
|
tag.add_auto_attribs({"TAG": TAG_BEFORE})
|
|
old_path = directory / "rev_a.dxf"
|
|
base.saveas(old_path)
|
|
|
|
old_snap, _blocks = read_revision(str(old_path))
|
|
move_tol = diff_snapshots(old_snap, old_snap)["move_tol"]
|
|
counts = Counter(signature(r) for r in old_snap.records)
|
|
unique = [r for r in old_snap.records if r.space == "Model" and counts[signature(r)] == 1]
|
|
moved = next(r for r in unique if r.type == "LINE")
|
|
text = next(r for r in unique if r.type in ("TEXT", "MTEXT"))
|
|
insert = next(r for r in old_snap.records if r.block == TAG_BLOCK)
|
|
deleted = next(
|
|
r
|
|
for r in unique
|
|
if r.type in ("LINE", "LWPOLYLINE") and (r.type, r.layer) != (moved.type, moved.layer)
|
|
)
|
|
|
|
new = ezdxf.readfile(old_path)
|
|
msp = new.modelspace()
|
|
if edits:
|
|
new.entitydb[moved.handle].translate(move_tol / 2.0, 0.0, 0.0)
|
|
target = new.entitydb[text.handle]
|
|
if target.dxftype() == "MTEXT":
|
|
target.text = NEW_TEXT
|
|
else:
|
|
target.dxf.text = NEW_TEXT
|
|
new.entitydb[insert.handle].get_attrib("TAG").dxf.text = TAG_AFTER
|
|
msp.delete_entity(new.entitydb[deleted.handle])
|
|
new.layers.add(ADDED_LAYER)
|
|
msp.add_circle(centre, move_tol / 10.0, dxfattribs={"layer": ADDED_LAYER})
|
|
renumber_modelspace(new)
|
|
new_path = directory / "rev_b.dxf"
|
|
new.saveas(new_path)
|
|
return {
|
|
"old": str(old_path),
|
|
"new": str(new_path),
|
|
"move_tol": move_tol,
|
|
"old_count": len(old_snap.records),
|
|
"moved": moved.handle,
|
|
"text": text.handle,
|
|
"text_before": text.text,
|
|
"insert": insert.handle,
|
|
"deleted": deleted.handle,
|
|
}
|