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>
110 lines
3.9 KiB
Python
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])
|