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>
205 lines
7.6 KiB
Python
205 lines
7.6 KiB
Python
"""Built-in node templates: SVG symbol + port configuration + default props.
|
|
|
|
A :class:`NodeTemplate` is a reusable definition dragged from the node panel to
|
|
create :class:`NodeModel` instances on the canvas.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from typing import Callable, Iterable, Optional
|
|
|
|
from .node import NodeModel
|
|
from .port import Port, PortDirection, PortSide
|
|
from .properties import Property, PropertyBag, PropertyGroup, PropertyType
|
|
|
|
|
|
def _presentation(title: str, color: str = "#37474f") -> PropertyGroup:
|
|
return PropertyGroup("Presentation", [
|
|
Property("title", "Title", PropertyType.STRING, title),
|
|
Property("color", "Color", PropertyType.COLOR, color),
|
|
Property("notes", "Notes", PropertyType.STRING, ""),
|
|
])
|
|
|
|
|
|
class NodeTemplate:
|
|
"""Definition of a draggable node type."""
|
|
|
|
def __init__(
|
|
self,
|
|
key: str,
|
|
label: str,
|
|
group: str,
|
|
svg_name: str,
|
|
ports: Iterable[Port],
|
|
*,
|
|
width: float = 80.0,
|
|
height: float = 80.0,
|
|
default_color: str = "#37474f",
|
|
physics: Optional[Callable[[], PropertyGroup]] = None,
|
|
) -> None:
|
|
self.key = key
|
|
self.label = label
|
|
self.group = group
|
|
self.svg_name = svg_name
|
|
self.width = width
|
|
self.height = height
|
|
self.default_color = default_color
|
|
self._ports = list(ports)
|
|
self._physics = physics
|
|
|
|
def make_properties(self) -> PropertyBag:
|
|
bag = PropertyBag([_presentation(self.label, self.default_color)])
|
|
if self._physics is not None:
|
|
bag.add_group(self._physics())
|
|
return bag
|
|
|
|
def instantiate(self, node_id: str, x: float, y: float) -> NodeModel:
|
|
return NodeModel(
|
|
node_id,
|
|
self.key,
|
|
svg_name=self.svg_name,
|
|
x=x,
|
|
y=y,
|
|
width=self.width,
|
|
height=self.height,
|
|
ports=[p.clone() for p in self._ports],
|
|
properties=self.make_properties(),
|
|
)
|
|
|
|
|
|
class NodeLibrary:
|
|
"""Ordered collection of templates, grouped for the node panel."""
|
|
|
|
def __init__(self, templates: Optional[Iterable[NodeTemplate]] = None) -> None:
|
|
self._templates: dict[str, NodeTemplate] = {}
|
|
for t in templates or []:
|
|
self.add(t)
|
|
|
|
def add(self, template: NodeTemplate) -> NodeTemplate:
|
|
self._templates[template.key] = template
|
|
return template
|
|
|
|
def get(self, key: str) -> Optional[NodeTemplate]:
|
|
return self._templates.get(key)
|
|
|
|
def groups(self) -> dict[str, list[NodeTemplate]]:
|
|
out: dict[str, list[NodeTemplate]] = {}
|
|
for t in self._templates.values():
|
|
out.setdefault(t.group, []).append(t)
|
|
return out
|
|
|
|
def __iter__(self):
|
|
return iter(self._templates.values())
|
|
|
|
|
|
# -- physics group builders ----------------------------------------------
|
|
def _pump_physics() -> PropertyGroup:
|
|
return PropertyGroup("Physics", [
|
|
Property("head", "Rated Head", PropertyType.FLOAT, 30.0, unit="m", minimum=0),
|
|
Property("power", "Power", PropertyType.FLOAT, 5.5, unit="kW", minimum=0),
|
|
Property("status", "Status", PropertyType.ENUM, "on", options=["on", "off"]),
|
|
])
|
|
|
|
|
|
def _tank_physics() -> PropertyGroup:
|
|
return PropertyGroup("Physics", [
|
|
Property("volume", "Volume", PropertyType.FLOAT, 100.0, unit="m3", minimum=0),
|
|
Property("level", "Level", PropertyType.FLOAT, 50.0, unit="%", minimum=0, maximum=100),
|
|
Property("elevation", "Elevation", PropertyType.FLOAT, 0.0, unit="m"),
|
|
])
|
|
|
|
|
|
def _valve_physics() -> PropertyGroup:
|
|
return PropertyGroup("Physics", [
|
|
Property("diameter", "Diameter", PropertyType.CATALOGUE_ITEM, "DN100",
|
|
catalogue="pipe_classes"),
|
|
Property("state", "State", PropertyType.ENUM, "open",
|
|
options=["open", "closed", "throttled"]),
|
|
Property("opening", "Opening", PropertyType.FLOAT, 100.0, unit="%",
|
|
minimum=0, maximum=100),
|
|
])
|
|
|
|
|
|
def _source_physics() -> PropertyGroup:
|
|
return PropertyGroup("Physics", [
|
|
Property("supply", "Supply Rate", PropertyType.FLOAT, 120.0, unit="m3/h", minimum=0),
|
|
Property("pressure", "Pressure", PropertyType.FLOAT, 4.0, unit="bar", minimum=0),
|
|
])
|
|
|
|
|
|
def _sink_physics() -> PropertyGroup:
|
|
return PropertyGroup("Physics", [
|
|
Property("demand", "Demand", PropertyType.FLOAT, 40.0, unit="m3/h", minimum=0),
|
|
Property("flow_type", "Flow Type", PropertyType.ENUM, "steady",
|
|
options=["steady", "peak", "intermittent"]),
|
|
])
|
|
|
|
|
|
def _junction_physics() -> PropertyGroup:
|
|
return PropertyGroup("Physics", [
|
|
Property("elevation", "Elevation", PropertyType.FLOAT, 0.0, unit="m"),
|
|
])
|
|
|
|
|
|
def _transformer_physics() -> PropertyGroup:
|
|
return PropertyGroup("Physics", [
|
|
Property("rating", "Rating", PropertyType.FLOAT, 250.0, unit="kVA", minimum=0),
|
|
Property("primary_v", "Primary", PropertyType.FLOAT, 11.0, unit="kV", minimum=0),
|
|
Property("secondary_v", "Secondary", PropertyType.FLOAT, 0.4, unit="kV", minimum=0),
|
|
])
|
|
|
|
|
|
def default_library() -> NodeLibrary:
|
|
"""A starter library covering water and power network symbols."""
|
|
W = "water"
|
|
P = "power"
|
|
lib = NodeLibrary()
|
|
|
|
lib.add(NodeTemplate(
|
|
"source", "Water Source", "Sources", "source.svg",
|
|
[Port("out", W, PortSide.RIGHT, 0.5, name="Outlet", direction=PortDirection.OUTPUT)],
|
|
default_color="#1565c0", physics=_source_physics,
|
|
))
|
|
lib.add(NodeTemplate(
|
|
"tank", "Storage Tank", "Storage", "tank.svg",
|
|
[Port("in", W, PortSide.TOP, 0.35, name="Inlet", direction=PortDirection.INPUT),
|
|
Port("out", W, PortSide.BOTTOM, 0.65, name="Outlet", direction=PortDirection.OUTPUT)],
|
|
default_color="#0277bd", physics=_tank_physics,
|
|
))
|
|
lib.add(NodeTemplate(
|
|
"pump", "Pump", "Equipment", "pump.svg",
|
|
[Port("in", W, PortSide.LEFT, 0.5, name="Suction", direction=PortDirection.INPUT),
|
|
Port("out", W, PortSide.RIGHT, 0.5, name="Discharge", direction=PortDirection.OUTPUT)],
|
|
default_color="#00838f", physics=_pump_physics,
|
|
))
|
|
lib.add(NodeTemplate(
|
|
"valve", "Valve", "Equipment", "valve.svg",
|
|
[Port("in", W, PortSide.LEFT, 0.5, name="In", direction=PortDirection.INPUT),
|
|
Port("out", W, PortSide.RIGHT, 0.5, name="Out", direction=PortDirection.OUTPUT)],
|
|
width=80, height=50, default_color="#455a64", physics=_valve_physics,
|
|
))
|
|
lib.add(NodeTemplate(
|
|
"junction", "Junction", "Junctions", "junction.svg",
|
|
[Port("n", W, PortSide.TOP, 0.5, name="N"),
|
|
Port("e", W, PortSide.RIGHT, 0.5, name="E"),
|
|
Port("s", W, PortSide.BOTTOM, 0.5, name="S"),
|
|
Port("w", W, PortSide.LEFT, 0.5, name="W")],
|
|
width=50, height=50, default_color="#546e7a", physics=_junction_physics,
|
|
))
|
|
lib.add(NodeTemplate(
|
|
"consumer", "Consumer", "Sinks", "consumer.svg",
|
|
[Port("in", W, PortSide.LEFT, 0.5, name="Inlet", direction=PortDirection.INPUT)],
|
|
default_color="#5d4037", physics=_sink_physics,
|
|
))
|
|
lib.add(NodeTemplate(
|
|
"transformer", "Transformer", "Power", "transformer.svg",
|
|
[Port("hv", P, PortSide.TOP, 0.5, name="HV", direction=PortDirection.INPUT),
|
|
Port("lv", P, PortSide.BOTTOM, 0.5, name="LV", direction=PortDirection.OUTPUT)],
|
|
default_color="#2e7d32", physics=_transformer_physics,
|
|
))
|
|
lib.add(NodeTemplate(
|
|
"power_source", "Power Source", "Power", "power_source.svg",
|
|
[Port("out", P, PortSide.RIGHT, 0.5, name="Feed", direction=PortDirection.OUTPUT)],
|
|
default_color="#388e3c", physics=_source_physics,
|
|
))
|
|
return lib
|