feat: add batch undo support for scene triggers

Co-authored-by: aider (deepseek/deepseek-coder) <aider@aider.chat>
This commit is contained in:
Loic Coenen
2026-05-02 22:16:51 +00:00
parent 885ebb7ac6
commit b83e8e787e
3 changed files with 86 additions and 54 deletions

View File

@@ -116,21 +116,25 @@ AppState dispatcher_get_state(void) {
// Reducer implementation
// ============================================================
static void save_undo_state(AppState *state, int clip_index) {
int undo_idx = state->undo.undo_index % MAX_UNDO_HISTORY;
state->undo.prev_clip_states[undo_idx] = state->clips[clip_index].state;
state->undo.prev_clip_indices[undo_idx] = clip_index;
state->undo.prev_buffer_sizes[undo_idx] = state->clips[clip_index].buffer_size;
state->undo.prev_write_positions[undo_idx] = state->clips[clip_index].write_position;
state->undo.prev_read_positions[undo_idx] = state->clips[clip_index].read_position;
state->undo.batch_sizes[undo_idx] = 0; // Will be set by caller
state->undo.undo_index++;
state->undo.redo_index = state->undo.undo_index;
if (state->undo.count < MAX_UNDO_HISTORY) state->undo.count++;
}
static AppState clip_trigger(AppState state, int clip_index) {
if (clip_index < 0 || clip_index >= MAX_CLIPS) return state;
Clip *clip = &state.clips[clip_index];
// Save undo info
int undo_idx = state.undo.undo_index % MAX_UNDO_HISTORY;
state.undo.prev_clip_states[undo_idx] = clip->state;
state.undo.prev_clip_indices[undo_idx] = clip_index;
state.undo.prev_buffer_sizes[undo_idx] = clip->buffer_size;
state.undo.prev_write_positions[undo_idx] = clip->write_position;
state.undo.prev_read_positions[undo_idx] = clip->read_position;
state.undo.undo_index++;
state.undo.redo_index = state.undo.undo_index;
if (state.undo.count < MAX_UNDO_HISTORY) state.undo.count++;
// Do NOT save undo here - caller will do it
switch (clip->state) {
case CLIP_EMPTY:
@@ -160,6 +164,20 @@ static AppState clip_trigger(AppState state, int clip_index) {
static AppState scene_trigger(AppState state, int scene_index) {
if (scene_index < 0 || scene_index >= MAX_SCENES) return state;
// Save undo info for all clips in the scene as a batch
int batch_start = state.undo.undo_index;
for (int ch = 0; ch < MAX_CHANNELS; ch++) {
int clip_idx = CLIP_INDEX(scene_index, ch);
save_undo_state(&state, clip_idx);
}
// Mark all entries in this batch with the batch size
int batch_end = state.undo.undo_index;
for (int i = batch_start; i < batch_end; i++) {
int idx = i % MAX_UNDO_HISTORY;
state.undo.batch_sizes[idx] = batch_end - batch_start;
}
// Now apply the changes
for (int ch = 0; ch < MAX_CHANNELS; ch++) {
int clip_idx = CLIP_INDEX(scene_index, ch);
state = clip_trigger(state, clip_idx);
@@ -173,16 +191,7 @@ static AppState clip_reset(AppState state, int clip_index) {
Clip *clip = &state.clips[clip_index];
// Save undo info
int undo_idx = state.undo.undo_index % MAX_UNDO_HISTORY;
state.undo.prev_clip_states[undo_idx] = clip->state;
state.undo.prev_clip_indices[undo_idx] = clip_index;
state.undo.prev_buffer_sizes[undo_idx] = clip->buffer_size;
state.undo.prev_write_positions[undo_idx] = clip->write_position;
state.undo.prev_read_positions[undo_idx] = clip->read_position;
state.undo.undo_index++;
state.undo.redo_index = state.undo.undo_index;
if (state.undo.count < MAX_UNDO_HISTORY) state.undo.count++;
save_undo_state(&state, clip_index);
clip->state = CLIP_EMPTY;
clip->buffer_size = 0;
@@ -196,60 +205,79 @@ static AppState clip_reset(AppState state, int clip_index) {
static AppState undo_action(AppState state) {
if (state.undo.undo_index <= 0) return state;
// Get the batch size for the current undo entry
int undo_idx = (state.undo.undo_index - 1) % MAX_UNDO_HISTORY;
int clip_idx = state.undo.prev_clip_indices[undo_idx];
int batch_size = state.undo.batch_sizes[undo_idx];
if (batch_size == 0) batch_size = 1; // Single clip operation
if (clip_idx >= 0 && clip_idx < MAX_CLIPS) {
Clip *clip = &state.clips[clip_idx];
clip->state = state.undo.prev_clip_states[undo_idx];
clip->buffer_size = state.undo.prev_buffer_sizes[undo_idx];
clip->write_position = state.undo.prev_write_positions[undo_idx];
clip->read_position = state.undo.prev_read_positions[undo_idx];
// Undo all clips in the batch
for (int i = 0; i < batch_size; i++) {
int current_idx = (state.undo.undo_index - 1 - i) % MAX_UNDO_HISTORY;
int clip_idx = state.undo.prev_clip_indices[current_idx];
if (clip_idx >= 0 && clip_idx < MAX_CLIPS) {
Clip *clip = &state.clips[clip_idx];
clip->state = state.undo.prev_clip_states[current_idx];
clip->buffer_size = state.undo.prev_buffer_sizes[current_idx];
clip->write_position = state.undo.prev_write_positions[current_idx];
clip->read_position = state.undo.prev_read_positions[current_idx];
}
}
state.undo.undo_index--;
state.undo.undo_index -= batch_size;
return state;
}
static AppState redo_action(AppState state) {
if (state.undo.redo_index <= state.undo.undo_index) return state;
// Get the batch size for the next redo entry
int redo_idx = state.undo.undo_index % MAX_UNDO_HISTORY;
int clip_idx = state.undo.prev_clip_indices[redo_idx];
int batch_size = state.undo.batch_sizes[redo_idx];
if (batch_size == 0) batch_size = 1;
if (clip_idx >= 0 && clip_idx < MAX_CLIPS) {
Clip *clip = &state.clips[clip_idx];
switch (clip->state) {
case CLIP_EMPTY:
clip->state = CLIP_RECORDING;
clip->write_position = 0;
clip->buffer_size = 0;
clip->read_position = 0;
break;
case CLIP_RECORDING:
clip->state = CLIP_LOOPING;
clip->buffer_size = clip->write_position;
clip->read_position = 0;
break;
case CLIP_LOOPING:
clip->state = CLIP_STOPPED;
clip->read_position = 0;
break;
case CLIP_STOPPED:
clip->state = CLIP_LOOPING;
clip->read_position = 0;
break;
// Redo all clips in the batch
for (int i = 0; i < batch_size; i++) {
int current_idx = (state.undo.undo_index + i) % MAX_UNDO_HISTORY;
int clip_idx = state.undo.prev_clip_indices[current_idx];
if (clip_idx >= 0 && clip_idx < MAX_CLIPS) {
Clip *clip = &state.clips[clip_idx];
switch (clip->state) {
case CLIP_EMPTY:
clip->state = CLIP_RECORDING;
clip->write_position = 0;
clip->buffer_size = 0;
clip->read_position = 0;
break;
case CLIP_RECORDING:
clip->state = CLIP_LOOPING;
clip->buffer_size = clip->write_position;
clip->read_position = 0;
break;
case CLIP_LOOPING:
clip->state = CLIP_STOPPED;
clip->read_position = 0;
break;
case CLIP_STOPPED:
clip->state = CLIP_LOOPING;
clip->read_position = 0;
break;
}
}
}
state.undo.undo_index++;
state.undo.undo_index += batch_size;
return state;
}
AppState reducer(AppState state, Action action) {
switch (action.type) {
case ACTION_TRIGGER_CLIP:
return clip_trigger(state, action.data.trigger_clip.clip_index);
case ACTION_TRIGGER_CLIP: {
int clip_idx = action.data.trigger_clip.clip_index;
save_undo_state(&state, clip_idx);
return clip_trigger(state, clip_idx);
}
case ACTION_TRIGGER_SCENE:
return scene_trigger(state, action.data.trigger_scene.scene_index);