Files
autocad-mcp-pro/engineering/pid/symbols_valves.py
T
Umut CANandClaude Opus 5 352ce21030 fix(task-8): actuators stack on the body's real top and every stem meets its glyph
Two authoring slips in the valve x actuator composition:

- The diaphragm actuator's stem stopped at y=4 while the dome's flat base
  sat at y=6, so the canonical control-valve symbol (PID_VALVE_*_DIAPHRAGM,
  9 catalogue entries) shipped with its dome floating 2 mm above the stem.
- Actuators were placed at a fixed y=4 regardless of the body, so on the
  needle body (top 5.6) the piston/motor/solenoid glyphs were drawn over
  the knob and the hand bar across the needle's stem; the stem line(0,0,0,4)
  also duplicated the needle's own axial line (and the butterfly disc).

Placement is now body-relative: `_body` reports `axis_top` (where its own
geometry on x=0 ends) and the stem runs from there to the glyph base at
`body_top + STEM_CLEARANCE`; the diaphragm dome's base is the stem top
(base + DIAPHRAGM_YOKE), and the FAIL attribute follows the glyph base.
The reference 8x4 bodies keep their published numbers (globe+diaphragm
signal at y=9.0, bbox top 9.0).

Tests: per-combination check that the stem starts at the body's derived
axis top, the glyph's lowest edge equals the stem top, the glyph clears
the body, and the signal port is the topmost edge; plus a catalogue-wide
no-duplicate-primitives gate. 33 combinations fail on the old geometry.
The plan's Step 3 snippet is synced with the fixed module.

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

