feat: align clip selections and slip sources without moving boundaries

This commit is contained in:
Johannes Findeisen 2026-11-06 00:16:31 +01:00
commit b0dbf6905f
7 changed files with 44 additions and 7 deletions

View file

@ -19,3 +19,7 @@ Ctrl+C/Ctrl+V in the sequencer, or Edit/Copy clips and Paste clips at cursor, co
## Phase 50 ## Phase 50
Edit/Repeat selected phrase asks for 1..1024 additional copies. The bounding span includes gaps and overlaps; copies remain on their original tracks. The entire repeat is one undo step. Clip/note capacity and timeline endpoints can limit the requested count. Edit/Repeat selected phrase asks for 1..1024 additional copies. The bounding span includes gaps and overlaps; copies remain on their original tracks. The entire repeat is one undo step. Clip/note capacity and timeline endpoints can limit the requested count.
## Phase 51
Edit/Align clip starts to cursor places every selected start at the cursor. Slip sources earlier/later changes source offsets by the snap step while retaining timeline positions and durations. Trimming must leave source material available; an invalid slip rejects the whole selection. No time stretching or destructive media edits are performed.

View file

@ -1378,12 +1378,12 @@ Implemented: bounded phrase repeat uses the complete selected bounding span, inc
Align selected clip starts to the cursor and slip source offsets without moving timeline boundaries. Align selected clip starts to the cursor and slip source offsets without moving timeline boundaries.
### Tasks and acceptance criteria ### Tasks and acceptance criteria
- [ ] Provide the control/model behavior and accessible GUI commands. - [x] Provide the control/model behavior and accessible GUI commands.
- [ ] Validate bounds and failure rollback; preserve existing project compatibility and callback safety. - [x] Validate bounds and failure rollback; preserve existing project compatibility and callback safety.
- [ ] Test the new workflow and document scope and limitations. - [x] Test the new workflow and document scope and limitations.
### Implementation status ### Implementation status
Planned. Implemented: selection-wide start alignment and source slip commands reuse atomic clip transactions. Clip start/length and source limits remain validated for audio and MIDI. Core late-failure rollback and focused GUI alignment/history tests pass.
--- ---

View file

