Эх сурвалжийг харах

app: cleanup pydrk and now allow drawing vectors over the wire :)

darkfi 3 долоо хоног өмнө
parent
commit
1226df19b1

+ 0 - 751
bin/app/gui/__init__.py

@@ -1,751 +0,0 @@
-from pydrk import SceneNodeType, PropertyType, vertex, face, serial
-from .print_tree import print_tree
-from .api import *
-from .gfx import Layer, add_object
-from . import settings
-import time
-
-latin = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
-numerals = "0123456789"
-punct = " .,<>/\\'[]{}:`~!@#$%^&*()_+?"
-keycmds = [
-    "Backspace"
-]
-
-class App(EventLoop):
-
-    def __init__(self):
-        super().__init__()
-
-        w = get_property("/window", "width")
-        h = get_property("/window", "height")
-
-        self.chatbox_layer = Layer("chatbox_layer")
-        self.chatbox_layer.resize(w, h)
-
-        self.chatbox_layer.add_obj("outline")
-        self.chatbox_layer.add_obj("user_input")
-
-        self.cursor_layer = Layer("cursor_layer")
-        self.cursor_layer.add_obj("cursor")
-        self.cursor_layer.resize(w, h)
-
-        self.rounded_box_layer = Layer("rounded_box_layer")
-        self.rounded_box_layer.add_obj("box")
-        self.rounded_box_layer.resize(w, h)
-
-        self.user_input = ""
-        self.last_keypress_time = 0
-
-    def resize_event(self, w, h):
-        self.chatbox_layer.resize(w, h)
-        self.cursor_layer.resize(w, h)
-        self.rounded_box_layer.resize(w, h)
-        resize_box()
-        resize_rounded_box()
-        draw_txt(self.user_input)
-
-    def mouse_click(self, x, y):
-        print(f"mouse click ({x}, {y})")
-
-    def mouse_wheel(self, y):
-        settings.ui_scale *= 1 + y/100
-        print(settings.ui_scale)
-
-    def key_down(self, keycode, keymods, repeat):
-        #print(f"key_down: '{keycode}'")
-        if keymods.ctrl and keycode == "=":
-            settings.ui_scale *= 1.01
-            print(settings.ui_scale)
-        elif keymods.ctrl and keycode == "-":
-            settings.ui_scale *= 0.99
-            print(settings.ui_scale)
-        elif keymods.ctrl and keycode == "H":
-            print("hello")
-        elif keymods.ctrl and keycode == "P":
-            print_tree()
-
-        elif keycode in latin:
-            key = keycode.upper() if keymods.shift else keycode.lower()
-            self.type_key(key, keymods, repeat)
-        elif keycode in numerals or keycode in punct or keycode in keycmds:
-            self.type_key(keycode, keymods, repeat)
-        #else:
-        #    print(keycode)
-
-    def type_key(self, key, _keymods, _repeat):
-        now = time.time()
-        if now - self.last_keypress_time < 0.2:
-            return
-        self.last_keypress_time = now
-        if key == "Backspace":
-            self.user_input = self.user_input[:-1]
-        else:
-            self.user_input += key
-        draw_txt(self.user_input)
-
-def draw_txt(user_input):
-    obj_id = add_object("/window/chatbox_layer", "user_input2")
-    # create a new one and link it
-    text_id = host.create_text("/font/inter-regular", "txt2", user_input, 30)
-    link_node(text_id, obj_id)
-
-    layer_w = get_property("/window/chatbox_layer", "rect_w")
-    layer_h = get_property("/window/chatbox_layer", "rect_h")
-
-    x = 20
-    y = layer_h - 20 - 30
-
-    set_property_f32("/window/chatbox_layer/user_input2", "x", x)
-    set_property_f32("/window/chatbox_layer/user_input2", "y", y)
-    set_property_f32("/window/chatbox_layer/user_input2/txt2", "r", 1)
-    set_property_f32("/window/chatbox_layer/user_input2/txt2", "g", 1)
-    set_property_f32("/window/chatbox_layer/user_input2/txt2", "b", 1)
-    set_property_f32("/window/chatbox_layer/user_input2/txt2", "a", 1)
-
-    # Switch visibility
-    set_property_bool("/window/chatbox_layer/user_input",  "is_visible", False)
-    set_property_bool("/window/chatbox_layer/user_input2", "is_visible", True)
-
-    # Remove the old object
-    old_id = lookup_node("/window/chatbox_layer/user_input")
-    unlink_from_parents(old_id)
-    remove_node_recursive(old_id)
-
-    rename_node("/window/chatbox_layer/user_input2/txt2", "txt")
-    rename_node("/window/chatbox_layer/user_input2",      "user_input")
-
-    reposition_cursor()
-
-def reposition_cursor():
-    layer_h = get_property("/window/chatbox_layer", "rect_h")
-    y = layer_h - 20 - 30
-
-    # Move the cursor
-    text_id = api.lookup_node_id("/window/chatbox_layer/user_input/txt")
-    text_px_w = 0
-    user_input = ""
-    if text_id is not None:
-        text_px_w = get_property(text_id, "width")
-        user_input = get_property(text_id, "text")
-    x = text_px_w + 25
-    if user_input and user_input[-1] == " ":
-        x += 10
-    set_property_f32("/window/cursor_layer/cursor", "x", x)
-    set_property_f32("/window/cursor_layer/cursor", "y", y)
-
-def draw_cursor():
-    node_id = api.add_node("cursor_box", SceneNodeType.RENDER_MESH)
-    api.add_property(node_id, "verts", PropertyType.BUFFER)
-    api.add_property(node_id, "faces", PropertyType.BUFFER)
-    link_node(node_id, "/window/cursor_layer/cursor")
-
-    x, y = 0, 0
-    w, h = 20, 40
-    vert1 = vertex(x,     y,     1, 1, 1, 1, 0, 0)
-    vert2 = vertex(x + w, y,     1, 1, 1, 1, 1, 0)
-    vert3 = vertex(x,     y + h, 1, 1, 1, 1, 0, 1)
-    vert4 = vertex(x + w, y + h, 1, 1, 1, 1, 1, 1)
-    api.set_property_buffer(node_id, "verts", vert1 + vert2 + vert3 + vert4)
-    api.set_property_buffer(node_id, "faces", face(0, 2, 1) + face(1, 2, 3))
-
-def draw_box():
-    node_id = api.add_node("box", SceneNodeType.RENDER_MESH)
-    api.add_property(node_id, "verts", PropertyType.BUFFER)
-    api.add_property(node_id, "faces", PropertyType.BUFFER)
-    link_node(node_id, "/window/chatbox_layer/outline")
-
-    node_id = api.add_node("inner_box", SceneNodeType.RENDER_MESH)
-    api.add_property(node_id, "verts", PropertyType.BUFFER)
-    api.add_property(node_id, "faces", PropertyType.BUFFER)
-    link_node(node_id, "/window/chatbox_layer/outline")
-
-    resize_box()
-
-def resize_box():
-    node_id = api.lookup_node_id("/window/chatbox_layer/outline/box")
-
-    layer_w = get_property("/window/chatbox_layer", "rect_w")
-    layer_h = get_property("/window/chatbox_layer", "rect_h")
-
-    box_h = 60
-    # Inner padding, so box inside will be (box_h - 2*padding) px high
-    padding = 10
-
-    # Lets add a poly - must be counterclockwise
-    x, y = 0, layer_h - box_h
-    w, h = layer_w, box_h
-    vert1 = vertex(x,     y,     1, 1, 1, 1, 0, 0)
-    vert2 = vertex(x + w, y,     1, 1, 1, 1, 1, 0)
-    vert3 = vertex(x,     y + h, 1, 1, 1, 1, 0, 1)
-    vert4 = vertex(x + w, y + h, 1, 1, 1, 1, 1, 1)
-    api.set_property_buffer(node_id, "verts", vert1 + vert2 + vert3 + vert4)
-    api.set_property_buffer(node_id, "faces", face(0, 2, 1) + face(1, 2, 3))
-
-    # Second mesh
-    node_id = api.lookup_node_id("/window/chatbox_layer/outline/inner_box")
-
-    x, y = x + padding, y + padding
-    w -= 2*padding
-    h -= 2*padding
-    vert1 = vertex(x,     y,     0, 0, 0, 1, 0, 0)
-    vert2 = vertex(x + w, y,     0, 0, 0, 1, 1, 0)
-    vert3 = vertex(x,     y + h, 0, 0, 0, 1, 0, 1)
-    vert4 = vertex(x + w, y + h, 0.3, 0.3, 0.3, 1, 1, 1)
-    api.set_property_buffer(node_id, "verts", vert1 + vert2 + vert3 + vert4)
-    api.set_property_buffer(node_id, "faces", face(0, 2, 1) + face(1, 2, 3))
-
-def draw_rounded_box():
-    node_id = api.add_node("box", SceneNodeType.RENDER_MESH)
-    api.add_property(node_id, "verts", PropertyType.BUFFER)
-    api.add_property(node_id, "faces", PropertyType.BUFFER)
-    link_node(node_id, "/window/rounded_box_layer/box")
-
-    node_id = api.add_node("inner_box", SceneNodeType.RENDER_MESH)
-    api.add_property(node_id, "verts", PropertyType.BUFFER)
-    api.add_property(node_id, "faces", PropertyType.BUFFER)
-    link_node(node_id, "/window/rounded_box_layer/box")
-
-    resize_rounded_box()
-
-def resize_rounded_box():
-    node_id = api.lookup_node_id("/window/rounded_box_layer/box/box")
-
-    layer_w = get_property("/window/rounded_box_layer", "rect_w")
-    layer_h = get_property("/window/rounded_box_layer", "rect_h")
-
-    # Inner padding, so box inside will be (box_h - 2*padding) px high
-    padding = 5
-
-    bevel = 20
-
-    # Lets add a poly - must be counterclockwise
-    x, y = layer_w/4, layer_h/4
-    w, h = layer_w/2, layer_h/2
-    y0 = y + bevel
-    h0 = h - 2*bevel
-    verts = (
-        vertex(x,     y0,     1, 1, 1, 1, 0, 0) +
-        vertex(x + w, y0,     1, 1, 1, 1, 1, 0) +
-        vertex(x,     y0 + h0, 1, 1, 1, 1, 0, 1) +
-        vertex(x + w, y0 + h0, 1, 1, 1, 1, 1, 1)
-    )
-    faces = face(0, 2, 1) + face(1, 2, 3)
-    x0 = x + bevel
-    w0 = w - 2*bevel
-    h0 = bevel
-    verts += (
-        vertex(x0,      y,     1, 1, 1, 1, 0, 0) +
-        vertex(x0 + w0, y,     1, 1, 1, 1, 1, 0) +
-        vertex(x0,      y + h0, 1, 1, 1, 1, 0, 1) +
-        vertex(x0 + w0, y + h0, 1, 1, 1, 1, 1, 1)
-    )
-    o = 4
-    faces += face(o + 0, o + 2, o + 1) + face(o + 1, o + 2, o + 3)
-    y0 = y + h - bevel
-    x0 = x + bevel
-    w0 = w - 2*bevel
-    h0 = bevel
-    verts += (
-        vertex(x0,      y0,     1, 1, 1, 1, 0, 0) +
-        vertex(x0 + w0, y0,     1, 1, 1, 1, 1, 0) +
-        vertex(x0,      y0 + h0, 1, 1, 1, 1, 0, 1) +
-        vertex(x0 + w0, y0 + h0, 1, 1, 1, 1, 1, 1)
-    )
-    o = 8
-    faces += face(o + 0, o + 2, o + 1) + face(o + 1, o + 2, o + 3)
-    api.set_property_buffer(node_id, "verts", verts)
-    api.set_property_buffer(node_id, "faces", faces)
-
-    # Second mesh
-    node_id = api.lookup_node_id("/window/rounded_box_layer/box/inner_box")
-
-    x, y = x + padding, y + padding
-    w -= 2*padding
-    h -= 2*padding
-    vert1 = vertex(x,     y,     0, 0, 0, 1, 0, 0)
-    vert2 = vertex(x + w, y,     0, 0, 0, 1, 1, 0)
-    vert3 = vertex(x,     y + h, 0, 0, 0, 1, 0, 1)
-    vert4 = vertex(x + w, y + h, 0.3, 0.3, 0.3, 1, 1, 1)
-    #api.set_property_buffer(node_id, "verts", vert1 + vert2 + vert3 + vert4)
-    #api.set_property_buffer(node_id, "faces", face(0, 2, 1) + face(1, 2, 3))
-
-def draw():
-    win_id = api.lookup_node_id("/window")
-
-    # Add foo layer
-
-    layer_id = api.add_node("foo", SceneNodeType.RENDER_LAYER)
-
-    prop = Property(
-        "is_visible", PropertyType.BOOL, PropertySubType.NULL,
-        None,
-        "Is Visible", "Visibility of the layer",
-        False, False, 1, None, None, []
-    )
-    api.add_property(layer_id, prop)
-    api.set_property_bool(layer_id, "is_visible", 0, True)
-
-    #prop = Property(
-    #    "redraw", PropertyType.BOOL, PropertySubType.NULL,
-    #    None,
-    #    "redraw", "Redraw this layer",
-    #    False, False, 1, None, None, []
-    #)
-    #api.add_property(layer_id, prop)
-    #api.set_property_bool(layer_id, "redraw", 0, True)
-
-    prop = Property(
-        "rect", PropertyType.UINT32, PropertySubType.PIXEL,
-        None,
-        "Rectangle", "The position and size within the layer",
-        False, True, 4, None, None, []
-    )
-    api.add_property(layer_id, prop)
-    # x
-    api.set_property_u32(layer_id, "rect", 0, 0)
-    # y
-    api.set_property_u32(layer_id, "rect", 1, 0)
-    # w
-    #api.set_property_u32(layer_id, "rect", 2, int(3838/2))
-    code = [["as_u32", ["/", ["load", "sw"], ["u32", 2]]]]
-    code = [["as_u32", ["load", "sw"]]]
-    api.set_property_expr(layer_id, "rect", 2, code)
-    # h
-    code = [["as_u32", ["/", ["load", "sh"], ["u32", 2]]]]
-    code = [["as_u32", ["load", "sh"]]]
-    api.set_property_expr(layer_id, "rect", 3, code)
-
-    api.link_node(layer_id, win_id)
-
-    # Add a bg box to our layer
-    node_id = api.add_node("bg", SceneNodeType.RENDER_MESH)
-
-    prop = Property(
-        "data", PropertyType.BUFFER, PropertySubType.NULL,
-        None,
-        "Mesh Data", "The face and vertex data for the mesh",
-        False, False, 2, None, None, []
-    )
-    api.add_property(node_id, prop)
-
-    #x, y = 0.1, 0.1
-    #w, h = 0.1, 0.1
-    x, y, w, h = 0, 0, 1, 1
-    #x, y, w, h = -1, 1, 2, -2
-    vert1 = vertex(x,     y,     0, 0.1, 0, 1, 0, 0)
-    vert2 = vertex(x + w, y,     0.1, 0, 0, 1, 1, 0)
-    vert3 = vertex(x,     y + h, 0.1, 0, 0, 1, 0, 1)
-    vert4 = vertex(x + w, y + h, 0.1, 0, 0, 1, 1, 1)
-
-    verts = vert1 + vert2 + vert3 + vert4
-    faces = face(0, 2, 1) + face(1, 2, 3)
-
-    api.set_property_buf(node_id, "data", 0, verts)
-    api.set_property_buf(node_id, "data", 1, faces)
-
-    prop = Property(
-        "rect", PropertyType.FLOAT32, PropertySubType.PIXEL,
-        None,
-        "Rectangle", "The position and size within the layer",
-        False, True, 4, None, None, []
-    )
-    api.add_property(node_id, prop)
-    # x
-    api.set_property_f32(node_id, "rect", 0, 0)
-    # y
-    api.set_property_f32(node_id, "rect", 1, 0)
-    # w
-    #api.set_property_f32(node_id, "rect", 2, 20)
-    code = [["-", ["load", "lw"], ["f32", 0]]]
-    api.set_property_expr(node_id, "rect", 2, code)
-    # h
-    #api.set_property_str(node_id, "rect", 3, "lh - 10")
-    code = [["-", ["load", "lh"], ["f32", 0]]]
-    api.set_property_expr(node_id, "rect", 3, code)
-
-    prop = Property(
-        "z_index", PropertyType.UINT32, PropertySubType.NULL,
-        None,
-        "Z-index", "Z-index: values greater than zero are deferred draws",
-        False, False, 1, None, None, []
-    )
-    api.add_property(node_id, prop)
-
-    api.link_node(node_id, layer_id)
-
-    # Add a second mesh to our layer
-
-    node_id = api.add_node("meshie2", SceneNodeType.RENDER_MESH)
-
-    prop = Property(
-        "data", PropertyType.BUFFER, PropertySubType.NULL,
-        None,
-        "Mesh Data", "The face and vertex data for the mesh",
-        False, False, 2, None, None, []
-    )
-    api.add_property(node_id, prop)
-
-    x, y, w, h = 0, 0, 1, 1
-    vert1 = vertex(x,     y,     1, 0, 1, 1, 0, 0)
-    vert2 = vertex(x + w, y,     0.5, 0, 1, 1, 1, 0)
-    vert3 = vertex(x,     y + h, 1, 0, 0.5, 1, 0, 1)
-    vert4 = vertex(x + w, y + h, 0.5, 1, 0.5, 1, 1, 1)
-
-    verts = vert1 + vert2 + vert3 + vert4
-    faces = face(0, 2, 1) + face(1, 2, 3)
-
-    api.set_property_buf(node_id, "data", 0, verts)
-    api.set_property_buf(node_id, "data", 1, faces)
-
-    prop = Property(
-        "rect", PropertyType.FLOAT32, PropertySubType.PIXEL,
-        None,
-        "Rectangle", "The position and size within the layer",
-        False, True, 4, None, None, []
-    )
-    api.add_property(node_id, prop)
-    api.set_property_f32(node_id, "rect", 0, 10)
-    api.set_property_f32(node_id, "rect", 1, 10)
-    api.set_property_f32(node_id, "rect", 2, 60)
-    api.set_property_f32(node_id, "rect", 3, 60)
-
-    prop = Property(
-        "z_index", PropertyType.UINT32, PropertySubType.NULL,
-        None,
-        "Z-index", "Z-index: values greater than zero are deferred draws",
-        False, False, 1, None, None, []
-    )
-    api.add_property(node_id, prop)
-
-    api.link_node(node_id, layer_id)
-
-    # Add some text
-
-    node_id = api.add_node("hellowurld", SceneNodeType.RENDER_TEXT)
-
-    prop = Property(
-        "rect", PropertyType.FLOAT32, PropertySubType.PIXEL,
-        None,
-        "Rectangle", "The position and size within the layer",
-        False, True, 4, None, None, []
-    )
-    api.add_property(node_id, prop)
-    api.set_property_f32(node_id, "rect", 0, 10)
-    api.set_property_f32(node_id, "rect", 1, 100)
-    api.set_property_f32(node_id, "rect", 2, 60)
-    api.set_property_f32(node_id, "rect", 3, 60)
-
-    prop = Property(
-        "baseline", PropertyType.FLOAT32, PropertySubType.PIXEL,
-        None,
-        "Baseline", "Y offset of baseline inside rect",
-        False, True, 1, None, None, []
-    )
-    api.add_property(node_id, prop)
-    api.set_property_f32(node_id, "baseline", 0, 50)
-
-    prop = Property(
-        "font_size", PropertyType.FLOAT32, PropertySubType.PIXEL,
-        None,
-        "Font Size", "Font Size",
-        False, True, 1, 0.0, None, []
-    )
-    api.add_property(node_id, prop)
-    api.set_property_f32(node_id, "font_size", 0, 30)
-
-    prop = Property(
-        "text", PropertyType.STR, PropertySubType.NULL,
-        None,
-        "Text", "Text",
-        False, False, 1, None, None, []
-    )
-    api.add_property(node_id, prop)
-    api.set_property_str(node_id, "text", 0, "hello!😁🍆jelly 🍆1234")
-
-    prop = Property(
-        "color", PropertyType.FLOAT32, PropertySubType.COLOR,
-        None,
-        "Color", "Color of the text",
-        False, False, 4, 0, 1, []
-    )
-    api.add_property(node_id, prop)
-    api.set_property_f32(node_id, "color", 0, 1)
-    api.set_property_f32(node_id, "color", 1, 1)
-    api.set_property_f32(node_id, "color", 2, 1)
-    api.set_property_f32(node_id, "color", 3, 1)
-
-    #prop = Property(
-    #    "overflow", PropertyType.ENUM, PropertySubType.NULL,
-    #    None,
-    #    "Overflow Behaviour", "Behaviour when text exceeds bounding box",
-    #    False, True, 1, None, None, [
-    #        "ScrollRight",
-    #        "OverflowRight"
-    #    ]
-    #)
-    #api.add_property(node_id, prop)
-    #api.set_property_enum(node_id, "overflow", 0, "ScrollRight")
-
-    prop = Property(
-        "z_index", PropertyType.UINT32, PropertySubType.NULL,
-        None,
-        "Z-index", "Z-index: values greater than zero are deferred draws",
-        False, False, 1, None, None, []
-    )
-    api.add_property(node_id, prop)
-
-    prop = Property(
-        "debug", PropertyType.BOOL, PropertySubType.NULL,
-        None,
-        "Debug", "Draw debug outlines",
-        False, False, 1, None, None, []
-    )
-    api.add_property(node_id, prop)
-
-    api.link_node(node_id, layer_id)
-
-    # EditBox
-
-    node_id = api.add_node("editz", SceneNodeType.EDIT_BOX)
-
-    prop = Property(
-        "is_active", PropertyType.BOOL, PropertySubType.NULL,
-        None,
-        "Is Active", "Whether the editbox is active",
-        False, True, 1, None, None, []
-    )
-    api.add_property(node_id, prop)
-    api.set_property_bool(node_id, "is_active", 0, True)
-
-    prop = Property(
-        "rect", PropertyType.FLOAT32, PropertySubType.PIXEL,
-        None,
-        "Rectangle", "The position and size within the layer",
-        False, True, 4, None, None, []
-    )
-    api.add_property(node_id, prop)
-    api.set_property_f32(node_id, "rect", 0, 60)
-    api.set_property_f32(node_id, "rect", 1, 200)
-    api.set_property_f32(node_id, "rect", 2, 300)
-    api.set_property_f32(node_id, "rect", 3, 60)
-
-    prop = Property(
-        "baseline", PropertyType.FLOAT32, PropertySubType.PIXEL,
-        None,
-        "Baseline", "Y offset of baseline inside rect",
-        False, True, 1, None, None, []
-    )
-    api.add_property(node_id, prop)
-    api.set_property_f32(node_id, "baseline", 0, 50)
-
-    prop = Property(
-        "scroll", PropertyType.FLOAT32, PropertySubType.NULL,
-        None,
-        "Scroll", "Current scroll",
-        False, False, 1, None, None, []
-    )
-    api.add_property(node_id, prop)
-
-    prop = Property(
-        "cursor_pos", PropertyType.UINT32, PropertySubType.NULL,
-        None,
-        "Cursor Position", "Cursor position within the text",
-        False, False, 1, None, None, []
-    )
-    api.add_property(node_id, prop)
-
-    prop = Property(
-        "font_size", PropertyType.FLOAT32, PropertySubType.PIXEL,
-        None,
-        "Font Size", "Font Size",
-        False, True, 1, 0.0, None, []
-    )
-    api.add_property(node_id, prop)
-    api.set_property_f32(node_id, "font_size", 0, 50)
-
-    prop = Property(
-        "text", PropertyType.STR, PropertySubType.NULL,
-        None,
-        "Text", "Text",
-        False, False, 1, None, None, []
-    )
-    api.add_property(node_id, prop)
-    api.set_property_str(node_id, "text", 0, "hello king!😁🍆jelly 🍆1234")
-
-    prop = Property(
-        "text_color", PropertyType.FLOAT32, PropertySubType.COLOR,
-        None,
-        "Text Color", "Color of the text",
-        False, False, 4, 0, 1, []
-    )
-    api.add_property(node_id, prop)
-    api.set_property_f32(node_id, "text_color", 0, 1)
-    api.set_property_f32(node_id, "text_color", 1, 1)
-    api.set_property_f32(node_id, "text_color", 2, 1)
-    api.set_property_f32(node_id, "text_color", 3, 1)
-
-    prop = Property(
-        "cursor_color", PropertyType.FLOAT32, PropertySubType.COLOR,
-        None,
-        "Cursor Color", "Color of the cursor",
-        False, False, 4, 0, 1, []
-    )
-    api.add_property(node_id, prop)
-    api.set_property_f32(node_id, "cursor_color", 0, 1)
-    api.set_property_f32(node_id, "cursor_color", 1, 0.5)
-    api.set_property_f32(node_id, "cursor_color", 2, 0.5)
-    api.set_property_f32(node_id, "cursor_color", 3, 1)
-
-    prop = Property(
-        "hi_bg_color", PropertyType.FLOAT32, PropertySubType.COLOR,
-        None,
-        "Highlight Bg Color", "Background color for highlighted text",
-        False, False, 4, 0, 1, []
-    )
-    api.add_property(node_id, prop)
-    api.set_property_f32(node_id, "hi_bg_color", 0, 1)
-    api.set_property_f32(node_id, "hi_bg_color", 1, 1)
-    api.set_property_f32(node_id, "hi_bg_color", 2, 1)
-    api.set_property_f32(node_id, "hi_bg_color", 3, 0.5)
-
-    prop = Property(
-        "selected", PropertyType.UINT32, PropertySubType.NULL,
-        None,
-        "Selected", "Selected range",
-        True, False, 2, 0, None, []
-    )
-    api.add_property(node_id, prop)
-    api.set_property_u32(node_id, "selected", 0, 1)
-    api.set_property_u32(node_id, "selected", 1, 4)
-
-    prop = Property(
-        "z_index", PropertyType.UINT32, PropertySubType.NULL,
-        None,
-        "Z-index", "Z-index: values greater than zero are deferred draws",
-        False, False, 1, None, None, []
-    )
-    api.add_property(node_id, prop)
-    api.set_property_u32(node_id, "z_index", 0, 4)
-
-    prop = Property(
-        "debug", PropertyType.BOOL, PropertySubType.NULL,
-        None,
-        "Debug", "Draw debug outlines",
-        False, False, 1, None, None, []
-    )
-    api.add_property(node_id, prop)
-    api.set_property_bool(node_id, "debug", 0, True)
-
-    arg_data = bytearray()
-    serial.write_u32(arg_data, node_id)
-    api.call_method(win_id, "create_edit_box", arg_data)
-
-    api.link_node(node_id, layer_id)
-
-    # ChatView
-
-    node_id = api.add_node("chatty", SceneNodeType.CHAT_VIEW)
-
-    prop = Property(
-        "rect", PropertyType.FLOAT32, PropertySubType.PIXEL,
-        None,
-        "Rectangle", "The position and size within the layer",
-        False, True, 4, None, None, []
-    )
-    api.add_property(node_id, prop)
-    api.set_property_f32(node_id, "rect", 0, 50)
-    api.set_property_f32(node_id, "rect", 1, 260)
-    code = [["-", ["load", "lw"], ["f32", 100]]]
-    api.set_property_expr(node_id, "rect", 2, code)
-    code = [["-", ["load", "lh"], ["f32", 260]]]
-    api.set_property_expr(node_id, "rect", 3, code)
-
-    prop = Property(
-        "debug", PropertyType.BOOL, PropertySubType.NULL,
-        None,
-        "Debug", "Draw debug outlines",
-        False, False, 1, None, None, []
-    )
-    api.add_property(node_id, prop)
-    api.set_property_bool(node_id, "debug", 0, True)
-
-    prop = Property(
-        "z_index", PropertyType.UINT32, PropertySubType.NULL,
-        None,
-        "Z-index", "Z-index: values greater than zero are deferred draws",
-        False, False, 1, None, None, []
-    )
-    api.add_property(node_id, prop)
-
-    arg_data = bytearray()
-    serial.write_u32(arg_data, node_id)
-    api.call_method(win_id, "create_chat_view", arg_data)
-
-    api.link_node(node_id, layer_id)
-
-class App2(EventLoop):
-
-    def key_down(self, keycode, keymods, repeat):
-        if repeat:
-            return
-
-        win_id = api.lookup_node_id("/window")
-        scale = api.get_property_value(win_id, "scale")[0]
-
-        if keymods.ctrl and keycode == "=":
-            scale *= 1.01
-        elif keymods.ctrl and keycode == "-":
-            scale *= 0.99
-
-        print(scale)
-        api.set_property_f32(win_id, "scale", 0, scale)
-
-def main():
-    draw()
-
-    # DEBUG
-    print_tree()
-
-    app = App2()
-    app.run()
-
-#def main():
-#    if True:
-#        node_id = api.add_node("foo", SceneNodeType.WINDOW)
-#        prop = Property(
-#            "myprop", PropertyType.FLOAT32, PropertySubType.NULL,
-#            None,
-#            "myprop", "",
-#            False, 2, None, None, []
-#        )
-#        api.add_property(1, prop)
-#        api.link_node(node_id, 0)
-#        api.set_property_f32(1, "myprop", 0, 4.0)
-#        api.set_property_f32(1, "myprop", 1, 110.0)
-#    print("val =", api.get_property_value(1, "myprop"))
-#    for prop in api.get_properties(1):
-#        print("Property:")
-#        print(f"  name = {prop.name}")
-#        print(f"  type = {prop.type}")
-#        print(f"  subtype = {prop.subtype}")
-#        print(f"  defaults = {prop.defaults}")
-#        print(f"  ui_name = {prop.ui_name}")
-#        print(f"  desc = {prop.desc}")
-#        print(f"  is_null_allowed = {prop.is_null_allowed}")
-#        print(f"  array_len = {prop.array_len}")
-#        print(f"  min_val = {prop.min_val}")
-#        print(f"  max_val = {prop.max_val}")
-#        print(f"  enum_items = {prop.enum_items}")
-#        print()
-#    print_tree()
-#    #garbage_collect()
-#
-#    #app = App()
-#    #draw_box()
-#    #draw_rounded_box()
-#    #draw_cursor()
-#    #reposition_cursor()
-#    ##print_tree()
-#    #app.run()
-

