Files
autocad-mcp-pro/engineering/understand/route.py
T
Umut CANandClaude Opus 5.5 e640def4ff feat(understand): orthogonal lengths and pipe takeoff rows
route.py: Manhattan distance, the rectilinear minimum spanning tree (Prim
over |dx| + |dy|, exact on the points given) and the x-then-y route.

pipe_rows turns the P&ID's runs into rows by room x service x diameter.
length_source auto measures on the P&ID only when the scale check calls it
to scale; otherwise each run is the RMST of its tags' layout positions plus
vertical_allowance per connection, with the drawn length kept beside it as
schematic_m. length_source=pid on a schematic P&ID is refused with the
statistics unless force=True. Status A / B / C; a C run goes to control
with its schematic length, counted and flagged. Routed lengths are shared
among the layout rooms their route crosses (track F's face finder); the
allowance is its own column. Lengths are metres from the unit the geometry
implies: a layout declaring inches over millimetres is read in mm and warned
about, never multiplied by 25.4.

The takeoff carries narrow stand-ins for the unit inference, the plan-copy
clusters and the scale check (group U's modules are on another branch); a
scale result passed in is used first. Tags written twice in the chosen
cluster are ambiguous, never picked.

Beyond the plan: the scale stand-in calls a non-positive median ratio
insufficient (as group U's scale_check does) instead of to_scale with a
factor of 0; an INSUNITS code the reader does not convert is named in the
unit warning instead of being called "no unit"; wall layers are classified
once per layer, not once per record.

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

64 lines
2.3 KiB
Python

"""Orthogonal lengths on a layout: Manhattan distance and the rectilinear MST.
A pipe or cable on a plant floor runs along the building grid, so its length
between two points is estimated as |dx| + |dy| - never the diagonal. A run that
joins more than two pieces of equipment is estimated as the minimum spanning
tree of their positions under that distance (Prim's algorithm); a two-ended run
is simply |dx| + |dy|. This is the tree on the given points only - no Steiner
points are added - so it is the length a drafter gets by routing each
connection separately, and it is exact for the points given.
A route is drawn along x first, then y (`route_legs`): deterministic, and
stated on every method sheet that uses it.
"""
from __future__ import annotations
import math
from collections.abc import Sequence
from engineering.understand.snapshot import Pt
__all__ = ["manhattan", "rmst", "route_legs"]
def manhattan(a: Pt, b: Pt) -> float:
"""|dx| + |dy| between two points."""
return abs(a[0] - b[0]) + abs(a[1] - b[1])
def rmst(points: Sequence[Pt]) -> tuple[float, tuple[tuple[int, int], ...]]:
"""Rectilinear minimum spanning tree: (total length, edges as (from, to) indices).
Prim's algorithm from point 0; each edge is (the tree vertex it grows from,
the new vertex), in the order the tree grows. Ties go to the lower index,
so the answer is deterministic. Fewer than two points: (0.0, ()).
"""
n = len(points)
if n < 2:
return 0.0, ()
in_tree = [False] * n
best = [math.inf] * n
parent = [-1] * n
best[0] = 0.0
total = 0.0
edges: list[tuple[int, int]] = []
for _ in range(n):
k = min((i for i in range(n) if not in_tree[i]), key=lambda i: (best[i], i))
in_tree[k] = True
if parent[k] >= 0:
edges.append((parent[k], k))
total += best[k]
for j in range(n):
if not in_tree[j]:
d = manhattan(points[k], points[j])
if d < best[j]:
best[j], parent[j] = d, k
return total, tuple(edges)
def route_legs(a: Pt, b: Pt) -> tuple[tuple[Pt, Pt], ...]:
"""The orthogonal route from a to b: along x first, then y; zero-length legs dropped."""
corner = (b[0], a[1])
return tuple(leg for leg in ((a, corner), (corner, b)) if leg[0] != leg[1])