Properties¶
editor-visible, validated, range-bounded node values.
▶ Run in browserTags: basics property inspector
A Property is a descriptor declared as a class attribute. It gives a node a
typed value the editor inspector can show and the scene serializer persists,
with automatic range clamping and an optional on_change hook. Here two bars
are driven entirely by their Property values: on_update animates the values,
the renderer reads them back, and an out-of-range write is clamped in place so
the drawing never exceeds its bounds.
What it demonstrates¶
Property(default, range=(lo, hi))– a class attribute holding a validated value, read/written viaself.attr.Range clamping: assigning outside
(lo, hi)silently clamps to the nearest bound (no exception).Property(default, on_change="method")– a bound method fires only when the value actually changes.Property values driving what a node draws each frame.
Source¶
1"""Properties: editor-visible, validated, range-bounded node values.
2
3A `Property` is a descriptor declared as a class attribute. It gives a node a
4typed value the editor inspector can show and the scene serializer persists,
5with automatic range clamping and an optional `on_change` hook. Here two bars
6are driven entirely by their `Property` values: `on_update` animates the values,
7the renderer reads them back, and an out-of-range write is clamped in place so
8the drawing never exceeds its bounds.
9
10# /// simvx
11# tags = ["basics", "property", "inspector"]
12# web = { root = "PropertiesDemo", width = 800, height = 600, responsive = true }
13# ///
14
15## What it demonstrates
16
17- `Property(default, range=(lo, hi))` -- a class attribute holding a validated value, read/written via `self.attr`.
18- Range clamping: assigning outside `(lo, hi)` silently clamps to the nearest bound (no exception).
19- `Property(default, on_change="method")` -- a bound method fires only when the value actually changes.
20- Property values driving what a node draws each frame.
21"""
22
23import colorsys
24import math
25
26from simvx.core import Node2D, Property, Vec2
27from simvx.graphics import App
28
29WIDTH, HEIGHT = 800, 600
30
31
32class Bar(Node2D):
33 """A horizontal bar whose width and height come straight from Properties."""
34
35 # Editor-visible, validated values. Reads/writes go through `self.length` etc.
36 length = Property(200.0, range=(0.0, 400.0), hint="Bar length in pixels")
37 height = Property(40.0, range=(10.0, 80.0), hint="Bar thickness in pixels")
38 # on_change fires only when the clamped value differs from the previous one.
39 hue = Property(0.0, range=(0.0, 1.0), on_change="_rebuild_colour", hint="Bar hue 0..1")
40
41 def on_ready(self):
42 self._rebuild_colour() # seed the cached colour from the initial hue
43
44 def _rebuild_colour(self):
45 # Cache a colour from the hue so the hook has visible, value-driven output.
46 r, g, b = colorsys.hsv_to_rgb(self.hue, 1.0, 1.0)
47 self._colour = (r, g, b, 1.0)
48
49 def on_draw(self, renderer):
50 renderer.draw_rect(self.position, (self.length, self.height), colour=self._colour, filled=True)
51 # Outline marks the full range so clamping at the maximum is visible.
52 renderer.draw_rect(self.position, (400.0, self.height), colour=(1, 1, 1, 0.25))
53
54
55class PropertiesDemo(Node2D):
56 # Each Bar draws its OWN Properties, so it auto-dirties when they change and
57 # needs nothing extra. This parent instead draws a numeric readout of the
58 # bars' values every frame -- a cross-node read of state it doesn't own, which
59 # the retained renderer can't detect -- so it opts into per-frame redraw.
60 dynamic = True
61
62 def on_ready(self):
63 self._t = 0.0
64 # Two bars, each animated purely by writing to its Properties.
65 self.bar_a = self.add_child(Bar(position=Vec2(60, 200)))
66 self.bar_b = self.add_child(Bar(position=Vec2(60, 320)))
67 self.bar_b.height = 30.0
68
69 def on_update(self, dt: float):
70 self._t += dt
71 # Drive length with a sine: deliberately overshoots 400 so the range
72 # clamps it, holding the bar at its maximum instead of overflowing.
73 self.bar_a.length = 200.0 + 260.0 * math.sin(self._t)
74 self.bar_a.hue = (self._t * 0.15) % 1.0
75 # Second bar tracks a different phase to show independent Property state.
76 self.bar_b.length = 200.0 + 260.0 * math.sin(self._t * 0.7 + 1.0)
77 self.bar_b.hue = (self._t * 0.25 + 0.5) % 1.0
78
79 def on_draw(self, renderer):
80 renderer.draw_text("Property-driven, clamped bars", (20, 20), scale=2, colour=(1, 1, 1))
81 grey = (0.7, 0.7, 0.7)
82 readout = f"bar_a.length = {self.bar_a.length:6.1f} (clamped to 0..400)"
83 renderer.draw_text(readout, (20, 56), scale=1, colour=grey)
84 renderer.draw_text(f"bar_b.length = {self.bar_b.length:6.1f}", (20, 80), scale=1, colour=grey)
85
86
87if __name__ == "__main__":
88 App(title="Properties", width=WIDTH, height=HEIGHT).run(PropertiesDemo())