diff --git a/src/static/sequences.js b/src/static/sequences.js index ab44a29..d5f7a4f 100644 --- a/src/static/sequences.js +++ b/src/static/sequences.js @@ -216,6 +216,12 @@ function logSequenceSelectionPresets(sequenceId, sequenceDoc, presetsMap) { }); } +function zoneGroupIdsFromDoc(zoneDoc) { + return Array.isArray(zoneDoc && zoneDoc.group_ids) + ? zoneDoc.group_ids.map((x) => String(x).trim()).filter(Boolean) + : []; +} + function groupIdsForLaneStep(sequenceDoc, step, laneIndex, numLanes) { const lgs = Array.isArray(sequenceDoc.lanes_group_ids) ? sequenceDoc.lanes_group_ids : []; if (laneIndex < lgs.length) { @@ -239,7 +245,6 @@ function groupIdsForLaneStep(sequenceDoc, step, laneIndex, numLanes) { function buildLaneGroupIdsForEditor(doc, laneIndex, numLanes) { const raw = Array.isArray(doc && doc.lanes_group_ids) ? doc.lanes_group_ids : []; - const shared = Array.isArray(doc && doc.group_ids) ? doc.group_ids.map(String) : []; if (laneIndex < raw.length) { const row = raw[laneIndex]; if (Array.isArray(row)) { @@ -249,17 +254,10 @@ function buildLaneGroupIdsForEditor(doc, laneIndex, numLanes) { if (numLanes > 1 && laneIndex >= raw.length) { return []; } - if (numLanes === 1) { - const lanes = normalizeSequenceLanes(doc); - const first = lanes[0] && lanes[0][0]; - const sg = - first && Array.isArray(first.group_ids) ? first.group_ids.map(String).filter(Boolean) : []; - return sg.length ? sg : shared.slice(); - } - return shared.slice(); + return []; } -function renderLaneGroupCheckboxes(groupsMap, selectedIds) { +function renderLaneGroupCheckboxes(groupsMap, selectedIds, zoneGroupIds) { const wrap = document.createElement('div'); wrap.className = 'sequence-lane-groups-wrap'; wrap.style.cssText = 'margin-bottom:0.6rem;'; @@ -267,15 +265,17 @@ function renderLaneGroupCheckboxes(groupsMap, selectedIds) { hint.className = 'muted-text'; hint.style.fontSize = '0.85em'; hint.style.marginBottom = '0.35rem'; - hint.textContent = 'Groups for this lane (none = whole zone)'; + hint.textContent = 'Only checked groups are used on this lane'; wrap.appendChild(hint); const row = document.createElement('div'); row.className = 'sequence-lane-groups'; row.style.cssText = 'display:flex;flex-wrap:wrap;gap:0.5rem;align-items:center;'; const sel = new Set((selectedIds || []).map((x) => String(x))); - Object.keys(groupsMap) - .sort((a, b) => a.localeCompare(b)) - .forEach((gid) => { + const zg = Array.isArray(zoneGroupIds) ? zoneGroupIds.map(String).filter(Boolean) : []; + const gidsToShow = zg.length + ? zg + : Object.keys(groupsMap).sort((a, b) => a.localeCompare(b)); + gidsToShow.forEach((gid) => { const g = groupsMap[gid]; const gn = g && g.name ? String(g.name) : gid; const id = `seq-lg-${gid}-${Math.random().toString(36).slice(2)}`; @@ -333,13 +333,6 @@ function presetsSectionElForZone(zoneId) { /** Match preset tiles: prefer DOM device list, then zone JSON (same as parseTabDeviceNames + computeZoneTargets). */ async function resolveSequenceSendDeviceNames(zoneId, zoneDoc, groupIds) { const gids = Array.isArray(groupIds) ? groupIds.map((x) => String(x).trim()).filter((x) => x.length > 0) : []; - if (!gids.length) { - const section = presetsSectionElForZone(zoneId); - if (typeof window.parseTabDeviceNames === 'function' && section) { - const fromDom = window.parseTabDeviceNames(section); - if (Array.isArray(fromDom) && fromDom.length) return fromDom; - } - } if (window.zonesManager && typeof window.zonesManager.resolveSequenceStepDeviceNames === 'function' && zoneDoc) { return await window.zonesManager.resolveSequenceStepDeviceNames(zoneDoc, gids); } @@ -407,33 +400,12 @@ function createSequenceTileRow(sequenceId, sequenceDoc, zoneId, zoneDoc, allPres button.className = 'pattern-button preset-tile-main sequence-tile-main'; button.title = sequenceDoc.name || `Sequence ${sequenceId}`; - const badge = document.createElement('span'); - badge.textContent = 'SEQ'; - badge.className = 'sequence-tile-badge'; - badge.style.cssText = - 'position:absolute;left:4px;top:4px;font-size:10px;font-weight:700;color:#fff;background:rgba(0,100,180,0.9);padding:2px 5px;border-radius:3px;pointer-events:none;z-index:2;'; - button.style.position = 'relative'; - button.appendChild(badge); - const label = document.createElement('span'); label.textContent = sequenceDoc.name || sequenceId; label.style.fontWeight = 'bold'; label.className = 'pattern-button-label'; button.appendChild(label); - const sub = document.createElement('span'); - sub.className = 'muted-text'; - sub.style.cssText = 'display:block;font-size:0.8em;margin-top:0.2rem;'; - const lanes = normalizeSequenceLanes(sequenceDoc); - const nLanes = lanes.filter((l) => l.length > 0).length || 1; - const nSteps = lanes.reduce((a, l) => a + l.length, 0); - const simRaw = sequenceDoc.simulated_bpm; - let sim = parseInt(String(simRaw != null ? simRaw : 120), 10); - if (!Number.isFinite(sim)) sim = 120; - sim = Math.min(300, Math.max(30, sim)); - sub.textContent = `${nLanes} lane${nLanes === 1 ? '' : 's'} · ${nSteps} step${nSteps === 1 ? '' : 's'} · beats · ${sim} BPM sim`; - button.appendChild(sub); - button.addEventListener('click', () => { const strip = document.getElementById('presets-list-zone'); const clearActiveStrip = () => { @@ -540,7 +512,7 @@ async function addSequenceToTab(sequenceId, zoneId) { const tabData = await tabResponse.json(); if ( typeof window.zoneAllowsSequences === 'function' && - !window.zoneAllowsSequences(tabData) + !window.zoneAllowsSequences(tabData, zoneId) ) { alert('This zone is for presets only. Add presets from the zone Edit menu instead.'); return; @@ -609,7 +581,7 @@ async function refreshEditTabSequencesUi(zoneId) { const zone = await zoneRes.json(); if ( typeof window.zoneAllowsSequences === 'function' && - !window.zoneAllowsSequences(zone) + !window.zoneAllowsSequences(zone, zoneId) ) { currentEl.innerHTML = 'This zone is for presets only. Sequences are hidden.'; @@ -865,7 +837,7 @@ function renderSequenceStepRow(presetsMap, step) { return row; } -function renderSequenceLane(laneIndex, laneSteps, laneGroupIds, presetsMap, groupsMap) { +function renderSequenceLane(laneIndex, laneSteps, laneGroupIds, presetsMap, groupsMap, zoneGroupIds) { const wrap = document.createElement('div'); wrap.className = 'sequence-lane'; wrap.dataset.laneIndex = String(laneIndex); @@ -904,7 +876,7 @@ function renderSequenceLane(laneIndex, laneSteps, laneGroupIds, presetsMap, grou head.appendChild(headBtns); wrap.appendChild(head); - wrap.appendChild(renderLaneGroupCheckboxes(groupsMap, laneGroupIds)); + wrap.appendChild(renderLaneGroupCheckboxes(groupsMap, laneGroupIds, zoneGroupIds)); const stepsHost = document.createElement('div'); stepsHost.className = 'sequence-lane-steps'; @@ -977,6 +949,24 @@ async function openSequenceEditor(sequenceId, existing) { const presetsMap = presetsRes.ok ? await presetsRes.json() : {}; const groupsMap = await fetchGroupsMapSeq(); + let zoneDoc = {}; + const zoneIdForEditor = resolveZoneIdForPresetStripRefresh(); + if (zoneIdForEditor) { + try { + const zr = await fetch(`/zones/${encodeURIComponent(zoneIdForEditor)}`, { + headers: { Accept: 'application/json' }, + credentials: 'same-origin', + }); + if (zr.ok) { + const zj = await zr.json(); + if (zj && typeof zj === 'object' && !zj.error) zoneDoc = zj; + } + } catch (_) { + /* no zone context */ + } + } + const zoneGroupIds = zoneGroupIdsFromDoc(zoneDoc); + let doc = existing; if (sequenceEditorId) { try { @@ -1010,11 +1000,11 @@ async function openSequenceEditor(sequenceId, existing) { lanesHost.innerHTML = ''; if (!lanes.some((l) => l.length > 0)) { const lg0 = buildLaneGroupIdsForEditor(doc, 0, 1); - lanesHost.appendChild(renderSequenceLane(0, [], lg0, presetsMap, groupsMap)); + lanesHost.appendChild(renderSequenceLane(0, [], lg0, presetsMap, groupsMap, zoneGroupIds)); } else { lanes.forEach((laneSteps, i) => { const lg = buildLaneGroupIdsForEditor(doc, i, lanes.length); - lanesHost.appendChild(renderSequenceLane(i, laneSteps, lg, presetsMap, groupsMap)); + lanesHost.appendChild(renderSequenceLane(i, laneSteps, lg, presetsMap, groupsMap, zoneGroupIds)); }); } refreshSequenceEditorLaneTitles(); diff --git a/src/util/sequence_playback.py b/src/util/sequence_playback.py index 237d908..77b46e1 100644 --- a/src/util/sequence_playback.py +++ b/src/util/sequence_playback.py @@ -78,8 +78,13 @@ def _normalize_sequence_lanes(doc: Dict[str, Any]) -> List[List[Dict[str, Any]]] def _group_ids_for_lane_step( - sequence_doc: Dict[str, Any], step: Dict[str, Any], lane_index: int, num_lanes: int + sequence_doc: Dict[str, Any], + step: Dict[str, Any], + lane_index: int, + num_lanes: int, + zone_doc: Optional[Dict[str, Any]] = None, ) -> List[str]: + _ = zone_doc lgs = sequence_doc.get("lanes_group_ids") if isinstance(lgs, list) and lane_index < len(lgs): for_lane = lgs[lane_index] @@ -234,7 +239,7 @@ def _resolve_step_device_names( ) -> List[str]: z_names, z_macs = _compute_zone_targets(zone_doc, devices, groups) if not step_group_ids: - return list(z_names) + return [] zone_mac_set = {m for m in (_norm_mac(x) for x in z_macs) if m} zone_name_by_mac: Dict[str, str] = {} for i, m in enumerate(z_macs): @@ -273,12 +278,29 @@ def _lane_has_non_empty_lanes_group_ids(sequence_doc: Dict[str, Any], lane_index return any(x is not None and str(x).strip() for x in for_lane) +def _partition_devices_for_lane( + num_lanes: int, + *, + lane_has_own_groups: bool, + step_group_ids: List[str], +) -> bool: + """Split zone devices across lanes only when lanes lack explicit group targeting.""" + if num_lanes <= 1: + return False + if lane_has_own_groups: + return False + # No lane groups (whole zone): every lane uses all zone / zone-group devices. + if not step_group_ids: + return False + return False + + def _split_device_names_for_lane( all_names: List[str], lane_index: int, num_lanes: int, *, - partition_shared_zone: bool = True, + partition_shared_zone: bool = False, ) -> List[str]: names = [n for n in all_names if n and str(n).strip()] if num_lanes <= 1 or not partition_shared_zone: @@ -368,15 +390,20 @@ def _resolve_lane_device_names(lane_index: int, ctx: Dict[str, Any]) -> List[str lane = lanes[lane_index] if 0 <= lane_index < len(lanes) else [] if not lane: return [] - gids = _group_ids_for_lane_step(sequence_doc, lane[0], lane_index, num_lanes) + gids = _group_ids_for_lane_step( + sequence_doc, lane[0], lane_index, num_lanes, zone_doc=zone_doc + ) device_names = _resolve_step_device_names( zone_doc, gids, devices, groups, sequence_doc=sequence_doc ) + lane_own = _lane_has_non_empty_lanes_group_ids(sequence_doc, lane_index) return _split_device_names_for_lane( device_names, lane_index, num_lanes, - partition_shared_zone=not _lane_has_non_empty_lanes_group_ids(sequence_doc, lane_index), + partition_shared_zone=_partition_devices_for_lane( + num_lanes, lane_has_own_groups=lane_own, step_group_ids=gids + ), ) @@ -545,16 +572,19 @@ def _union_macs_for_sequence(ctx: Dict[str, Any]) -> List[str]: for step in lane: if not isinstance(step, dict): continue - gids = _group_ids_for_lane_step(sequence_doc, step, lane_index, num_lanes) + gids = _group_ids_for_lane_step( + sequence_doc, step, lane_index, num_lanes, zone_doc=zone_doc + ) device_names = _resolve_step_device_names( zone_doc, gids, devices, groups, sequence_doc=sequence_doc ) + lane_own = _lane_has_non_empty_lanes_group_ids(sequence_doc, lane_index) device_names = _split_device_names_for_lane( device_names, lane_index, num_lanes, - partition_shared_zone=not _lane_has_non_empty_lanes_group_ids( - sequence_doc, lane_index + partition_shared_zone=_partition_devices_for_lane( + num_lanes, lane_has_own_groups=lane_own, step_group_ids=gids ), ) if gids and not device_names: @@ -563,10 +593,7 @@ def _union_macs_for_sequence(ctx: Dict[str, Any]) -> List[str]: if m and m not in seen: seen.add(m) out.append(m) - if out: - return out - _, z_macs = _compute_zone_targets(zone_doc, devices, groups) - return list(z_macs) + return out def _coerce_loop(sequence_doc: Dict[str, Any]) -> bool: @@ -641,15 +668,22 @@ async def _send_lane( display_preset = _display_preset_for_step(preset_id, presets_map, palette_colors) if not display_preset: return - gids = _group_ids_for_lane_step(sequence_doc, step, lane_index, int(ctx["num_lanes"])) - device_names = _resolve_step_device_names( - ctx["zone_doc"], gids, devices, ctx["groups"], sequence_doc=sequence_doc + num_lanes = int(ctx["num_lanes"]) + zone_doc = ctx["zone_doc"] + gids = _group_ids_for_lane_step( + sequence_doc, step, lane_index, num_lanes, zone_doc=zone_doc ) + device_names = _resolve_step_device_names( + zone_doc, gids, devices, ctx["groups"], sequence_doc=sequence_doc + ) + lane_own = _lane_has_non_empty_lanes_group_ids(sequence_doc, lane_index) device_names = _split_device_names_for_lane( device_names, lane_index, - int(ctx["num_lanes"]), - partition_shared_zone=not _lane_has_non_empty_lanes_group_ids(sequence_doc, lane_index), + num_lanes, + partition_shared_zone=_partition_devices_for_lane( + num_lanes, lane_has_own_groups=lane_own, step_group_ids=gids + ), ) if gids and not device_names: return