+ 0 - 158
bin/app/gui/api.py

@@ -1,158 +0,0 @@
-from collections import namedtuple
-from pydrk import Api, PropertyType, PropertySubType, Property, serial
-import zmq
-
-api = Api()
-print("Node status:", api.hello())
-
-def make_sub_socket():
-    context = zmq.Context()
-    socket = context.socket(zmq.SUB)
-    socket.setsockopt(zmq.SUBSCRIBE, b'')
-    socket.connect("tcp://localhost:9485")
-    return socket
-
-def rename_node(node, name):
-    node_id = lookup_node(node)
-    api.rename_node(node_id, name)
-
-def remove_all_slots(node_path, sig):
-    node_id = api.lookup_node_id(node_path)
-    for slot_id, slot in api.get_slots(node_id, sig):
-        print(f"{node_path}:{sig}(): Unregistering slot '{slot}':{slot_id}")
-        api.unregister_slot(node_id, sig, slot_id)
-
-def register_slot(node_path, sig, tag):
-    #remove_all_slots(node_path, sig)
-    node_id = api.lookup_node_id(node_path)
-    api.register_slot(node_id, sig, "", tag)
-
-def get_property(node_id, prop):
-    node_id = lookup_node(node_id)
-    return api.get_property_value(node_id, prop)
-
-def set_property(node_id, prop, val):
-    node_id = lookup_node(node_id)
-    match val:
-        case float():
-            api.set_property_f32(node_id, prop, val)
-        case int():
-            api.set_property_u32(node_id, prop, val)
-def set_property_bool(node_id, prop, val):
-    node_id = lookup_node(node_id)
-    api.set_property_bool(node_id, prop, val)
-def set_property_f32(node_id, prop, val):
-    node_id = lookup_node(node_id)
-    api.set_property_f32(node_id, prop, float(val))
-def set_property_u32(node_id, prop, val):
-    node_id = lookup_node(node_id)
-    api.set_property_u32(node_id, prop, int(val))
-
-def add_property_bool(node_id, prop, val=None):
-    api.add_property(node_id, prop, PropertyType.BOOL)
-    if val is not None:
-        api.set_property_bool(node_id, prop, val)
-def add_property_f32(node_id, prop, val=None):
-    api.add_property(node_id, prop, PropertyType.FLOAT32)
-    if val is not None:
-        api.set_property_f32(node_id, prop, val)
-def add_property_u32(node_id, prop, val=None):
-    api.add_property(node_id, prop, PropertyType.UINT32)
-    if val is not None:
-        api.set_property_u32(node_id, prop, val)
-
-def lookup_node(node_id):
-    if isinstance(node_id, str):
-        node_id = api.lookup_node_id(node_id)
-    return node_id
-
-def link_node(child_id, parent_id):
-    child_id = lookup_node(child_id)
-    parent_id = lookup_node(parent_id)
-    api.link_node(child_id, parent_id)
-def unlink_node(child_id, parent_id):
-    child_id = lookup_node(child_id)
-    parent_id = lookup_node(parent_id)
-    api.unlink_node(child_id, parent_id)
-
-def unlink_from_parents(node_id):
-    node_id = lookup_node(node_id)
-    for (_, parent_id, _) in api.get_parents(node_id):
-        api.unlink_node(node_id, parent_id)
-
-def remove_node_recursive(node_id):
-    node_id = lookup_node(node_id)
-
-    for (_, child_id, _) in api.get_children(node_id):
-        # Unlink the child
-        api.unlink_node(child_id, node_id)
-        # Remove the node
-        remove_node_recursive(child_id)
-
-    # Garbage collection
-    if not api.get_parents(node_id):
-        api.remove_node(node_id)
-
-def garbage_collect():
-    dangling = api.scan_dangling()
-    for node_id in dangling:
-        remove_node_recursive(node_id)
-    print(f"Garbage collect: removed {len(dangling)} nodes")
-
-KeyMods = namedtuple("KeyMods", ["shift", "ctrl", "alt", "logo"])
-
-class EventLoop:
-
-    def __init__(self):
-        self.subsock = make_sub_socket()
-        #register_slot("/window",                "resize",        b"rs")
-        #register_slot("/window/input/mouse",    "button_down",   b"ck")
-        #register_slot("/window/input/mouse",    "wheel",         b"wh")
-        #register_slot("/window/input/mouse",    "move",          b"mm")
-        register_slot("/window/input/keyboard", "key_down",      b"kd")
-
-    def run(self):
-        while True:
-            signal_data, user_data = self.subsock.recv_multipart()
-            cur = serial.Cursor(signal_data)
-            match user_data:
-                #case b"rs":
-                #    w = get_property("/window", "width")
-                #    h = get_property("/window", "height")
-                #    self.resize_event(w, h)
-                #case b"ck":
-                #    x = get_property("/window/input/mouse", "click_x")
-                #    y = get_property("/window/input/mouse", "click_y")
-                #    self.mouse_click(x, y)
-                #case b"wh":
-                #    y = get_property("/window/input/mouse", "wheel_y")
-                #    self.mouse_wheel(y)
-                #case b"mm":
-                #    pass
-                case b"kd":
-                    shift = bool(serial.read_u8(cur))
-                    ctrl = bool(serial.read_u8(cur))
-                    alt = bool(serial.read_u8(cur))
-                    logo = bool(serial.read_u8(cur))
-                    repeat = bool(serial.read_u8(cur))
-                    keycode = serial.decode_str(cur)
-
-                    keymods = KeyMods(shift, ctrl, alt, logo)
-                    # Sometimes these get stuck when exiting the window.
-                    # We don't need these anyway
-                    if keycode in ("LeftShift", "LeftSuper"):
-                        continue
-                    self.key_down(keycode, keymods, repeat)
-
-    def resize_event(self, w, h):
-        pass
-
-    def mouse_click(self, x, y):
-        pass
-
-    def mouse_wheel(self, y):
-        pass
-
-    def key_down(self, keycode, keymods, repeat):
-        pass
-

