Ilya 62780c66be feat(model): domain model, node library, and SVG symbols
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>
2026-07-02 23:13:18 +02:00

110 lines
3.9 KiB
Python

"""Catalogues of reusable engineering items (pipe classes, materials, ...).
A ``CATALOGUE_ITEM`` / ``CATALOGUE_ITEMS`` property stores item id(s); the
catalogue registry resolves them to full records for display and calculation.
"""
from __future__ import annotations
from typing import Any, Iterable, Iterator, Optional
class CatalogueItem:
"""A single catalogue record: an id, a display name and arbitrary attributes."""
__slots__ = ("item_id", "name", "attributes")
def __init__(self, item_id: str, name: str, attributes: Optional[dict] = None) -> None:
self.item_id = item_id
self.name = name
self.attributes = dict(attributes or {})
def attr(self, key: str, default: Any = None) -> Any:
return self.attributes.get(key, default)
def to_dict(self) -> dict:
return {"id": self.item_id, "name": self.name, "attributes": self.attributes}
@classmethod
def from_dict(cls, data: dict) -> "CatalogueItem":
return cls(data["id"], data.get("name", data["id"]), data.get("attributes"))
def __repr__(self) -> str: # pragma: no cover
return f"CatalogueItem({self.item_id!r}, {self.name!r})"
class Catalogue:
"""A named, ordered collection of :class:`CatalogueItem`."""
def __init__(self, name: str, items: Optional[Iterable[CatalogueItem]] = None) -> None:
self.name = name
self._items: dict[str, CatalogueItem] = {}
for it in items or []:
self.add(it)
def add(self, item: CatalogueItem) -> CatalogueItem:
self._items[item.item_id] = item
return item
def get(self, item_id: str) -> Optional[CatalogueItem]:
return self._items.get(item_id)
def __iter__(self) -> Iterator[CatalogueItem]:
return iter(self._items.values())
def __len__(self) -> int:
return len(self._items)
def to_dict(self) -> dict:
return {"name": self.name, "items": [i.to_dict() for i in self._items.values()]}
@classmethod
def from_dict(cls, data: dict) -> "Catalogue":
return cls(data["name"], [CatalogueItem.from_dict(i) for i in data.get("items", [])])
class CatalogueRegistry:
"""Holds all catalogues available to a document."""
def __init__(self, catalogues: Optional[Iterable[Catalogue]] = None) -> None:
self._catalogues: dict[str, Catalogue] = {}
for c in catalogues or []:
self.register(c)
def register(self, catalogue: Catalogue) -> Catalogue:
self._catalogues[catalogue.name] = catalogue
return catalogue
def get(self, name: str) -> Optional[Catalogue]:
return self._catalogues.get(name)
def resolve(self, catalogue: str, item_id: str) -> Optional[CatalogueItem]:
cat = self._catalogues.get(catalogue)
return cat.get(item_id) if cat else None
def __iter__(self) -> Iterator[Catalogue]:
return iter(self._catalogues.values())
def to_dict(self) -> dict:
return {"catalogues": [c.to_dict() for c in self._catalogues.values()]}
@classmethod
def from_dict(cls, data: dict) -> "CatalogueRegistry":
return cls([Catalogue.from_dict(c) for c in data.get("catalogues", [])])
def default_registry() -> CatalogueRegistry:
"""A small starter registry of pipe classes and materials."""
pipe_classes = Catalogue("pipe_classes", [
CatalogueItem("DN50", "DN50", {"diameter_mm": 50, "pn": 16}),
CatalogueItem("DN100", "DN100", {"diameter_mm": 100, "pn": 16}),
CatalogueItem("DN200", "DN200", {"diameter_mm": 200, "pn": 10}),
CatalogueItem("DN300", "DN300", {"diameter_mm": 300, "pn": 10}),
])
materials = Catalogue("materials", [
CatalogueItem("steel", "Steel", {"roughness_mm": 0.045}),
CatalogueItem("pvc", "PVC", {"roughness_mm": 0.0015}),
CatalogueItem("copper", "Copper", {"roughness_mm": 0.0015}),
CatalogueItem("cast_iron", "Cast Iron", {"roughness_mm": 0.26}),
])
return CatalogueRegistry([pipe_classes, materials])