314 lines
9.5 KiB
Python
314 lines
9.5 KiB
Python
"""Símbolos navales para el editor de mímicos.
|
|
|
|
Cada símbolo es un QGraphicsItemGroup parametrizable que:
|
|
- Se dibuja con el estilo Deep Ocean
|
|
- Tiene `tag_ref: str | None` (id del Tag al que está vinculado)
|
|
- Expone `kind: str` para serialización
|
|
- Soporta `setState(value)` que actualiza color/animación
|
|
|
|
Sprint 3: motor, bomba, válvula, tanque, sensor genérico, indicador, línea.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, field
|
|
from typing import ClassVar
|
|
|
|
from PySide6.QtCore import Qt
|
|
from PySide6.QtGui import (
|
|
QBrush,
|
|
QColor,
|
|
QFont,
|
|
QLinearGradient,
|
|
QPainterPath,
|
|
QPen,
|
|
)
|
|
from PySide6.QtWidgets import (
|
|
QGraphicsEllipseItem,
|
|
QGraphicsItem,
|
|
QGraphicsItemGroup,
|
|
QGraphicsLineItem,
|
|
QGraphicsPathItem,
|
|
QGraphicsRectItem,
|
|
QGraphicsTextItem,
|
|
)
|
|
|
|
from vmssailor.studio.theme import (
|
|
C_ABYSS,
|
|
C_CYAN,
|
|
C_FOAM,
|
|
C_FOG,
|
|
C_IRON,
|
|
C_MIDNIGHT,
|
|
C_OK,
|
|
C_SAND,
|
|
C_STEEL,
|
|
)
|
|
|
|
SYMBOL_KINDS = (
|
|
"motor",
|
|
"pump",
|
|
"valve",
|
|
"tank",
|
|
"sensor",
|
|
"indicator",
|
|
"line",
|
|
"label",
|
|
)
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class SymbolSpec:
|
|
"""Spec serializable de un símbolo dentro de un mímico."""
|
|
|
|
kind: str
|
|
x: float
|
|
y: float
|
|
width: float = 80.0
|
|
height: float = 60.0
|
|
label: str = ""
|
|
tag_ref: str | None = None
|
|
rotation_deg: float = 0.0
|
|
extra: dict = field(default_factory=dict)
|
|
|
|
|
|
class _BaseSymbol(QGraphicsItemGroup):
|
|
"""Símbolo base. Subclases dibujan en `_build()`.
|
|
|
|
Cuando el usuario arrastra el símbolo, `itemChange` propaga la nueva
|
|
posición al `SymbolSpec` para que sobreviva a re-renders del editor.
|
|
"""
|
|
|
|
KIND: ClassVar[str] = "base"
|
|
|
|
def __init__(self, spec: SymbolSpec) -> None:
|
|
super().__init__()
|
|
# NOTA: asignar self.spec ANTES de setPos para que itemChange tenga
|
|
# acceso al spec cuando Qt dispare ItemPositionHasChanged durante init.
|
|
self.spec = spec
|
|
self.setFlag(QGraphicsItem.ItemIsMovable, True)
|
|
self.setFlag(QGraphicsItem.ItemIsSelectable, True)
|
|
self.setFlag(QGraphicsItem.ItemSendsGeometryChanges, True)
|
|
self.setPos(spec.x, spec.y)
|
|
self.setRotation(spec.rotation_deg)
|
|
self._build()
|
|
if spec.label:
|
|
self._add_label(spec.label)
|
|
|
|
def itemChange(self, change, value): # type: ignore[override]
|
|
"""Propaga la posición arrastrada al SymbolSpec en vivo."""
|
|
if change == QGraphicsItem.GraphicsItemChange.ItemPositionHasChanged:
|
|
spec = getattr(self, "spec", None)
|
|
if spec is not None:
|
|
spec.x = float(self.pos().x())
|
|
spec.y = float(self.pos().y())
|
|
return super().itemChange(change, value)
|
|
|
|
def _build(self) -> None:
|
|
raise NotImplementedError
|
|
|
|
def _add_label(self, text: str) -> None:
|
|
item = QGraphicsTextItem(text)
|
|
font = QFont("Inter", 8)
|
|
font.setBold(False)
|
|
item.setFont(font)
|
|
item.setDefaultTextColor(QColor(C_SAND))
|
|
rect = item.boundingRect()
|
|
item.setPos(-rect.width() / 2 + self.spec.width / 2, self.spec.height + 4)
|
|
self.addToGroup(item)
|
|
|
|
def set_state_color(self, color: QColor) -> None:
|
|
"""Cambia el color principal del símbolo (para reflejar estado del tag)."""
|
|
# Subclases lo implementan si tienen un elemento principal recoloreable
|
|
pass
|
|
|
|
|
|
class MotorSymbol(_BaseSymbol):
|
|
KIND = "motor"
|
|
|
|
def _build(self) -> None:
|
|
w, h = self.spec.width, self.spec.height
|
|
# Cuerpo (rectángulo con esquinas redondeadas)
|
|
body = QGraphicsRectItem(0, 0, w, h)
|
|
body.setBrush(QBrush(QColor(C_MIDNIGHT)))
|
|
body.setPen(QPen(QColor(C_CYAN), 2))
|
|
self.addToGroup(body)
|
|
self._body = body
|
|
# Letra M
|
|
letter = QGraphicsTextItem("M")
|
|
letter.setFont(QFont("Space Grotesk", 22, QFont.Weight.Bold))
|
|
letter.setDefaultTextColor(QColor(C_CYAN))
|
|
rect = letter.boundingRect()
|
|
letter.setPos((w - rect.width()) / 2, (h - rect.height()) / 2)
|
|
self.addToGroup(letter)
|
|
|
|
def set_state_color(self, color: QColor) -> None:
|
|
self._body.setPen(QPen(color, 2))
|
|
|
|
|
|
class PumpSymbol(_BaseSymbol):
|
|
KIND = "pump"
|
|
|
|
def _build(self) -> None:
|
|
w, h = self.spec.width, self.spec.height
|
|
# Circulo (cuerpo de bomba)
|
|
body = QGraphicsEllipseItem(0, 0, w, h)
|
|
body.setBrush(QBrush(QColor(C_MIDNIGHT)))
|
|
body.setPen(QPen(QColor(C_CYAN), 2))
|
|
self.addToGroup(body)
|
|
self._body = body
|
|
# Aspas en cruz
|
|
cx, cy = w / 2, h / 2
|
|
line1 = QGraphicsLineItem(cx - w / 3, cy, cx + w / 3, cy)
|
|
line2 = QGraphicsLineItem(cx, cy - h / 3, cx, cy + h / 3)
|
|
for ln in (line1, line2):
|
|
ln.setPen(QPen(QColor(C_CYAN), 2.5, Qt.PenStyle.SolidLine, Qt.PenCapStyle.RoundCap))
|
|
self.addToGroup(ln)
|
|
center = QGraphicsEllipseItem(cx - 3, cy - 3, 6, 6)
|
|
center.setBrush(QBrush(QColor(C_CYAN)))
|
|
center.setPen(QPen(Qt.PenStyle.NoPen))
|
|
self.addToGroup(center)
|
|
|
|
def set_state_color(self, color: QColor) -> None:
|
|
self._body.setPen(QPen(color, 2))
|
|
|
|
|
|
class ValveSymbol(_BaseSymbol):
|
|
KIND = "valve"
|
|
|
|
def _build(self) -> None:
|
|
w, h = self.spec.width, self.spec.height
|
|
# Reloj de arena (dos triángulos)
|
|
path = QPainterPath()
|
|
path.moveTo(0, 0)
|
|
path.lineTo(w, h)
|
|
path.lineTo(w, 0)
|
|
path.lineTo(0, h)
|
|
path.closeSubpath()
|
|
item = QGraphicsPathItem(path)
|
|
item.setBrush(QBrush(QColor(C_OK)))
|
|
item.setPen(QPen(QColor(C_ABYSS), 2))
|
|
self.addToGroup(item)
|
|
self._body = item
|
|
# Vástago
|
|
stem = QGraphicsLineItem(w / 2, -h / 2, w / 2, 0)
|
|
stem.setPen(QPen(QColor(C_FOG), 2))
|
|
self.addToGroup(stem)
|
|
handle = QGraphicsEllipseItem(w / 2 - 5, -h / 2 - 5, 10, 10)
|
|
handle.setBrush(QBrush(QColor(C_OK)))
|
|
handle.setPen(QPen(QColor(C_ABYSS)))
|
|
self.addToGroup(handle)
|
|
|
|
def set_state_color(self, color: QColor) -> None:
|
|
self._body.setBrush(QBrush(color))
|
|
|
|
|
|
class TankSymbol(_BaseSymbol):
|
|
KIND = "tank"
|
|
|
|
def _build(self) -> None:
|
|
w, h = self.spec.width, self.spec.height
|
|
body = QGraphicsRectItem(0, 0, w, h)
|
|
body.setBrush(QBrush(QColor(C_ABYSS)))
|
|
body.setPen(QPen(QColor(C_IRON), 2))
|
|
self.addToGroup(body)
|
|
# Fill (default 50%)
|
|
fill_h = h * 0.5
|
|
grad = QLinearGradient(0, h - fill_h, 0, h)
|
|
grad.setColorAt(0.0, QColor("#00D9FF"))
|
|
grad.setColorAt(1.0, QColor("#1B7FB5"))
|
|
fill = QGraphicsRectItem(2, h - fill_h + 2, w - 4, fill_h - 2)
|
|
fill.setBrush(QBrush(grad))
|
|
fill.setPen(QPen(Qt.PenStyle.NoPen))
|
|
self.addToGroup(fill)
|
|
self._fill = fill
|
|
self._h = h
|
|
self._w = w
|
|
|
|
def set_fill_pct(self, pct: float) -> None:
|
|
pct = max(0.0, min(100.0, pct))
|
|
fill_h = self._h * (pct / 100.0)
|
|
self._fill.setRect(2, self._h - fill_h + 2, self._w - 4, max(0, fill_h - 2))
|
|
|
|
|
|
class SensorSymbol(_BaseSymbol):
|
|
KIND = "sensor"
|
|
|
|
def _build(self) -> None:
|
|
w, h = self.spec.width, self.spec.height
|
|
body = QGraphicsEllipseItem(0, 0, w, h)
|
|
body.setBrush(QBrush(QColor(C_MIDNIGHT)))
|
|
body.setPen(QPen(QColor(C_OK), 2))
|
|
self.addToGroup(body)
|
|
self._body = body
|
|
letter = QGraphicsTextItem(self.spec.extra.get("letter", "T"))
|
|
letter.setFont(QFont("JetBrains Mono", 14, QFont.Weight.Bold))
|
|
letter.setDefaultTextColor(QColor(C_OK))
|
|
rect = letter.boundingRect()
|
|
letter.setPos((w - rect.width()) / 2, (h - rect.height()) / 2)
|
|
self.addToGroup(letter)
|
|
|
|
def set_state_color(self, color: QColor) -> None:
|
|
self._body.setPen(QPen(color, 2))
|
|
|
|
|
|
class IndicatorSymbol(_BaseSymbol):
|
|
KIND = "indicator"
|
|
|
|
def _build(self) -> None:
|
|
w, h = self.spec.width, self.spec.height
|
|
bg = QGraphicsRectItem(0, 0, w, h)
|
|
bg.setBrush(QBrush(QColor(C_MIDNIGHT)))
|
|
bg.setPen(QPen(QColor(C_STEEL), 1))
|
|
self.addToGroup(bg)
|
|
value = QGraphicsTextItem(self.spec.extra.get("value", "--"))
|
|
value.setFont(QFont("JetBrains Mono", 14, QFont.Weight.Bold))
|
|
value.setDefaultTextColor(QColor(C_FOAM))
|
|
rect = value.boundingRect()
|
|
value.setPos((w - rect.width()) / 2, (h - rect.height()) / 2)
|
|
self.addToGroup(value)
|
|
self._value = value
|
|
|
|
def set_value_text(self, text: str) -> None:
|
|
self._value.setPlainText(text)
|
|
|
|
|
|
class LineSymbol(_BaseSymbol):
|
|
KIND = "line"
|
|
|
|
def _build(self) -> None:
|
|
w, h = self.spec.width, self.spec.height
|
|
# Línea horizontal por defecto
|
|
line = QGraphicsLineItem(0, h / 2, w, h / 2)
|
|
line.setPen(QPen(QColor(C_CYAN), 2.5, Qt.PenStyle.SolidLine))
|
|
self.addToGroup(line)
|
|
|
|
|
|
class LabelSymbol(_BaseSymbol):
|
|
KIND = "label"
|
|
|
|
def _build(self) -> None:
|
|
text = self.spec.extra.get("text", self.spec.label or "Label")
|
|
item = QGraphicsTextItem(text)
|
|
font = QFont("Inter", 10, QFont.Weight.DemiBold)
|
|
item.setFont(font)
|
|
item.setDefaultTextColor(QColor(C_FOAM))
|
|
self.addToGroup(item)
|
|
|
|
|
|
SYMBOL_FACTORY = {
|
|
"motor": MotorSymbol,
|
|
"pump": PumpSymbol,
|
|
"valve": ValveSymbol,
|
|
"tank": TankSymbol,
|
|
"sensor": SensorSymbol,
|
|
"indicator": IndicatorSymbol,
|
|
"line": LineSymbol,
|
|
"label": LabelSymbol,
|
|
}
|
|
|
|
|
|
def make_symbol(spec: SymbolSpec) -> _BaseSymbol:
|
|
cls = SYMBOL_FACTORY.get(spec.kind, _BaseSymbol)
|
|
return cls(spec)
|