+ 0 - 52
bin/app/gui/gfx.py

@@ -1,52 +0,0 @@
-from pydrk import SceneNodeType, PropertyType, vertex, face
-from .api import *
-
-def clear_layer(layer_name):
-    layer_id = api.lookup_node_id(f"/window/{layer_name}")
-    if layer_id is None:
-        return
-
-    unlink_node(layer_id, "/window")
-
-    for (_, child_id, child_type) in api.get_children(layer_id):
-        api.unlink_node(child_id, layer_id)
-        if child_type == SceneNodeType.RENDER_OBJECT:
-            remove_node_recursive(child_id)
-
-    api.remove_node(layer_id)
-
-def add_layer(layer_name):
-    layer_id = api.add_node(layer_name, SceneNodeType.RENDER_LAYER)
-    add_property_u32(layer_id, "rect_x")
-    add_property_u32(layer_id, "rect_y")
-    add_property_u32(layer_id, "rect_w")
-    add_property_u32(layer_id, "rect_h")
-    add_property_bool(layer_id, "is_visible", True)
-    link_node(layer_id, "/window")
-    return layer_id
-
-def add_object(layer_id, obj_name):
-    layer_id = lookup_node(layer_id)
-    obj_id = api.add_node(obj_name, SceneNodeType.RENDER_OBJECT)
-    add_property_f32(obj_id, "x")
-    add_property_f32(obj_id, "y")
-    add_property_f32(obj_id, "scale_x", 1.0)
-    add_property_f32(obj_id, "scale_y", 1.0)
-    add_property_bool(obj_id, "is_visible", True)
-    link_node(obj_id, layer_id)
-    return obj_id
-
-class Layer:
-
-    def __init__(self, name):
-        self.name = name
-        clear_layer(name)
-        self.id = add_layer(name)
-
-    def resize(self, w, h):
-        set_property_u32(self.id, "rect_w", w)
-        set_property_u32(self.id, "rect_h", h)
-
-    def add_obj(self, name):
-        return add_object(self.id, name)
-

