From b0dbf6905f6321fd3181422b9ee74b8147dbd898 Mon Sep 17 00:00:00 2001 From: hanez Date: Fri, 6 Nov 2026 00:16:31 +0100 Subject: [PATCH] feat: align clip selections and slip sources without moving boundaries --- docs/MODERN_EDITING.md | 4 ++++ docs/ROADMAP.md | 8 ++++---- include/xtc/xtc_editing.h | 4 +++- src/gui/main_window.cpp | 3 +++ src/project/editing.c | 8 +++++++- tests/integration/test_gui.cpp | 5 +++++ tests/unit/test_modern_editing.c | 19 ++++++++++++++++++- 7 files changed, 44 insertions(+), 7 deletions(-) diff --git a/docs/MODERN_EDITING.md b/docs/MODERN_EDITING.md index 04af8c4..feaa8c7 100644 --- a/docs/MODERN_EDITING.md +++ b/docs/MODERN_EDITING.md @@ -19,3 +19,7 @@ Ctrl+C/Ctrl+V in the sequencer, or Edit/Copy clips and Paste clips at cursor, co ## 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. + +## 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. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 413268c..8c1c2b3 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -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. ### Tasks and acceptance criteria -- [ ] Provide the control/model behavior and accessible GUI commands. -- [ ] Validate bounds and failure rollback; preserve existing project compatibility and callback safety. -- [ ] Test the new workflow and document scope and limitations. +- [x] Provide the control/model behavior and accessible GUI commands. +- [x] Validate bounds and failure rollback; preserve existing project compatibility and callback safety. +- [x] Test the new workflow and document scope and limitations. ### 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. --- diff --git a/include/xtc/xtc_editing.h b/include/xtc/xtc_editing.h index 6893303..64005e4 100644 --- a/include/xtc/xtc_editing.h +++ b/include/xtc/xtc_editing.h @@ -9,8 +9,10 @@ extern "C" { * 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. * 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. */ -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_clip_operation operation,int64_t amount,uint64_t *out_ids); /* Repeat the bounding phrase span, including gaps, 1..1024 additional copies. diff --git a/src/gui/main_window.cpp b/src/gui/main_window.cpp index 1773860..0513a61 100644 --- a/src/gui/main_window.cpp +++ b/src/gui/main_window.cpp @@ -322,6 +322,9 @@ MainWindow::MainWindow(bool prompt_unsaved,const char *configuration) Fl_Group::current(parent); menu->add("Edit/Nudge selection left",0,[](Fl_Widget *,void *c){static_cast(c)->nudge_selection(-1);},sequencer_); menu->add("Edit/Nudge selection right",0,[](Fl_Widget *,void *c){static_cast(c)->nudge_selection(1);},sequencer_); + menu->add("Edit/Align clip starts to cursor",0,[](Fl_Widget *,void *c){auto *s=static_cast(c);s->transform_selection(XTC_CLIPS_ALIGN,static_cast(s->position()));},sequencer_); + menu->add("Edit/Slip sources earlier",0,[](Fl_Widget *,void *c){auto *s=static_cast(c);s->transform_selection(XTC_CLIPS_SLIP,-static_cast(s->grid()));},sequencer_); + menu->add("Edit/Slip sources later",0,[](Fl_Widget *,void *c){auto *s=static_cast(c);s->transform_selection(XTC_CLIPS_SLIP,static_cast(s->grid()));},sequencer_); menu->add("Edit/Repeat selected phrase...",0,[](Fl_Widget *,void *c){static_cast(c)->repeat_dialog();},sequencer_); menu->add("Edit/Copy clips",0,[](Fl_Widget *,void *c){static_cast(c)->copy_selection();},sequencer_); menu->add("Edit/Paste clips at cursor",0,[](Fl_Widget *,void *c){static_cast(c)->paste_selection();},sequencer_); diff --git a/src/project/editing.c b/src/project/editing.c index bcb0ca1..7a8ce15 100644 --- a/src/project/editing.c +++ b/src/project/editing.c @@ -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_result r=selection_valid(p,ids,count); if(r!=XTC_OK)return r; - if(operationXTC_CLIPS_DELETE || + if(operationXTC_CLIPS_SLIP || amount<-(int64_t)XTC_PROJECT_MAX_TICK || amount>(int64_t)XTC_PROJECT_MAX_TICK)return XTC_ERR_INVALID_ARGUMENT; r=xtc_project_clone(p,©); for(i=0;r==XTC_OK && i(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 r=xtc_project_edit_clip(copy,ids[i],c.track_id,(uint64_t)start,c.length_tick,c.offset_tick); diff --git a/tests/integration/test_gui.cpp b/tests/integration/test_gui.cpp index 61115d0..3657de4 100644 --- a/tests/integration/test_gui.cpp +++ b/tests/integration/test_gui.cpp @@ -1326,6 +1326,11 @@ int main(int argc, char **argv) CHECK(s->paste_selection()==XTC_OK && s->selected_clips().size()==2); 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(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(); } if (failures != 0) return 1; diff --git a/tests/unit/test_modern_editing.c b/tests/unit/test_modern_editing.c index 26d351e..c409514 100644 --- a/tests/unit/test_modern_editing.c +++ b/tests/unit/test_modern_editing.c @@ -2,6 +2,23 @@ #include #include +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) { 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) { - 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; size_t tracks,clips,notes; assert(xtc_project_create(&p)==XTC_OK);