diff --git a/src/App.jsx b/src/App.jsx index 0db913f..f32a33b 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -191,6 +191,26 @@ export default function App({ onSwitchToGame }) { emit(); }, []); + // Center view on all modules + const handleCenterView = useCallback(() => { + if (state.modules.length === 0) { state.camX = 0; state.camY = 0; emit(); return; } + const container = containerRef.current; + const cw = container?.clientWidth || 800; + const ch = container?.clientHeight || 600; + let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; + for (const m of state.modules) { + minX = Math.min(minX, m.x); + minY = Math.min(minY, m.y); + maxX = Math.max(maxX, m.x + 200); + maxY = Math.max(maxY, m.y + 150); + } + const cx = (minX + maxX) / 2 * state.zoom; + const cy = (minY + maxY) / 2 * state.zoom; + state.camX = cw / 2 - cx; + state.camY = ch / 2 - cy; + emit(); + }, []); + const handleToggleAudio = async () => { if (state.isRunning) { stopAudio(); @@ -305,6 +325,7 @@ export default function App({ onSwitchToGame }) { {(state.zoom * 100).toFixed(0)}% + {/* Module palette */} diff --git a/src/components/SequencerWidget.jsx b/src/components/SequencerWidget.jsx index 897d087..437a5b8 100644 --- a/src/components/SequencerWidget.jsx +++ b/src/components/SequencerWidget.jsx @@ -22,13 +22,21 @@ export default function SequencerWidget({ moduleId }) { const clockRef = useRef(null); const stepsRef = useRef(null); - // Init steps data + // Init steps data — also grow/shrink when numSteps changes const numSteps = parseInt(mod?.params?.steps || '16'); - if (!mod?.params?._steps) { - const initial = DEFAULT_STEPS.slice(0, numSteps); - while (initial.length < numSteps) initial.push({ midi: 60, gate: false }); - if (mod) { + if (mod) { + if (!mod.params._steps) { + const initial = DEFAULT_STEPS.slice(0, numSteps); + while (initial.length < numSteps) initial.push({ midi: 60, gate: false }); mod.params._steps = initial; + } else if (mod.params._steps.length < numSteps) { + // Grow: pad with empty steps + while (mod.params._steps.length < numSteps) { + mod.params._steps.push({ midi: 60, gate: false }); + } + } else if (mod.params._steps.length > numSteps) { + // Shrink: truncate + mod.params._steps = mod.params._steps.slice(0, numSteps); } } const steps = mod?.params?._steps || DEFAULT_STEPS; diff --git a/src/game/PuzzleView.jsx b/src/game/PuzzleView.jsx index 612bcd1..32ce0d4 100644 --- a/src/game/PuzzleView.jsx +++ b/src/game/PuzzleView.jsx @@ -222,6 +222,26 @@ export default function PuzzleView({ level, levelIndex, worldLevels, onBack, onN emit(); }, []); + // Center view on all modules + const handleCenterView = useCallback(() => { + if (state.modules.length === 0) { state.camX = 0; state.camY = 0; emit(); return; } + const container = containerRef.current; + const cw = container?.clientWidth || 800; + const ch = container?.clientHeight || 600; + let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; + for (const m of state.modules) { + minX = Math.min(minX, m.x); + minY = Math.min(minY, m.y); + maxX = Math.max(maxX, m.x + 200); + maxY = Math.max(maxY, m.y + 150); + } + const cx = (minX + maxX) / 2 * state.zoom; + const cy = (minY + maxY) / 2 * state.zoom; + state.camX = cw / 2 - cx; + state.camY = ch / 2 - cy; + emit(); + }, []); + const handleAddModule = (type) => { const x = (-state.camX + 250) / state.zoom + Math.random() * 30; const y = (-state.camY + 150) / state.zoom + Math.random() * 30; @@ -471,6 +491,7 @@ export default function PuzzleView({ level, levelIndex, worldLevels, onBack, onN {(state.zoom * 100).toFixed(0)}% + {state.modules.length > 0 && state.connections.length === 0 && (