+ 0 - 2
bin/app/gui/settings.py

@@ -1,2 +0,0 @@
-ui_scale = 1.0
-

+ 3 - 0
bin/app/pydrk/__init__.py

@@ -1,4 +1,7 @@
 from .api import (Api, ErrorCode, SceneNodeType,
 from .api import (Api, ErrorCode, SceneNodeType,
                   PropertyType, PropertySubType, CallArgType, Property,
                   PropertyType, PropertySubType, CallArgType, Property,
                   Expr, vertex, face)
                   Expr, vertex, face)
+from .event import EventLoop, make_sub_socket
+from .print_tree import print_tree
+from .vector_shape import VectorShape
 from . import exc, serial
 from . import exc, serial

+ 51 - 0
bin/app/pydrk/event.py

@@ -0,0 +1,51 @@
+from collections import namedtuple
+
+import zmq
+
+from . import serial
+from .api import Api
+
+def make_sub_socket(addr="localhost", port=9485):
+    context = zmq.Context()
+    socket = context.socket(zmq.SUB)
+    socket.setsockopt(zmq.SUBSCRIBE, b'')
+    socket.connect(f"tcp://{addr}:{port}")
+    return socket
+
+KeyMods = namedtuple("KeyMods", ["shift", "ctrl", "alt", "logo"])
+
+class EventLoop:
+    """Subscribes to scene-graph signals over the netdebug PUB socket and
+    dispatches them to overridable handlers. Register extra slots in a
+    subclass constructor via self.register_slot()."""
+
+    def __init__(self, api, addr="localhost"):
+        self.api = api
+        self.subsock = make_sub_socket(addr)
+        self.register_slot("/window/input/keyboard", "key_down", b"kd")
+
+    def register_slot(self, node_path, sig, tag):
+        self.api.register_slot(node_path, sig, "", tag)
+
+    def run(self):
+        while True:
+            signal_data, user_data = self.subsock.recv_multipart()
+            cur = serial.Cursor(signal_data)
+            match user_data:
+                case b"kd":
+                    shift = bool(serial.read_u8(cur))
+                    ctrl = bool(serial.read_u8(cur))
+                    alt = bool(serial.read_u8(cur))
+                    logo = bool(serial.read_u8(cur))
+                    repeat = bool(serial.read_u8(cur))
+                    keycode = serial.decode_str(cur)
+
+                    keymods = KeyMods(shift, ctrl, alt, logo)
+                    # Sometimes these get stuck when exiting the window.
+                    # We don't need these anyway
+                    if keycode in ("LeftShift", "LeftSuper"):
+                        continue
+                    self.key_down(keycode, keymods, repeat)
+
+    def key_down(self, keycode, keymods, repeat):
+        pass