@ -9,8 +9,10 @@ extern "C" {
* ID counters and optional count-element output. No media loading or DSP work. * ID counters and optional count-element output. No media loading or DSP work.
* MOVE/DUPLICATE amount is a signed tick displacement, bounded by MAX_TICK. * MOVE/DUPLICATE amount is a signed tick displacement, bounded by MAX_TICK.
* DUPLICATE retains tracks, source ranges, notes and media with fresh IDs. * DUPLICATE retains tracks, source ranges, notes and media with fresh IDs.
* ALIGN amount is an absolute timeline start; SLIP amount is a signed source
* offset change (audio or MIDI), retaining timeline start/length.
* out_ids receives resulting IDs (removed IDs for DELETE). May be NULL. */ * out_ids receives resulting IDs (removed IDs for DELETE). May be NULL. */
typedef enum { XTC_CLIPS_MOVE, XTC_CLIPS_DUPLICATE, XTC_CLIPS_DELETE } xtc_clip_operation; typedef enum { XTC_CLIPS_MOVE, XTC_CLIPS_DUPLICATE, XTC_CLIPS_DELETE, XTC_CLIPS_ALIGN, XTC_CLIPS_SLIP } xtc_clip_operation;
xtc_result xtc_project_transform_clips(xtc_project *project,const uint64_t *ids,size_t count, xtc_result xtc_project_transform_clips(xtc_project *project,const uint64_t *ids,size_t count,
xtc_clip_operation operation,int64_t amount,uint64_t *out_ids); xtc_clip_operation operation,int64_t amount,uint64_t *out_ids);
/* Repeat the bounding phrase span, including gaps, 1..1024 additional copies. /* Repeat the bounding phrase span, including gaps, 1..1024 additional copies.

View file

@ -322,6 +322,9 @@ MainWindow::MainWindow(bool prompt_unsaved,const char *configuration)
Fl_Group::current(parent); Fl_Group::current(parent);
menu->add("Edit/Nudge selection left",0,[](Fl_Widget *,void *c){static_cast<SequencerWidget *>(c)->nudge_selection(-1);},sequencer_); menu->add("Edit/Nudge selection left",0,[](Fl_Widget *,void *c){static_cast<SequencerWidget *>(c)->nudge_selection(-1);},sequencer_);
menu->add("Edit/Nudge selection right",0,[](Fl_Widget *,void *c){static_cast<SequencerWidget *>(c)->nudge_selection(1);},sequencer_); menu->add("Edit/Nudge selection right",0,[](Fl_Widget *,void *c){static_cast<SequencerWidget *>(c)->nudge_selection(1);},sequencer_);
menu->add("Edit/Align clip starts to cursor",0,[](Fl_Widget *,void *c){auto *s=static_cast<SequencerWidget *>(c);s->transform_selection(XTC_CLIPS_ALIGN,static_cast<int64_t>(s->position()));},sequencer_);
menu->add("Edit/Slip sources earlier",0,[](Fl_Widget *,void *c){auto *s=static_cast<SequencerWidget *>(c);s->transform_selection(XTC_CLIPS_SLIP,-static_cast<int64_t>(s->grid()));},sequencer_);
menu->add("Edit/Slip sources later",0,[](Fl_Widget *,void *c){auto *s=static_cast<SequencerWidget *>(c);s->transform_selection(XTC_CLIPS_SLIP,static_cast<int64_t>(s->grid()));},sequencer_);
menu->add("Edit/Repeat selected phrase...",0,[](Fl_Widget *,void *c){static_cast<SequencerWidget *>(c)->repeat_dialog();},sequencer_); menu->add("Edit/Repeat selected phrase...",0,[](Fl_Widget *,void *c){static_cast<SequencerWidget *>(c)->repeat_dialog();},sequencer_);
menu->add("Edit/Copy clips",0,[](Fl_Widget *,void *c){static_cast<SequencerWidget *>(c)->copy_selection();},sequencer_); menu->add("Edit/Copy clips",0,[](Fl_Widget *,void *c){static_cast<SequencerWidget *>(c)->copy_selection();},sequencer_);
menu->add("Edit/Paste clips at cursor",0,[](Fl_Widget *,void *c){static_cast<SequencerWidget *>(c)->paste_selection();},sequencer_); menu->add("Edit/Paste clips at cursor",0,[](Fl_Widget *,void *c){static_cast<SequencerWidget *>(c)->paste_selection();},sequencer_);

View file

@ -23,7 +23,7 @@ xtc_result xtc_project_transform_clips(xtc_project *p,const uint64_t *ids,size_t
xtc_project *copy=NULL;size_t i;uint64_t output[XTC_PROJECT_MAX_CLIPS]; xtc_project *copy=NULL;size_t i;uint64_t output[XTC_PROJECT_MAX_CLIPS];
xtc_result r=selection_valid(p,ids,count); xtc_result r=selection_valid(p,ids,count);
if(r!=XTC_OK)return r; if(r!=XTC_OK)return r;
if(operation<XTC_CLIPS_MOVE || operation>XTC_CLIPS_DELETE || if(operation<XTC_CLIPS_MOVE || operation>XTC_CLIPS_SLIP ||
amount<-(int64_t)XTC_PROJECT_MAX_TICK || amount>(int64_t)XTC_PROJECT_MAX_TICK)return XTC_ERR_INVALID_ARGUMENT; amount<-(int64_t)XTC_PROJECT_MAX_TICK || amount>(int64_t)XTC_PROJECT_MAX_TICK)return XTC_ERR_INVALID_ARGUMENT;
r=xtc_project_clone(p,&copy); r=xtc_project_clone(p,&copy);
for(i=0;r==XTC_OK && i<count;++i) { for(i=0;r==XTC_OK && i<count;++i) {
@ -31,6 +31,12 @@ xtc_result xtc_project_transform_clips(xtc_project *p,const uint64_t *ids,size_t
r=xtc_project_find_clip(copy,ids[i],&c);if(r!=XTC_OK)break; r=xtc_project_find_clip(copy,ids[i],&c);if(r!=XTC_OK)break;
start=(int64_t)c.start_tick+amount;output[i]=ids[i]; start=(int64_t)c.start_tick+amount;output[i]=ids[i];
if(operation==XTC_CLIPS_DELETE)r=xtc_project_remove_clip(copy,ids[i]); if(operation==XTC_CLIPS_DELETE)r=xtc_project_remove_clip(copy,ids[i]);
else if(operation==XTC_CLIPS_ALIGN) {
r=amount<0 ? XTC_ERR_INVALID_ARGUMENT : xtc_project_edit_clip(copy,ids[i],c.track_id,(uint64_t)amount,c.length_tick,c.offset_tick);
} else if(operation==XTC_CLIPS_SLIP) {
int64_t offset=(int64_t)c.offset_tick+amount;
r=offset<0 ? XTC_ERR_INVALID_ARGUMENT : xtc_project_edit_clip(copy,ids[i],c.track_id,c.start_tick,c.length_tick,(uint64_t)offset);
}
else if(start<0 || start>(int64_t)XTC_PROJECT_MAX_TICK)r=XTC_ERR_INVALID_ARGUMENT; else if(start<0 || start>(int64_t)XTC_PROJECT_MAX_TICK)r=XTC_ERR_INVALID_ARGUMENT;
else if(operation==XTC_CLIPS_DUPLICATE)r=xtc_project_duplicate_clip(copy,ids[i],c.track_id,(uint64_t)start,output+i); else if(operation==XTC_CLIPS_DUPLICATE)r=xtc_project_duplicate_clip(copy,ids[i],c.track_id,(uint64_t)start,output+i);
else r=xtc_project_edit_clip(copy,ids[i],c.track_id,(uint64_t)start,c.length_tick,c.offset_tick); else r=xtc_project_edit_clip(copy,ids[i],c.track_id,(uint64_t)start,c.length_tick,c.offset_tick);

View file

@ -1326,6 +1326,11 @@ int main(int argc, char **argv)
CHECK(s->paste_selection()==XTC_OK && s->selected_clips().size()==2); CHECK(s->paste_selection()==XTC_OK && s->selected_clips().size()==2);
xtc_project_get_counts(s->project(),&tc,&cc);CHECK(cc==4); xtc_project_get_counts(s->project(),&tc,&cc);CHECK(cc==4);
CHECK(s->repeat_selection(2)==XTC_OK);xtc_project_get_counts(s->project(),&tc,&cc);CHECK(cc==8); CHECK(s->repeat_selection(2)==XTC_OK);xtc_project_get_counts(s->project(),&tc,&cc);CHECK(cc==8);
CHECK(window.undo()==XTC_OK);xtc_project_get_counts(s->project(),&tc,&cc);CHECK(cc==4);
CHECK(window.undo(true)==XTC_OK);xtc_project_get_counts(s->project(),&tc,&cc);CHECK(cc==8);
s->select_all_clips();CHECK(s->transform_selection(XTC_CLIPS_ALIGN,3840)==XTC_OK);
CHECK(xtc_project_find_clip(s->project(),first,&moved)==XTC_OK && moved.start_tick==3840);
CHECK(s->transform_selection(XTC_CLIPS_SLIP,0)==XTC_OK);
window.do_callback(); window.do_callback();
} }
if (failures != 0) return 1; if (failures != 0) return 1;

View file

@ -2,6 +2,23 @@
#include <assert.h> #include <assert.h>
#include <stdio.h> #include <stdio.h>
static void test_align_slip(void)
{
xtc_project *p=NULL;uint64_t t,ids[2];xtc_clip_info c;
assert(xtc_project_create(&p)==XTC_OK && xtc_project_add_track(p,"Track",&t)==XTC_OK);
assert(xtc_project_add_clip(p,t,"A",0,1920,&ids[0])==XTC_OK);
assert(xtc_project_add_clip(p,t,"B",960,960,&ids[1])==XTC_OK);
assert(xtc_project_edit_clip(p,ids[0],t,0,480,240)==XTC_OK);
assert(xtc_project_edit_clip(p,ids[1],t,960,480,240)==XTC_OK);
assert(xtc_project_transform_clips(p,ids,2,XTC_CLIPS_SLIP,240,NULL)==XTC_OK);
assert(xtc_project_find_clip(p,ids[0],&c)==XTC_OK && c.offset_tick==480 && c.start_tick==0);
/* First clip permits this slip, second does not: no partial publication. */
assert(xtc_project_transform_clips(p,ids,2,XTC_CLIPS_SLIP,240,NULL)==XTC_ERR_INVALID_ARGUMENT);
assert(xtc_project_find_clip(p,ids[0],&c)==XTC_OK && c.offset_tick==480);
assert(xtc_project_transform_clips(p,ids,2,XTC_CLIPS_ALIGN,3840,NULL)==XTC_OK);
assert(xtc_project_find_clip(p,ids[1],&c)==XTC_OK && c.start_tick==3840 && c.offset_tick==480);
xtc_project_destroy(p);
}
static void test_repeat(void) static void test_repeat(void)
{ {
xtc_project *p=NULL;uint64_t t,ids[2];xtc_clip_info c;size_t tracks,clips; xtc_project *p=NULL;uint64_t t,ids[2];xtc_clip_info c;size_t tracks,clips;
@ -18,7 +35,7 @@ static void test_repeat(void)
} }
int main(void) int main(void)
{ {
test_repeat(); test_repeat();test_align_slip();
xtc_project *p=NULL;uint64_t track,a,b,n,ids[2],out[2]={999,999};xtc_clip_info c; xtc_project *p=NULL;uint64_t track,a,b,n,ids[2],out[2]={999,999};xtc_clip_info c;
size_t tracks,clips,notes; size_t tracks,clips,notes;
assert(xtc_project_create(&p)==XTC_OK); assert(xtc_project_create(&p)==XTC_OK);