Files
autocad-mcp-pro/engineering/pid/geometry.py
T

157 lines
4.5 KiB
Python

# engineering/pid/geometry.py
"""Primitive-spec helpers for authoring symbols (block-local millimetres).
Each helper returns a ``block_define`` entity/ATTDEF spec (see
``backends/block_specs.py``), so a symbol builder composes plain dicts and the
same spec is validated and written identically on both engines.
"""
from __future__ import annotations
import math
def line(x1: float, y1: float, x2: float, y2: float) -> dict:
return {"type": "line", "x1": float(x1), "y1": float(y1), "x2": float(x2), "y2": float(y2)}
def circle(cx: float, cy: float, r: float) -> dict:
return {"type": "circle", "cx": float(cx), "cy": float(cy), "r": float(r)}
def arc(cx: float, cy: float, r: float, start_deg: float, end_deg: float) -> dict:
return {
"type": "arc",
"cx": float(cx),
"cy": float(cy),
"r": float(r),
"start_deg": float(start_deg),
"end_deg": float(end_deg),
}
def polyline(points, closed: bool = True, bulges=None) -> dict:
spec = {
"type": "polyline",
"points": [[float(x), float(y)] for x, y in points],
"closed": closed,
}
if bulges is not None:
spec["bulges"] = [float(b) for b in bulges]
return spec
def text(
t: str,
x: float,
y: float,
height: float,
align: str = "middle_center",
rotation_deg: float = 0.0,
) -> dict:
return {
"type": "text",
"text": t,
"x": float(x),
"y": float(y),
"height": float(height),
"align": align,
"rotation_deg": float(rotation_deg),
}
def solid(points) -> dict:
return {"type": "solid", "points": [[float(x), float(y)] for x, y in points]}
def dashed_line(
x1: float, y1: float, x2: float, y2: float, on: float = 1.0, off: float = 1.0
) -> list[dict]:
"""A dashed line as short LINE primitives, so the block needs no linetype."""
length = math.hypot(x2 - x1, y2 - y1)
if length <= 0:
raise ValueError("dashed_line: zero-length line")
if on <= 0 or off < 0:
raise ValueError("dashed_line: 'on' must be > 0 and 'off' >= 0")
ux, uy = (x2 - x1) / length, (y2 - y1) / length
out: list[dict] = []
pos = 0.0
while pos < length - 1e-9:
end = min(pos + on, length)
out.append(line(x1 + ux * pos, y1 + uy * pos, x1 + ux * end, y1 + uy * end))
pos = end + off
return out
def regular_polygon(
cx: float, cy: float, r: float, n: int, start_deg: float = 0.0
) -> list[tuple[float, float]]:
return [
(
cx + r * math.cos(math.radians(start_deg + 360.0 * i / n)),
cy + r * math.sin(math.radians(start_deg + 360.0 * i / n)),
)
for i in range(n)
]
def attdef(
tag: str,
x: float,
y: float,
height: float,
align: str = "center",
invisible: bool = False,
default: str = "",
) -> dict:
return {
"tag": tag,
"prompt": tag,
"default": default,
"x": float(x),
"y": float(y),
"height": float(height),
"align": align,
"invisible": invisible,
}
def _arc_extent(cx, cy, r, start_deg, end_deg):
"""Points along the arc at 1° steps plus both ends (CCW from start to end)."""
pts = []
sweep = (end_deg - start_deg) % 360.0 or 360.0
for k in range(0, int(sweep) + 1):
a = math.radians(start_deg + k)
pts.append((cx + r * math.cos(a), cy + r * math.sin(a)))
a = math.radians(end_deg)
pts.append((cx + r * math.cos(a), cy + r * math.sin(a)))
return pts
def bbox_of(primitives) -> tuple[float, float, float, float]:
"""Extent of the drawn geometry (text counts by its insertion point only)."""
xs: list[float] = []
ys: list[float] = []
for p in primitives:
kind = p["type"]
if kind == "line":
xs += [p["x1"], p["x2"]]
ys += [p["y1"], p["y2"]]
elif kind == "circle":
xs += [p["cx"] - p["r"], p["cx"] + p["r"]]
ys += [p["cy"] - p["r"], p["cy"] + p["r"]]
elif kind == "arc":
for x, y in _arc_extent(p["cx"], p["cy"], p["r"], p["start_deg"], p["end_deg"]):
xs.append(x)
ys.append(y)
elif kind in ("polyline", "solid"):
for x, y in p["points"]:
xs.append(x)
ys.append(y)
elif kind == "text":
xs.append(p["x"])
ys.append(p["y"])
if not xs:
return (0.0, 0.0, 0.0, 0.0)
return (min(xs), min(ys), max(xs), max(ys))