+ 5 - 12
bin/app/gui/print_tree.py → bin/app/pydrk/print_tree.py

@@ -1,16 +1,10 @@
-from pydrk import SceneNodeType, PropertyType, CallArgType, Expr
-from .api import api
+from .api import Api, PropertyType, SceneNodeType, CallArgType, Expr
 
 
-def join(parent_path, child_name):
-    if parent_path == "/":
-        return f"/{child_name}"
-    return f"{parent_path}/{child_name}"
-
-def print_tree(node_path="/", depth=None):
+def print_tree(api, node_path="/", depth=None):
     print(node_path)
     print(node_path)
-    print_node_info(node_path, depth, indent=1)
+    print_node_info(api, node_path, depth, indent=1)
 
 
-def print_node_info(parent_path, depth, indent):
+def print_node_info(api, parent_path, depth, indent):
     if indent - 1 == depth:
     if indent - 1 == depth:
         return
         return
 
 
@@ -58,7 +52,7 @@ def print_node_info(parent_path, depth, indent):
         else:
         else:
             child_path = parent_path + "/" + child_name
             child_path = parent_path + "/" + child_name
 
 
-        print_node_info(child_path, depth, indent+1)
+        print_node_info(api, child_path, depth, indent+1)
 
 
     for prop in api.get_properties(parent_path):
     for prop in api.get_properties(parent_path):
         prop_val = api.get_property_value(parent_path, prop.name)
         prop_val = api.get_property_value(parent_path, prop.name)
