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: drag-free sliders animate 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: drag-free sliders animate the
7values, the renderer reads them back, and an out-of-range write is clamped in
8place so the 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 math
24
25from simvx.core import Node2D, Property, Vec2
26from simvx.graphics import App
27
28WIDTH, HEIGHT = 800, 600
29
30
31class Bar(Node2D):
32 """A horizontal bar whose width and height come straight from Properties."""
33
34 # Editor-visible, validated values. Reads/writes go through `self.length` etc.
35 length = Property(200.0, range=(0.0, 400.0), hint="Bar length in pixels")
36 height = Property(40.0, range=(10.0, 80.0), hint="Bar thickness in pixels")
37 # on_change fires only when the clamped value differs from the previous one.
38 hue = Property(0.0, range=(0.0, 1.0), on_change="_rebuild_colour", hint="Bar hue 0..1")
39
40 def on_ready(self):
41 self._colour = (1.0, 0.4, 0.2, 1.0)
42 self._rebuild_colour() # seed the cached colour from the initial hue
43
44 def _rebuild_colour(self):
45 # Cheap hue -> RGB so the hook has visible, value-driven output.
46 h = self.hue * 6.0
47 c = 1.0 - abs(h % 2.0 - 1.0)
48 table = [(1, c, 0), (c, 1, 0), (0, 1, c), (0, c, 1), (c, 0, 1), (1, 0, c)]
49 r, g, b = table[min(int(h), 5)]
50 self._colour = (r, g, b, 1.0)
51
52 def on_draw(self, renderer):
53 x, y = float(self.position[0]), float(self.position[1])
54 renderer.draw_rect((x, y), (self.length, self.height), colour=self._colour, filled=True)
55 # Outline marks the full range so clamping at the maximum is visible.
56 renderer.draw_rect((x, y), (400.0, self.height), colour=(1, 1, 1, 0.25))
57
58
59class PropertiesDemo(Node2D):
60 def on_ready(self):
61 self._t = 0.0
62 # Two bars, each animated purely by writing to its Properties.
63 self.bar_a = self.add_child(Bar(position=Vec2(60, 200)))
64 self.bar_b = self.add_child(Bar(position=Vec2(60, 320)))
65 self.bar_b.height = 30.0
66
67 def on_update(self, dt: float):
68 self._t += dt
69 # Drive length with a sine: deliberately overshoots 400 so the range
70 # clamps it, holding the bar at its maximum instead of overflowing.
71 self.bar_a.length = 200.0 + 260.0 * math.sin(self._t)
72 self.bar_a.hue = (self._t * 0.15) % 1.0
73 # Second bar tracks a different phase to show independent Property state.
74 self.bar_b.length = 200.0 + 260.0 * math.sin(self._t * 0.7 + 1.0)
75 self.bar_b.hue = (self._t * 0.25 + 0.5) % 1.0
76
77 def on_draw(self, renderer):
78 renderer.draw_text("Property: range-clamped, on_change-driven bars", (20, 20), scale=2, colour=(1, 1, 1))
79 renderer.draw_text(f"bar_a.length = {self.bar_a.length:6.1f} (clamped to 0..400)", (20, 56), scale=1, colour=(0.7, 0.7, 0.7))
80 renderer.draw_text(f"bar_b.length = {self.bar_b.length:6.1f}", (20, 80), scale=1, colour=(0.7, 0.7, 0.7))
81
82
83if __name__ == "__main__":
84 App(title="Properties", width=WIDTH, height=HEIGHT).run(PropertiesDemo())