Round dashboards: shape and ring, the ring drawn and turned in every look, Knob preset and face guide in the editor
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
f067fb60d4
commit
8b8cb5cda9
@@ -90,6 +90,12 @@ INPUT_WIDGETS = {
|
||||
"color",
|
||||
}
|
||||
|
||||
#: What a round dashboard may draw on its ring: the widgets that are one value
|
||||
#: along a range, which is what turning a knob's ring can set or a circle's
|
||||
#: edge can show. Mirrored in the client
|
||||
#: (``frontend/src/components/Dashboard/DashboardView.tsx``).
|
||||
RING_WIDGETS = {"slider", "dropdown", "color", "gauge"}
|
||||
|
||||
#: What a colour widget puts on the wire, by the format it was configured for.
|
||||
#: The default is what the Node-RED instance this ports already sends its
|
||||
#: DMX encoders — ``[h, s, v]``, hue in degrees and the other two in percent —
|
||||
@@ -475,6 +481,14 @@ class DashboardDef(BaseModel):
|
||||
#: "unset" and the client falls back to its default.
|
||||
canvas_width: int = Field(default=1920, ge=0, le=7680)
|
||||
canvas_height: int = Field(default=1080, ge=0, le=4320)
|
||||
#: ``round`` draws the dashboard in the circle inscribed in its canvas, for
|
||||
#: a round screen such as a rotary knob. A circle has one size, so a round
|
||||
#: canvas is square: its height is its width.
|
||||
shape: Literal["rect", "round"] = "rect"
|
||||
#: The widget a round dashboard draws along its edge instead of in the grid
|
||||
#: — one of ``RING_WIDGETS``, by id — which a knob's ring turns. Empty
|
||||
#: draws no ring.
|
||||
ring: str = ""
|
||||
#: A lucide icon name, drawn on the panel rail; empty falls back to two
|
||||
#: letters of the title.
|
||||
icon: str = ""
|
||||
@@ -534,6 +548,24 @@ class DashboardDef(BaseModel):
|
||||
)
|
||||
return self
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _check_shape(self) -> DashboardDef:
|
||||
"""Square a round canvas, and hold the ring to a widget it can draw."""
|
||||
if self.shape == "round":
|
||||
self.canvas_height = self.canvas_width
|
||||
if not self.ring:
|
||||
return self
|
||||
if self.shape != "round":
|
||||
raise ValueError("only a round dashboard has a ring")
|
||||
widget = next((w for w in self.widgets if w.id == self.ring), None)
|
||||
if widget is None:
|
||||
raise ValueError(
|
||||
f"the ring names '{self.ring}', which is not a widget here"
|
||||
)
|
||||
if widget.type not in RING_WIDGETS:
|
||||
raise ValueError(f"a '{widget.type}' widget cannot be drawn on the ring")
|
||||
return self
|
||||
|
||||
@property
|
||||
def setting_messages(self) -> list[str]:
|
||||
"""Every message a bound setting reads. Empty for a static dashboard."""
|
||||
@@ -549,6 +581,9 @@ class DashboardDef(BaseModel):
|
||||
"""
|
||||
shapes = []
|
||||
for widget in self.widgets:
|
||||
# Drawn along the edge, so it takes no place in the grid.
|
||||
if widget.id == self.ring:
|
||||
continue
|
||||
at = (
|
||||
widget.layout.get("lg")
|
||||
or widget.layout.get("md")
|
||||
|
||||
@@ -445,6 +445,27 @@ def test_a_dashboard_is_cut_into_a_sane_number_of_columns():
|
||||
DashboardDef(name="house", columns=columns)
|
||||
|
||||
|
||||
def test_a_round_dashboard_is_square_and_rings_one_of_its_own_widgets():
|
||||
dial = WidgetDef(id="dial", type="slider", layout={"lg": Placement(x=1)})
|
||||
stat = WidgetDef(id="stat", type="stat")
|
||||
|
||||
knob = DashboardDef(
|
||||
name="knob",
|
||||
shape="round",
|
||||
canvas_width=360,
|
||||
canvas_height=200,
|
||||
ring="dial",
|
||||
widgets=[dial, stat],
|
||||
)
|
||||
|
||||
assert knob.canvas_height == 360
|
||||
# Drawn along the edge, so the outline of the grid leaves it out.
|
||||
assert [shape.type for shape in knob.footprint] == ["stat"]
|
||||
for shape, ring in (("rect", "dial"), ("round", "gone"), ("round", "stat")):
|
||||
with pytest.raises(ValueError):
|
||||
DashboardDef(name="knob", shape=shape, ring=ring, widgets=[dial, stat])
|
||||
|
||||
|
||||
def test_a_setting_refuses_a_message_it_cannot_carry():
|
||||
"""The same rule a widget binding is held to, from the document alone."""
|
||||
DashboardDef(
|
||||
|
||||
Reference in New Issue
Block a user