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 && (