Properties

editor-visible, validated, range-bounded node values.

▶ Run in browser

Tags: 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 via self.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())