feat: tosc sections page for song build
CI build oscope-of / build-check (push) Has been cancelled

Add SECTIONS (page 4): 8 slot launch+save buttons, PREV/NEXT nav,
/scene/state receive on the GROUP so sections.lua colors slots
(green=current, blue=filled, dark=empty). Update pager to 4 pages,
add 4 new tests (26 pass), rebuild dist/av-live-control.tosc.
This commit is contained in:
L'électron rare
2026-06-28 16:16:14 +02:00
parent 7b90acf80e
commit 495c5680bf
4 changed files with 151 additions and 3 deletions
Binary file not shown.
+57 -1
View File
@@ -317,6 +317,62 @@ def seq_page():
return page return page
# ---------------------------------------------------------------------------
# Page 4 — SECTIONS
# ---------------------------------------------------------------------------
def sections_page():
"""Page 4: 8 section slots (launch + save), prev/next nav, SC scene feedback.
The GROUP carries sections.lua and a /scene/state receive message so that
onReceiveOSC fires on the GROUP and can color the slot buttons.
"""
page = s.node(
"GROUP",
[
s.prop("name", "SECTIONS", "s"),
s.frame(0, 0, *CANVAS),
s.script(_lua("sections.lua")),
],
messages=[
s.osc("/scene/state", [s.val()], send=0, receive=1, feedback=1),
],
)
ch = page.find("children")
ch.append(label("SECTIONS", 10, 8, 200, 30))
gx = 10
bw = 138
gap = 10
step = bw + gap # 148 px per column
launch_y, launch_h = 46, 140
save_y, save_h = 196, 60
for i in range(8):
x = gx + i * step
# Launch button: name=slot{i} so sections.lua can findByName("slot"..i)
ch.append(s.node(
"BUTTON",
[s.prop("name", f"slot{i}", "s"), s.frame(x, launch_y, bw, launch_h)],
messages=[s.osc("/scene/launch", [s.consti(i)])],
))
# Save button
ch.append(s.node(
"BUTTON",
[s.prop("name", f"S{i + 1}", "s"), s.frame(x, save_y, bw, save_h)],
messages=[s.osc("/scene/save", [s.consti(i)])],
))
# Nav buttons
nav_y, nav_h = 272, 80
ch.append(button("< PREV", "/scene/prev", 10, nav_y, 200, nav_h, args=[s.vali()]))
ch.append(button("NEXT >", "/scene/next", 220, nav_y, 200, nav_h, args=[s.vali()]))
return page
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Pager + root # Pager + root
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -335,6 +391,6 @@ def build():
root = s.node( root = s.node(
"GROUP", "GROUP",
[s.prop("name", "av-live-control", "s"), s.frame(0, 0, *CANVAS)], [s.prop("name", "av-live-control", "s"), s.frame(0, 0, *CANVAS)],
children=[pager([live_page(), fx_page(), seq_page()])], children=[pager([live_page(), fx_page(), seq_page(), sections_page()])],
) )
return root return root
+23
View File
@@ -0,0 +1,23 @@
-- SECTIONS GROUP: color the 8 launch slot buttons from /scene/state.
-- API verified on GrosMac at fidelity gate.
-- /scene/state <cur:int> <fill0:int> ... <fill7:int>
-- cur=-1 means no active section; fillN=1 means slot N is saved.
function onReceiveOSC(message, connections)
local path = message[1]
if path ~= "/scene/state" then return end
local args = message[2]
local cur = math.floor((args[1] and tonumber(args[1].value)) or -1)
for i = 0, 7 do
local fill = math.floor((args[i + 2] and tonumber(args[i + 2].value)) or 0)
local slot = self:findByName("slot" .. i, true)
if slot then
if i == cur then
slot.color = Color(0.27, 0.8, 0.4, 1) -- current (green)
elseif fill == 1 then
slot.color = Color(0.2, 0.4, 0.8, 1) -- filled (blue)
else
slot.color = Color(0.15, 0.15, 0.15, 1) -- empty (dark)
end
end
end
end
+71 -2
View File
@@ -23,7 +23,7 @@ def _addr_list(el):
# Root / pager # Root / pager
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def test_root_has_pager_with_three_pages(tmp_path): def test_root_has_pager_with_four_pages(tmp_path):
root = layout.build() root = layout.build()
out = tmp_path / "full.tosc" out = tmp_path / "full.tosc"
schema.write_tosc(root, str(out)) schema.write_tosc(root, str(out))
@@ -31,7 +31,7 @@ def test_root_has_pager_with_three_pages(tmp_path):
pager = parsed.find(".//node[@type='PAGER']") pager = parsed.find(".//node[@type='PAGER']")
assert pager is not None, "PAGER node not found" assert pager is not None, "PAGER node not found"
pages = pager.findall("children/node[@type='GROUP']") pages = pager.findall("children/node[@type='GROUP']")
assert len(pages) == 3, f"expected 3 pages, got {len(pages)}" assert len(pages) == 4, f"expected 4 pages, got {len(pages)}"
def test_sixteen_patterns(): def test_sixteen_patterns():
@@ -322,3 +322,72 @@ def test_live_page_pad_groups_carry_armed_receive():
f"pad GROUP '{grp_name}' must have exactly 1 /armed/* receive msg " f"pad GROUP '{grp_name}' must have exactly 1 /armed/* receive msg "
f"on the GROUP itself, got {len(armed_msgs)}" f"on the GROUP itself, got {len(armed_msgs)}"
) )
# ---------------------------------------------------------------------------
# Page 4 — SECTIONS
# ---------------------------------------------------------------------------
def test_sections_page_launch_buttons():
"""SECTIONS page: 8 /scene/launch messages with CONSTANT/INTEGER args 0..7."""
page = layout.sections_page()
launch_args = []
for osc_el in page.findall(".//messages/osc"):
path_val = osc_el.find("path/partial/value")
if path_val is not None and path_val.text == "/scene/launch":
for part in osc_el.findall("arguments/partial"):
typ = part.find("type")
conv = part.find("conversion")
val = part.find("value")
if typ is not None and conv is not None and val is not None:
launch_args.append((typ.text, conv.text, val.text))
assert len(launch_args) == 8, f"expected 8 /scene/launch arg partials, got {len(launch_args)}"
for typ, conv, _ in launch_args:
assert typ == "CONSTANT", f"launch arg type must be CONSTANT, got {typ}"
assert conv == "INTEGER", f"launch arg conversion must be INTEGER, got {conv}"
vals = {v for _, _, v in launch_args}
assert vals == {"0", "1", "2", "3", "4", "5", "6", "7"}, (
f"expected launch values 0..7, got {vals}"
)
def test_sections_page_save_buttons():
"""SECTIONS page: 8 /scene/save messages with CONSTANT/INTEGER args 0..7."""
page = layout.sections_page()
save_args = []
for osc_el in page.findall(".//messages/osc"):
path_val = osc_el.find("path/partial/value")
if path_val is not None and path_val.text == "/scene/save":
for part in osc_el.findall("arguments/partial"):
typ = part.find("type")
conv = part.find("conversion")
val = part.find("value")
if typ is not None and conv is not None and val is not None:
save_args.append((typ.text, conv.text, val.text))
assert len(save_args) == 8, f"expected 8 /scene/save arg partials, got {len(save_args)}"
for typ, conv, _ in save_args:
assert typ == "CONSTANT", f"save arg type must be CONSTANT, got {typ}"
assert conv == "INTEGER", f"save arg conversion must be INTEGER, got {conv}"
def test_sections_page_nav_buttons():
"""SECTIONS page: /scene/prev and /scene/next present."""
page = layout.sections_page()
addrs = _addrs(page)
assert "/scene/prev" in addrs, "/scene/prev not found on SECTIONS page"
assert "/scene/next" in addrs, "/scene/next not found on SECTIONS page"
def test_sections_page_state_receive_on_group():
"""SECTIONS GROUP itself must carry /scene/state receive so sections.lua fires."""
page = layout.sections_page()
# Direct messages/osc on the top-level GROUP (not descendants)
state_msgs = [
osc_el for osc_el in page.findall("messages/osc")
if osc_el.get("receive") == "1"
and osc_el.find("path/partial/value") is not None
and osc_el.find("path/partial/value").text == "/scene/state"
]
assert len(state_msgs) == 1, (
f"SECTIONS GROUP must have exactly 1 /scene/state receive msg, got {len(state_msgs)}"
)