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:
Vendored
BIN
Binary file not shown.
+57
-1
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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)}"
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user