Files
autocad-mcp-pro/engineering/understand/draw.py
T
Umut CANandClaude Opus 5.5 c4759e696d fix(understand): the pipe network's tolerance follows the P&ID's unit, and a dropped stub is said
pipe_takeoff merged line ends closer than tol = 1.0 drawing units and
silently dropped any segment shorter than that. On a millimetre P&ID that is
1 mm. On a metre drawing it is a metre: ends a metre apart were merged and
every pipe under a metre disappeared from the totals, with no warning. The
review of the network engine found this.

- tol now defaults to 1 mm in the unit the P&ID's geometry implies: 1.0 on
  a millimetre drawing, 0.001 on a metre one. A tol the caller gives is
  used as given.
- build_network counts the segments that collapse to a point
  (stats.segments_below_tol), and pipe_rows turns a non-zero count into a
  warning naming the tolerance.

A new wire test builds a metre P&ID with a 0.6 m pipe and a 5 m pipe. Before
this change the takeoff said 5.0 m; now it says 5.6 m. The network test
checks that a sub-tolerance stub is counted. Registry, packs, aliases and
every track H suite: 472 passed. The inventory matches the live surface.

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

124 lines
4.2 KiB
Python

"""The async layer of track H's takeoffs: read the drawings, run the pure engine, write the workbook.
The only module of the takeoffs that touches a backend, and it only reads:
`take_snapshot` gives one bulk read of each drawing (a file, or the current
document), the pure modules compute in a worker thread so a 35,000-entity
P&ID does not stall the server, and the workbook is a file beside the path the
caller names. The drawings themselves are never modified.
"""
from __future__ import annotations
import asyncio
import math
from engineering.understand.network import build_network
from engineering.understand.report import check_lang, write_workbook
from engineering.understand.snapshot import take_snapshot
from engineering.understand.takeoff import (
cable_rows,
check_allowance,
check_pipe_options,
check_section_rules,
pipe_rows,
unit_of,
)
__all__ = ["run_cable_takeoff", "run_pipe_takeoff"]
def _check_tol(tol) -> float:
try:
value = float(tol)
except (TypeError, ValueError):
raise ValueError(f"tol: expected a number, got {tol!r}") from None
if not math.isfinite(value) or value <= 0.0:
raise ValueError(f"tol: must be a finite number greater than zero, got {tol!r}")
return value
async def run_pipe_takeoff(
backend,
*,
pid_path: str | None = None,
layout_path: str | None = None,
output: str | None = None,
lang: str = "en",
services=None,
allowance: float = 0.20,
vertical_allowance: float = 0.0,
length_source: str = "auto",
force: bool = False,
layers=None,
tol: float | None = None,
) -> dict:
"""`pipe_rows` over a P&ID and an optional layout, plus the workbook when `output` is named.
Every option is checked before a drawing is read. `pid_path=None` reads the
current document as the P&ID. The result is `pipe_rows`'s dict with
``files`` added: `write_workbook`'s answer, or None when no `output` was named.
"""
check_lang(lang)
options = check_pipe_options(
services=services,
allowance=allowance,
vertical_allowance=vertical_allowance,
length_source=length_source,
)
tol = None if tol is None else _check_tol(tol)
if length_source == "layout" and not layout_path:
raise ValueError("length_source='layout' needs a layout drawing; none was given")
pid = await take_snapshot(backend, pid_path)
layout = await take_snapshot(backend, layout_path) if layout_path else None
def compute() -> dict:
# 1 mm in the unit the P&ID's geometry implies: a fixed 1.0 would be a
# metre on a metre drawing, merging ends a metre apart and dropping pipes
joint = tol if tol is not None else 1.0 / (unit_of(pid)["m_per_unit"] * 1000.0)
network = build_network(pid, layers=layers, tol=joint)
return pipe_rows(
network,
pid,
layout,
services=options["services"],
allowance=options["allowance"],
vertical_allowance=options["vertical_allowance"],
length_source=options["length_source"],
force=bool(force),
)
result = await asyncio.to_thread(compute)
result["files"] = (
await asyncio.to_thread(write_workbook, result, kind="pipe", lang=lang, path=output)
if output
else None
)
return result
async def run_cable_takeoff(
backend,
*,
pid_path: str | None = None,
layout_path: str | None = None,
output: str | None = None,
lang: str = "en",
allowance: float = 0.20,
section_rules=None,
) -> dict:
"""`cable_rows` over a P&ID and an optional layout, plus the workbook when `output` is named."""
check_lang(lang)
allowance = check_allowance(allowance)
rules = check_section_rules(section_rules)
pid = await take_snapshot(backend, pid_path)
layout = await take_snapshot(backend, layout_path) if layout_path else None
result = await asyncio.to_thread(
cable_rows, pid, layout, allowance=allowance, section_rules=list(rules) or None
)
result["files"] = (
await asyncio.to_thread(write_workbook, result, kind="cable", lang=lang, path=output)
if output
else None
)
return result