Add UI-agnostic model layer: typed grouped properties (string/int/float/ bool/enum/color/catalogue item(s)/value list), catalogue registry, port relations with color schemes and compatibility rules, ports, node/edge models, and DiagramDocument with change signals and JSON round-trip. Include a starter node library (source/tank/pump/valve/junction/consumer/ transformer) with matching SVG symbols, plus model+document tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
136 lines
4.5 KiB
Python
136 lines
4.5 KiB
Python
"""Port relations (media/domain a port carries) and their color schemes.
|
|
|
|
A relation like ``water`` or ``power`` defines both a palette and the set of
|
|
relations it may connect to. Ports may only be wired together when their
|
|
relations are mutually compatible.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from typing import Iterable, Iterator, Optional
|
|
|
|
|
|
class PortRelation:
|
|
"""A connectable domain, e.g. water / power / gas.
|
|
|
|
Attributes:
|
|
key: stable identifier.
|
|
label: display name.
|
|
color: base ``#rrggbb`` color used for ports and edges of this relation.
|
|
compatible: set of relation keys this one may connect to. A relation is
|
|
always compatible with itself; ``compatible`` lists *additional* keys.
|
|
"""
|
|
|
|
__slots__ = ("key", "label", "color", "compatible")
|
|
|
|
def __init__(
|
|
self,
|
|
key: str,
|
|
label: str,
|
|
color: str,
|
|
compatible: Optional[Iterable[str]] = None,
|
|
) -> None:
|
|
self.key = key
|
|
self.label = label
|
|
self.color = color
|
|
self.compatible = set(compatible or [])
|
|
|
|
def can_connect(self, other: "PortRelation") -> bool:
|
|
if other.key == self.key:
|
|
return True
|
|
return other.key in self.compatible or self.key in other.compatible
|
|
|
|
def to_dict(self) -> dict:
|
|
return {
|
|
"key": self.key,
|
|
"label": self.label,
|
|
"color": self.color,
|
|
"compatible": sorted(self.compatible),
|
|
}
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: dict) -> "PortRelation":
|
|
return cls(data["key"], data.get("label", data["key"]),
|
|
data.get("color", "#888888"), data.get("compatible"))
|
|
|
|
|
|
class RelationRegistry:
|
|
"""Registry of relations plus named color schemes.
|
|
|
|
A *color scheme* remaps relation base colors, letting the whole diagram be
|
|
re-themed (e.g. light vs. dark, or a high-contrast palette) without touching
|
|
per-item data.
|
|
"""
|
|
|
|
def __init__(self, relations: Optional[Iterable[PortRelation]] = None) -> None:
|
|
self._relations: dict[str, PortRelation] = {}
|
|
for r in relations or []:
|
|
self.register(r)
|
|
# scheme name -> {relation_key: color}
|
|
self._schemes: dict[str, dict[str, str]] = {}
|
|
self.active_scheme = "default"
|
|
|
|
def register(self, relation: PortRelation) -> PortRelation:
|
|
self._relations[relation.key] = relation
|
|
return relation
|
|
|
|
def get(self, key: str) -> Optional[PortRelation]:
|
|
return self._relations.get(key)
|
|
|
|
def __iter__(self) -> Iterator[PortRelation]:
|
|
return iter(self._relations.values())
|
|
|
|
def can_connect(self, key_a: str, key_b: str) -> bool:
|
|
a, b = self._relations.get(key_a), self._relations.get(key_b)
|
|
if a is None or b is None:
|
|
return False
|
|
return a.can_connect(b)
|
|
|
|
# -- color schemes ----------------------------------------------------
|
|
def add_scheme(self, name: str, colors: dict[str, str]) -> None:
|
|
self._schemes[name] = dict(colors)
|
|
|
|
def scheme_names(self) -> list[str]:
|
|
return ["default", *[n for n in self._schemes if n != "default"]]
|
|
|
|
def set_active_scheme(self, name: str) -> None:
|
|
self.active_scheme = name
|
|
|
|
def color(self, relation_key: str) -> str:
|
|
"""Resolve the color for a relation under the active scheme."""
|
|
scheme = self._schemes.get(self.active_scheme, {})
|
|
if relation_key in scheme:
|
|
return scheme[relation_key]
|
|
rel = self._relations.get(relation_key)
|
|
return rel.color if rel else "#888888"
|
|
|
|
|
|
def default_registry() -> RelationRegistry:
|
|
"""Starter relations covering common utility networks."""
|
|
reg = RelationRegistry([
|
|
PortRelation("water", "Water", "#1e88e5"),
|
|
PortRelation("hot_water", "Hot Water", "#e53935", compatible=["water"]),
|
|
PortRelation("gas", "Gas", "#fdd835"),
|
|
PortRelation("power", "Power", "#43a047"),
|
|
PortRelation("signal", "Signal", "#8e24aa"),
|
|
PortRelation("sewage", "Sewage", "#6d4c41"),
|
|
])
|
|
# A high-contrast alternative scheme.
|
|
reg.add_scheme("high_contrast", {
|
|
"water": "#0000ff",
|
|
"hot_water": "#ff0000",
|
|
"gas": "#ffcc00",
|
|
"power": "#00cc00",
|
|
"signal": "#cc00cc",
|
|
"sewage": "#663300",
|
|
})
|
|
# A muted / dark-friendly scheme.
|
|
reg.add_scheme("muted", {
|
|
"water": "#6fa8dc",
|
|
"hot_water": "#e06666",
|
|
"gas": "#ffd966",
|
|
"power": "#93c47d",
|
|
"signal": "#b4a7d6",
|
|
"sewage": "#a67c52",
|
|
})
|
|
return reg
|