168 lines
7.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# engineering/pid/symbols_valves.py
"""Valve bodies (ISO 10628-2) composed with actuators (ISA-5.1 Table 5.5).
Every body is authored once in symbol-local mm (reference size 8 × 4, process
ports on the ``in`` / ``out`` faces) and any actuator stacks on top of it along
the stem at x = 0: the stem starts where the body's own geometry on that axis
ends (``axis_top``) and the actuator glyph sits ``STEM_CLEARANCE`` above the
body's top edge, so a tall body (needle, diaphragm) pushes the actuator up
instead of being overprinted by it. An actuated valve gains a ``signal`` port
on its topmost edge and a ``FAIL`` attribute; check and relief valves take no
actuator.
"""
from __future__ import annotations
from .geometry import arc, attdef, bbox_of, circle, line, polyline, text
from .symbols import DESC_HEIGHT, Port, SymbolSpec, make_spec, register, tag_attdefs
VALVE_BODIES = (
"gate",
"globe",
"ball",
"butterfly",
"check",
"needle",
"plug",
"diaphragm",
"three_way",
"angle",
"relief",
)
ACTUATORS = ("none", "diaphragm", "piston", "motor", "solenoid", "hand")
NO_ACTUATOR = frozenset({"check", "relief"})
SOURCE = "ISO 10628-2:2012 valves; ISA-5.1-2009 Table 5.5 actuators"
_LEFT = polyline([(-4, -2), (-4, 2), (0, 0)])
_RIGHT = polyline([(4, -2), (4, 2), (0, 0)])
_BOTTOM = polyline([(-2, -4), (2, -4), (0, 0)])
_IN = Port("in", -4.0, 0.0, 180.0)
_OUT = Port("out", 4.0, 0.0, 0.0)
_BOX = (-4.0, -2.0, 4.0, 2.0)
# Gap between a body's top edge and the actuator glyph — the visible stem on the
# reference 8 × 4 body (stem 0..4, glyph from y = 4).
STEM_CLEARANCE = 2.0
# Extra stem (the yoke) between the clearance point and a diaphragm dome's base.
DIAPHRAGM_YOKE = 2.0
# (primitives, process ports, declared bbox, axis_top) — ``axis_top`` is the y
# where the body's own geometry on the x = 0 axis ends, i.e. where an actuator
# stem may start without overprinting it.
Body = tuple[list[dict], tuple[Port, ...], tuple[float, float, float, float], float]
def _body(body: str) -> Body:
"""Primitives, process ports, declared bbox and axis top of a bare valve body."""
std = [_LEFT, _RIGHT]
if body == "gate":
return std, (_IN, _OUT), _BOX, 0.0
if body == "globe":
return std + [circle(0, 0, 1.0)], (_IN, _OUT), _BOX, 1.0
if body == "ball":
return std + [circle(0, 0, 1.8)], (_IN, _OUT), _BOX, 1.8
if body == "butterfly":
return std + [line(0, -2, 0, 2), circle(0, 0, 0.8)], (_IN, _OUT), _BOX, 2.0
if body == "check":
return std + [arc(0, 0, 2.5, 20, 160)], (_IN, _OUT), (-4.0, -2.0, 4.0, 2.5), 2.5
if body == "needle":
prims = std + [line(0, 0, 0, 5), circle(0, 5, 0.6)]
return prims, (_IN, _OUT), (-4.0, -2.0, 4.0, 5.6), 5.6
if body == "plug":
return std + [polyline([(-1, -1), (1, -1), (1, 1), (-1, 1)])], (_IN, _OUT), _BOX, 1.0
if body == "diaphragm":
return std + [arc(0, 2, 2, 0, 180)], (_IN, _OUT), (-4.0, -2.0, 4.0, 4.0), 4.0
if body == "three_way":
ports = (_IN, _OUT, Port("branch", 0.0, -4.0, 270.0))
return std + [_BOTTOM], ports, (-4.0, -4.0, 4.0, 2.0), 0.0
if body == "angle":
ports = (_IN, Port("out", 0.0, -4.0, 270.0))
return [_LEFT, _BOTTOM], ports, (-4.0, -4.0, 2.0, 2.0), 0.0
if body == "relief":
spring = polyline(
[(0, 0), (-1, 1.5), (1, 2.5), (-1, 3.5), (1, 4.5), (0, 6)],
closed=False,
)
ports = (Port("in", 0.0, -4.0, 270.0), _OUT)
return [_BOTTOM, _RIGHT, spring], ports, (-2.0, -4.0, 4.0, 6.0), 6.0
raise ValueError(f"unknown valve body {body!r}; valid: {', '.join(VALVE_BODIES)}")
def _actuator(
actuator: str, body_top: float, axis_top: float = 0.0
) -> tuple[list[dict], Port | None, float]:
"""Primitives above the body, the signal port (or None), and the new top y.
The stem (always the first primitive) runs from ``axis_top`` — where the
body's axial geometry ends — to the glyph base at
``body_top + STEM_CLEARANCE``; every glyph's lowest edge sits exactly on
the stem's top, so an actuator is never drawn detached from, or over, its
body.
"""
if actuator == "none":
return [], None, body_top
base = body_top + STEM_CLEARANCE
if actuator == "hand":
return [line(0, axis_top, 0, base), line(-2.5, base, 2.5, base)], None, base
if actuator == "diaphragm":
dome = base + DIAPHRAGM_YOKE
prims = [line(0, axis_top, 0, dome), arc(0, dome, 3, 0, 180), line(-3, dome, 3, dome)]
return prims, Port("signal", 0.0, dome + 3.0, 90.0, "signal"), dome + 3.0
stem = line(0, axis_top, 0, base)
if actuator == "piston":
top = base + 4.0
prims = [stem, polyline([(-2, base), (2, base), (2, top), (-2, top)])]
return prims, Port("signal", 0.0, top, 90.0, "signal"), top
if actuator == "motor":
top = base + 4.0
prims = [stem, circle(0, base + 2.0, 2), text("M", 0, base + 2.0, 2.0)]
return prims, Port("signal", 0.0, top, 90.0, "signal"), top
if actuator == "solenoid":
top = base + 3.0
box = polyline([(-1.5, base), (1.5, base), (1.5, top), (-1.5, top)])
prims = [stem, box, text("S", 0, base + 1.5, 1.8)]
return prims, Port("signal", 0.0, top, 90.0, "signal"), top
raise ValueError(f"unknown actuator {actuator!r}; valid: {', '.join(ACTUATORS)}")
def build_valve(body: str = "gate", actuator: str = "none") -> SymbolSpec:
"""Compose ``body`` with ``actuator`` into a ``SymbolSpec``.
Raises ``ValueError`` for an unknown body or actuator, and for an actuator
on a check or relief valve (they are self-acting).
"""
prims, ports, box, axis_top = _body(body)
if actuator not in ACTUATORS:
raise ValueError(f"unknown actuator {actuator!r}; valid: {', '.join(ACTUATORS)}")
if actuator != "none" and body in NO_ACTUATOR:
raise ValueError(f"{body} valves take no actuator (got {actuator!r})")
glyph_base = box[3] + STEM_CLEARANCE
extra, signal, top = _actuator(actuator, box[3], axis_top)
prims = list(prims) + extra
ports = tuple(ports) + ((signal,) if signal else ())
if extra:
# An actuator can be wider than a narrow body (angle: xmax 2 vs. a hand
# bar at ±2.5 or a diaphragm at ±3), so union its real extent, not just y.
ax0, ay0, ax1, ay1 = bbox_of(extra)
box = (min(box[0], ax0), min(box[1], ay0), max(box[2], ax1), max(box[3], top, ay1))
attdefs = list(tag_attdefs(box[3]))
if signal is not None:
# Beside the actuator glyph (spec 4.7): right of its widest extent (±3
# for a dome), 1.5 above the glyph base.
attdefs.append(attdef("FAIL", 3.5, glyph_base + 1.5, DESC_HEIGHT, align="left"))
variant = None if actuator == "none" else actuator
name = f"PID_VALVE_{body.upper()}" + (f"_{actuator.upper()}" if variant else "")
return make_spec(name, "valve", body, variant, prims, attdefs, ports, SOURCE, bbox=box)
def _valve_builder(body: str):
def build(actuator: str = "none") -> SymbolSpec:
return build_valve(body, actuator)
return build
for _body_name in VALVE_BODIES:
register(
_body_name, "valve", _valve_builder(_body_name), variants={"actuator": list(ACTUATORS)}
)