@@ -98,4 +92,3 @@ def print_node_info(parent_path, depth, indent):
 
 
         method_str = f"{method_name}(" + ", ".join(args) + ") -> (" + ", ".join(results) + ")"
         method_str = f"{method_name}(" + ", ".join(args) + ") -> (" + ", ".join(results) + ")"
         print(f"{ws}{method_str}")
         print(f"{ws}{method_str}")
-

+ 187 - 0
bin/app/pydrk/vector_shape.py

@@ -0,0 +1,187 @@
+"""Python reimplementation of the shape-building routines from
+src/ui/vector_art/shape.rs.
+
+Coordinates are expr source strings ("w/2", "h - 10") or numbers (normalized
+to float literals), matching the wire format of Api.set_property_shape: the
+app compiles and evaluates them server-side, so there is no client-side
+eval here. scaled()/offset() wrap coordinates in arithmetic just like the
+app-side op surgery.
+"""
+
+import math
+
+def _coord(x):
+    if isinstance(x, str):
+        return x
+    return repr(float(x))
+
+def _mul(a, b):
+    return f"({_coord(a)} * {_coord(b)})"
+
+def _add(a, b):
+    return f"({_coord(a)} + {_coord(b)})"
+
+class VectorShape:
+
+    def __init__(self):
+        # [x_expr, y_expr, [r, g, b, a]]
+        self.verts = []
+        self.indices = []
+
+    def _vertex(self, x, y, color):
+        self.verts.append([_coord(x), _coord(y), list(color)])
+
+    def set(self, api, node_path, prop_name="shape", i=0):
+        api.set_property_shape(node_path, prop_name, i, self.verts, self.indices)
+
+    def join(self, other):
+        off = len(self.verts)
+        self.verts.extend([list(v) for v in other.verts])
+        self.indices.extend([index + off for index in other.indices])
+
+    def add_filled_box(self, x1, y1, x2, y2, color):
+        self.add_gradient_box(x1, y1, x2, y2, [color, color, color, color])
+
+    # Colors go clockwise from top-left
+    def add_gradient_box(self, x1, y1, x2, y2, color):
+        color = [list(c) for c in color]
+        base = len(self.verts)
+        self._vertex(x1, y1, color[0])
+        self._vertex(x2, y1, color[1])
+        self._vertex(x1, y2, color[3])
+        self._vertex(x2, y2, color[2])
+        self.indices.extend([base + 0, base + 2, base + 1, base + 1, base + 2, base + 3])
+
+    # Create a smooth vertical gradient by subdividing into multiple strips.
+    # gamma: low gamma below 0.5 is good
+    def add_smooth_vertical_gradient(self, x1, y1, x2, y2, top_color, bottom_color, strips, gamma):
+        for i in range(strips):
+            t0 = i / strips
+            t1 = (i + 1) / strips
+
+            # Interpolate colors with gamma correction
+            t0_color = t0 ** gamma
+            t1_color = t1 ** gamma
+            color_top = [top_color[j] + (bottom_color[j] - top_color[j]) * t0_color for j in range(4)]
+            color_bottom = [top_color[j] + (bottom_color[j] - top_color[j]) * t1_color for j in range(4)]
+
+            # Y coordinates use linear spacing (equal strip heights)
+            y_top = _add(_mul(1.0 - t0, y1), _mul(t0, y2))
+            y_bottom = _add(_mul(1.0 - t1, y1), _mul(t1, y2))
+
+            self.add_gradient_box(
+                x1,
+                y_top,
+                x2,
+                y_bottom,
+                [color_top, color_top, color_bottom, color_bottom],
+            )
+
+    def add_outline(self, x1, y1, x2, y2, border_px, color):
+        # LHS
+        self.add_filled_box(x1, y1, _add(x1, border_px), y2, color)
+        # THS
+        self.add_filled_box(x1, y1, x2, _add(y1, border_px), color)
+        # RHS
+        self.add_filled_box(_add(x2, -border_px), y1, x2, y2, color)
+        # BHS
+        self.add_filled_box(x1, _add(y2, -border_px), x2, y2, color)
+
+    # Draw a line of a certain thickness between two points.
+    # Coordinates are constants, so this does not track expressions like `w` or `h`.
+    def add_line(self, from_x, from_y, to_x, to_y, thickness, color):
+        dx = to_x - from_x
+        dy = to_y - from_y
+        length = math.sqrt(dx * dx + dy * dy)
+        if length == 0.:
+            return
+
+        half = thickness / 2.
+        px = -dy / length * half
+        py = dx / length * half
+
+        base = len(self.verts)
+        self._vertex(from_x + px, from_y + py, color)
+        self._vertex(to_x + px, to_y + py, color)
+        self._vertex(from_x - px, from_y - py, color)
+        self._vertex(to_x - px, to_y - py, color)
+        self.indices.extend([base, base + 2, base + 1, base + 1, base + 2, base + 3])
+
+    def add_radial_glow(self, center_x, center_y, width, height, segments, start_angle, end_angle, color):
+        def ellipse_x(cos_angle):
+            return _add(center_x, _mul(width, cos_angle * 0.5))
+
+        def ellipse_y(sin_angle):
+            return _add(center_y, _mul(height, sin_angle * 0.5))
+
+        base = len(self.verts)
+        self._vertex(center_x, center_y, color)
+
+        arc_color = list(color)
+        arc_color[3] = 0.
+        for i in range(segments + 1):
+            t = i / segments
+            angle = start_angle + t * (end_angle - start_angle)
+            self._vertex(ellipse_x(math.cos(angle)), ellipse_y(math.sin(angle)), arc_color)
+
+        for i in range(segments):
+            self.indices.extend([base, base + 1 + i, base + 2 + i])
+
+    def scaled(self, scale):
+        shape = VectorShape()
+        shape.verts = [[_mul(scale, v[0]), _mul(scale, v[1]), list(v[2])] for v in self.verts]
+        shape.indices = list(self.indices)
+        return shape
+
+    def offset(self, off_x, off_y):
+        shape = VectorShape()
+        shape.verts = [[_add(v[0], off_x), _add(v[1], off_y), list(v[2])] for v in self.verts]
+        shape.indices = list(self.indices)
+        return shape
+
+# python -m pydrk.vector_shape
+if __name__ == "__main__":
+    shape = VectorShape()
+    shape.add_filled_box("w/2", 0, "w", 10, [1., 0., 0., 1.])
+    assert len(shape.verts) == 4 and len(shape.indices) == 6
+    assert shape.verts[0][0] == "w/2" and shape.verts[2][1] == "10.0"
+    assert shape.indices == [0, 2, 1, 1, 2, 3]
+
+    shape = VectorShape()
+    shape.add_smooth_vertical_gradient(0, 0, 10, 100, [1., 1., 1., 1.], [0., 0., 0., 0.], 8, 0.45)
+    assert len(shape.verts) == 8 * 4 and len(shape.indices) == 8 * 6
+    assert shape.verts[0][1] == "((1.0 * 0.0) + (0.0 * 100.0))"
+    assert shape.verts[3][1] == "((0.875 * 0.0) + (0.125 * 100.0))"
+
+    shape = VectorShape()
+    shape.add_outline("x1", "y1", "x2", "y2", 2.0, [0., 0., 0., 1.])
+    assert len(shape.verts) == 16 and len(shape.indices) == 24
+    assert shape.verts[1][0] == "(x1 + 2.0)"
+    assert shape.verts[8][0] == "(x2 + -2.0)"
+    assert shape.verts[12][1] == "(y2 + -2.0)"
+
+    shape = VectorShape()
+    shape.add_line(0., 0., 10., 0., 4., [1., 1., 1., 1.])
+    assert len(shape.verts) == 4 and len(shape.indices) == 6
+    assert shape.verts[0][1] == "2.0" and shape.verts[2][1] == "-2.0"
+
+    shape = VectorShape()
+    shape.add_radial_glow("w/2", "h/2", "w", "h", 12, 0., math.pi * 2., [1., 0., 0., 1.])
+    assert len(shape.verts) == 14 and len(shape.indices) == 36
+    assert shape.verts[0][0] == "w/2"
+    assert shape.verts[1][0] == "(w/2 + (w * 0.5))"
+    assert shape.verts[13][2][3] == 0.
+
+    a = VectorShape()
+    a.add_filled_box(0, 0, 1, 1, [1., 1., 1., 1.])
+    b = VectorShape()
+    b.add_filled_box(0, 0, 1, 1, [0., 0., 0., 1.])
+    a.join(b)
+    assert len(a.verts) == 8 and a.indices[6:] == [4, 6, 5, 5, 6, 7]
+
+    s = a.scaled(2.5)
+    assert s.verts[0][0] == "(2.5 * 0.0)"
+    o = a.offset(10., 20.)
+    assert o.verts[4][0] == "(0.0 + 10.0)" and o.verts[5][1] == "(0.0 + 20.0)"
+
+    print("vector_shape self-test OK")

