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: 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 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: `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())