Files
Umut CANandClaude Opus 5.5 2b74bf20a6 feat(understand): drawing_topology_check and the three topo_* critique focuses
engineering/understand/topology.py reads a line network (LINE, polylines
with true bulge arcs, ARC) on the chosen layers and reports dangling
ends, near misses (end-end and end-segment within gap, one row per pair)
and interior crossings of two straight segments of one layer. An end on
another network line is joined; an end on any curve of another layer or
inside a block reference's box is attached; a line drawn twice is not a
join, so a doubled pipe still dangles. gap defaults to 0.2 % of the
network diagonal, never under ten tol.

drawing_topology_check joins drawing_diff in SECTION 26: default layers
are those classify_layer calls piping or electrical at >= 0.9, reported
with their classification; no such layer is a refusal, not a pass.

topo_dangling_endpoint (warning), topo_near_miss (error) and
topo_interior_crossing (info) join the closed enum and run under
focus=None, excluding the P&ID and architectural layer sets, so a
pid_from_spec sheet and an arch_plan_from_spec plan get none of them;
a drawing with no candidate layer is never snapshotted.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
2026-09-24 12:20:24 +03:00

140 lines
3.7 KiB
Python

"""Premium drawing planning + critique data models.
PlanSpec is committed up-front by the LLM via `drawing_plan(...)` before any
geometry is created; CritiqueFocus is a closed enum to keep
`drawing_critique(...)` scope from drifting.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Literal
SheetSize = Literal["A4", "A3", "A2", "A1", "A0"]
LayerSetId = Literal["iso13567", "mech", "pid", "arch"]
DimStyle = Literal["chain", "baseline", "ordinate", "mixed"]
CritiqueFocus = Literal[
"iso128",
"layer_color",
"dim_overlap",
"untrimmed_corner",
"duplicate_entities",
"construction_left",
"gdt",
"pid_dangling_line",
"pid_duplicate_tag",
"pid_incompatible_connection",
"pid_untagged_instrument",
"pid_illegal_tag",
"pid_unconnected_equipment",
"mech_missing_centreline",
"mech_unhatched_section",
"mech_view_misaligned",
"mech_duplicate_dimension",
"mech_thread_unrepresented",
"mech_bom_balloon_mismatch",
"arch_room_unlabelled",
"arch_wall_gap",
"arch_opening_clash",
"topo_dangling_endpoint",
"topo_near_miss",
"topo_interior_crossing",
]
SnapType = Literal["end", "mid", "center", "quad", "int", "perp", "near"]
ALL_CRITIQUE_FOCUSES: tuple[CritiqueFocus, ...] = (
"iso128",
"layer_color",
"dim_overlap",
"untrimmed_corner",
"duplicate_entities",
"construction_left",
"gdt",
"pid_dangling_line",
"pid_duplicate_tag",
"pid_incompatible_connection",
"pid_untagged_instrument",
"pid_illegal_tag",
"pid_unconnected_equipment",
"mech_missing_centreline",
"mech_unhatched_section",
"mech_view_misaligned",
"mech_duplicate_dimension",
"mech_thread_unrepresented",
"mech_bom_balloon_mismatch",
"arch_room_unlabelled",
"arch_wall_gap",
"arch_opening_clash",
"topo_dangling_endpoint",
"topo_near_miss",
"topo_interior_crossing",
)
@dataclass
class PlanSpec:
"""Drawing intent committed before geometry creation.
Returned by `drawing_plan(...)` and replayed by `drawing_critique(...)` to
verify the finished drawing matches the original intent.
"""
intent: str
sheet_size: SheetSize
scale: float
layer_set_id: LayerSetId
view_count: int = 1
dim_style: DimStyle = "chain"
notes: list[str] = field(default_factory=list)
requirements: dict = field(default_factory=dict)
spec_hash: str | None = None
preflight_status: Literal["ready", "skipped"] = "skipped"
def to_dict(self) -> dict:
return {
"intent": self.intent,
"sheet_size": self.sheet_size,
"scale": self.scale,
"layer_set_id": self.layer_set_id,
"view_count": self.view_count,
"dim_style": self.dim_style,
"notes": list(self.notes),
"requirements": dict(self.requirements),
"spec_hash": self.spec_hash,
"preflight_status": self.preflight_status,
}
@dataclass
class Issue:
"""One problem reported by `drawing_critique(...)`."""
severity: Literal["error", "warning", "info"]
focus: CritiqueFocus
message: str
handles: list[str] = field(default_factory=list)
detail: dict = field(default_factory=dict)
def to_dict(self) -> dict:
return {
"severity": self.severity,
"focus": self.focus,
"message": self.message,
"handles": list(self.handles),
"detail": self.detail,
}
# ISO 128 lineweight set (mm). Any lineweight outside this set is flagged.
ISO_128_LINEWEIGHTS_MM: tuple[float, ...] = (
0.13,
0.18,
0.25,
0.35,
0.50,
0.70,
1.00,
1.40,
2.00,
)