fix(sequences): target only checked lane groups

Use zone group checkboxes in the editor; empty lane groups no longer
fall back to the whole zone. Remove cross-lane device splitting.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-19 00:23:21 +12:00
parent baec87068a
commit 7a7bedc07c
2 changed files with 89 additions and 65 deletions

View File

@@ -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 =
'<span class="muted-text">This zone is for presets only. Sequences are hidden.</span>';
@@ -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();