+ 15 - 6
bin/app/src/net.rs

@@ -237,12 +237,21 @@ impl ZeroMQAdapter {
                         let expr = prop.get_expr(i)?;
                         let expr = prop.get_expr(i)?;
                         decompile(&expr).encode(&mut reply).unwrap();
                         decompile(&expr).encode(&mut reply).unwrap();
                     } else if val.is_unset() {
                     } else if val.is_unset() {
-                        1u8.encode(&mut reply).unwrap();
-                        // Shapes are not serialized on the get path;
-                        // the python client shows a "<...>" placeholder.
-                        if prop.typ != PropertyType::VectorShape {
-                            let default = &prop.defaults[i];
-                            default.encode(&mut reply).unwrap();
+                        // A null default encodes zero payload bytes, so it
+                        // is reported as the NULL status instead of UNSET.
+                        // This mirrors the old get_value() semantics, where
+                        // an unset index with a null default resolved to
+                        // null.
+                        let default = &prop.defaults[i];
+                        if default.is_null() {
+                            2u8.encode(&mut reply).unwrap();
+                        } else {
+                            1u8.encode(&mut reply).unwrap();
+                            // Shapes are not serialized on the get path;
+                            // the python client shows a "<...>" placeholder.
+                            if prop.typ != PropertyType::VectorShape {
+                                default.encode(&mut reply).unwrap();
+                            }
                         }
                         }
                     } else if val.is_null() {
                     } else if val.is_null() {
                         2u8.encode(&mut reply).unwrap();
                         2u8.encode(&mut reply).unwrap();