engine: timeline panel core edit commands migrate to the facade (part 1)
- new batch primitives: split_clips (link-preserving, single undo command), delete_clips (gap replace + optional ripple with explicit region support), ripple_delete_range, marker_add_ex with color - razor/split-at-playhead, clip delete, ripple-to-point, track delete, and the non-dialog marker path now issue facade commands instead of the app's own undo command classes - batch operations deliberately produce one undo command per user action (deleting twenty clips is one entry, not twenty); selection and transition removal stay UI-side as documented leftovers
This commit is contained in:
@@ -274,12 +274,14 @@ oakengine_sequence_marker_count(const OakEngineSequence *self);
|
||||
/**
|
||||
* @brief Marker at `index`: `time` receives its in-point as a timestamp in
|
||||
* timebase units (may be NULL), `name` its label using the buf/size
|
||||
* truncation convention (may be NULL to only fetch the time). Returns
|
||||
* truncation convention (may be NULL to only fetch the time), `color` its
|
||||
* color index (may be NULL). Returns
|
||||
* OAKENGINE_OK on success, OAKENGINE_E_NOT_FOUND for an out-of-range index.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_sequence_marker_at(const OakEngineSequence *self,
|
||||
int index, int64_t *time,
|
||||
char *name, int name_size);
|
||||
char *name, int name_size,
|
||||
int *color);
|
||||
|
||||
/* ---- Timeline editing primitives ---------------------------------------- */
|
||||
|
||||
@@ -437,6 +439,61 @@ OAKENGINE_API int oakengine_sequence_move_clip(OakEngineSequence *seq,
|
||||
int clip_index,
|
||||
int64_t new_in);
|
||||
|
||||
/* ---- Batch editing (timeline panel) ------------------------------------------
|
||||
*
|
||||
* Higher-level operations mirroring the application's timeline panel
|
||||
* (app/widget/timelinewidget), each undoable as ONE command like the
|
||||
* panel's own undo entries. Clip arrays hold borrowed handles
|
||||
* (oakengine_sequence_clip_at(); the handle is the engine ClipBlock
|
||||
* pointer in this family, so the application can pass its own clips
|
||||
* directly). All times are frame timestamps in the sequence's frame-rate
|
||||
* timebase.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief Split every given clip at timeline `time_ts`, preserving links
|
||||
* (undoable; olive::BlockSplitPreservingLinksCommand -- the application's
|
||||
* razor tool / split-at-playhead command).
|
||||
*
|
||||
* Clips not spanning `time_ts` are skipped (same as the engine command);
|
||||
* when none of the clips spans it, the call fails with
|
||||
* OAKENGINE_E_NOT_FOUND and nothing is pushed. The halves of linked clips
|
||||
* come out linked, like the application's split.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_sequence_split_clips(
|
||||
OakEngineSequence *seq, OakEngineClip **clips, int clip_count,
|
||||
int64_t time_ts);
|
||||
|
||||
/**
|
||||
* @brief Delete clips leaving gaps, optionally rippling regions closed
|
||||
* (undoable; the clip-deletion core of the application's
|
||||
* TimelineWidget::DeleteSelected).
|
||||
*
|
||||
* Each clip is replaced with a gap (olive::TrackReplaceBlockWithGapCommand,
|
||||
* transitions left to the caller like the application) and removed from
|
||||
* the graph with its exclusive dependencies
|
||||
* (olive::NodeRemoveWithExclusiveDependenciesAndDisconnect).
|
||||
*
|
||||
* When `ripple` != 0, a olive::TimelineRippleDeleteGapsAtRegionsCommand
|
||||
* follows over `ripple_ranges_ts` -- 4 int64 per range: track_type,
|
||||
* track_index, in_ts, out_ts (NULL with `ripple_range_count` 0 ripples the
|
||||
* deleted clips' own ranges instead). `rippled` (may be NULL) receives 1
|
||||
* when the ripple actually produced commands. Everything lands as one
|
||||
* undoable command.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_sequence_delete_clips(
|
||||
OakEngineSequence *seq, OakEngineClip **clips, int clip_count, int ripple,
|
||||
const int64_t *ripple_ranges_ts, int ripple_range_count, int *rippled);
|
||||
|
||||
/**
|
||||
* @brief Remove the area [in_ts, out_ts) on every track and shift the
|
||||
* following content left (undoable;
|
||||
* olive::TimelineRippleRemoveAreaCommand -- the application's
|
||||
* "ripple to playhead"). `in_ts` must be >= 0 and `out_ts` > `in_ts`.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_sequence_ripple_delete_range(
|
||||
OakEngineSequence *seq, int64_t in_ts, int64_t out_ts);
|
||||
|
||||
/* ---- Track structure and markers ------------------------------------------
|
||||
*
|
||||
* Track structure edits are undoable like the other editing primitives.
|
||||
@@ -524,6 +581,18 @@ OAKENGINE_API int oakengine_sequence_marker_add(OakEngineSequence *seq,
|
||||
int64_t time_ts,
|
||||
const char *name);
|
||||
|
||||
/**
|
||||
* @brief Add a timeline marker with an explicit color index (undoable).
|
||||
*
|
||||
* Same as oakengine_sequence_marker_add() (which passes color 0) but the
|
||||
* caller picks the marker color, like the application's "set marker"
|
||||
* action (color of the closest marker, or the configured default).
|
||||
*/
|
||||
OAKENGINE_API int oakengine_sequence_marker_add_ex(OakEngineSequence *seq,
|
||||
int64_t time_ts,
|
||||
const char *name,
|
||||
int color);
|
||||
|
||||
/**
|
||||
* @brief Remove the (first) marker at `time_ts` (undoable;
|
||||
* olive::MarkerRemoveCommand). OAKENGINE_E_NOT_FOUND when no marker exists
|
||||
|
||||
@@ -714,7 +714,8 @@ int oakengine_sequence_marker_count(const OakEngineSequence *self)
|
||||
}
|
||||
|
||||
int oakengine_sequence_marker_at(const OakEngineSequence *self, int index,
|
||||
int64_t *time, char *name, int name_size)
|
||||
int64_t *time, char *name, int name_size,
|
||||
int *color)
|
||||
{
|
||||
if (!self || index < 0) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
@@ -734,6 +735,9 @@ int oakengine_sequence_marker_at(const OakEngineSequence *self, int index,
|
||||
if (name && name_size > 0) {
|
||||
copy_to_buf(marker->name(), name, size_t(name_size));
|
||||
}
|
||||
if (color) {
|
||||
*color = marker->color();
|
||||
}
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
@@ -1076,6 +1080,189 @@ int oakengine_sequence_move_clip(OakEngineSequence *seq, int track_type,
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
/* ---- Batch editing (timeline panel) ------------------------------------------ */
|
||||
|
||||
int oakengine_sequence_split_clips(OakEngineSequence *seq,
|
||||
OakEngineClip **clips, int clip_count,
|
||||
int64_t time_ts)
|
||||
{
|
||||
set_seq_error(QString());
|
||||
olive::Sequence *sequence = reinterpret_cast<olive::Sequence *>(seq);
|
||||
if (!sequence || !clips || clip_count <= 0) {
|
||||
set_seq_error(QStringLiteral("invalid arguments"));
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
olive::Rational tb;
|
||||
if (!time_base_of(sequence, &tb)) {
|
||||
set_seq_error(QStringLiteral("sequence has no valid frame rate"));
|
||||
return OAKENGINE_E_STATE;
|
||||
}
|
||||
const olive::Rational time =
|
||||
olive::core::Timecode::timestamp_to_time(time_ts, tb);
|
||||
|
||||
QVector<olive::Block *> blocks;
|
||||
blocks.reserve(clip_count);
|
||||
bool any_spanning = false;
|
||||
for (int i = 0; i < clip_count; i++) {
|
||||
olive::ClipBlock *clip =
|
||||
reinterpret_cast<olive::ClipBlock *>(clips[i]);
|
||||
if (!clip) {
|
||||
set_seq_error(QStringLiteral("invalid clip at index %1").arg(i));
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
if (blocks.contains(clip)) {
|
||||
continue;
|
||||
}
|
||||
blocks.append(clip);
|
||||
if (clip->in() < time && clip->out() > time) {
|
||||
any_spanning = true;
|
||||
}
|
||||
}
|
||||
if (!any_spanning) {
|
||||
set_seq_error(QStringLiteral("no clip spans time %1").arg(time_ts));
|
||||
return OAKENGINE_E_NOT_FOUND;
|
||||
}
|
||||
|
||||
// Same as the application's razor tool / split-at-playhead
|
||||
// (BlockSplitPreservingLinksCommand): split every block spanning the
|
||||
// time and link the halves of linked blocks, one undoable command.
|
||||
push_or_run(new olive::BlockSplitPreservingLinksCommand(blocks, { time }),
|
||||
QStringLiteral("Split Clips"));
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_sequence_delete_clips(OakEngineSequence *seq,
|
||||
OakEngineClip **clips, int clip_count,
|
||||
int ripple, const int64_t *ripple_ranges_ts,
|
||||
int ripple_range_count, int *rippled)
|
||||
{
|
||||
set_seq_error(QString());
|
||||
if (rippled) {
|
||||
*rippled = 0;
|
||||
}
|
||||
olive::Sequence *sequence = reinterpret_cast<olive::Sequence *>(seq);
|
||||
if (!sequence || clip_count < 0 || (clip_count > 0 && !clips) ||
|
||||
ripple_range_count < 0 ||
|
||||
(ripple_range_count > 0 && !ripple_ranges_ts)) {
|
||||
set_seq_error(QStringLiteral("invalid arguments"));
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
if (clip_count == 0 && (!ripple || ripple_range_count == 0)) {
|
||||
// Nothing to delete and nothing to ripple.
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
olive::Rational tb;
|
||||
if (!time_base_of(sequence, &tb)) {
|
||||
set_seq_error(QStringLiteral("sequence has no valid frame rate"));
|
||||
return OAKENGINE_E_STATE;
|
||||
}
|
||||
|
||||
olive::MultiUndoCommand *command = new olive::MultiUndoCommand();
|
||||
olive::TimelineRippleDeleteGapsAtRegionsCommand::RangeList clip_ranges;
|
||||
for (int i = 0; i < clip_count; i++) {
|
||||
olive::ClipBlock *clip =
|
||||
reinterpret_cast<olive::ClipBlock *>(clips[i]);
|
||||
if (!clip || !clip->track()) {
|
||||
set_seq_error(QStringLiteral("invalid clip at index %1").arg(i));
|
||||
delete command;
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
// Same as the application's delete-selection core
|
||||
// (TimelineWidget::DeleteSelected): replace the clip with a gap
|
||||
// (transitions are the caller's job) and remove it from the graph
|
||||
// with its exclusive dependencies.
|
||||
command->add_child(new olive::TrackReplaceBlockWithGapCommand(
|
||||
clip->track(), clip, false));
|
||||
command->add_child(
|
||||
new olive::NodeRemoveWithExclusiveDependenciesAndDisconnect(clip));
|
||||
clip_ranges.append({ clip->track(), clip->range() });
|
||||
}
|
||||
|
||||
olive::TimelineRippleDeleteGapsAtRegionsCommand *ripple_command = nullptr;
|
||||
if (ripple) {
|
||||
olive::TimelineRippleDeleteGapsAtRegionsCommand::RangeList ranges;
|
||||
if (ripple_ranges_ts && ripple_range_count > 0) {
|
||||
for (int i = 0; i < ripple_range_count; i++) {
|
||||
const int64_t *range = ripple_ranges_ts + i * 4;
|
||||
const int track_type = int(range[0]);
|
||||
const int track_index = int(range[1]);
|
||||
if (track_type < OAKENGINE_TRACK_TYPE_VIDEO ||
|
||||
track_type > OAKENGINE_TRACK_TYPE_SUBTITLE) {
|
||||
set_seq_error(QStringLiteral("invalid track type in "
|
||||
"ripple range %1")
|
||||
.arg(i));
|
||||
delete command;
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
olive::TrackList *list =
|
||||
sequence->track_list(to_track_type(track_type));
|
||||
if (track_index < 0 ||
|
||||
track_index >= list->get_track_count()) {
|
||||
set_seq_error(QStringLiteral("no track at index %1 in "
|
||||
"ripple range %2")
|
||||
.arg(track_index)
|
||||
.arg(i));
|
||||
delete command;
|
||||
return OAKENGINE_E_NOT_FOUND;
|
||||
}
|
||||
ranges.append({ list->get_track_at(track_index),
|
||||
olive::TimeRange(
|
||||
olive::core::Timecode::timestamp_to_time(
|
||||
range[2], tb),
|
||||
olive::core::Timecode::timestamp_to_time(
|
||||
range[3], tb)) });
|
||||
}
|
||||
} else {
|
||||
ranges = clip_ranges;
|
||||
}
|
||||
if (!ranges.isEmpty()) {
|
||||
ripple_command =
|
||||
new olive::TimelineRippleDeleteGapsAtRegionsCommand(sequence,
|
||||
ranges);
|
||||
command->add_child(ripple_command);
|
||||
}
|
||||
}
|
||||
|
||||
push_or_run(command, QStringLiteral("Delete Clips"));
|
||||
if (rippled) {
|
||||
// has_commands() is valid after the stack prepared the command;
|
||||
// without an engine the command ran directly and a non-null ripple
|
||||
// command means regions were queued.
|
||||
*rippled = (ripple_command && (!olive::EngineCore::instance() ||
|
||||
ripple_command->has_commands())) ?
|
||||
1 :
|
||||
0;
|
||||
}
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_sequence_ripple_delete_range(OakEngineSequence *seq,
|
||||
int64_t in_ts, int64_t out_ts)
|
||||
{
|
||||
set_seq_error(QString());
|
||||
olive::Sequence *sequence = reinterpret_cast<olive::Sequence *>(seq);
|
||||
if (!sequence || in_ts < 0 || out_ts <= in_ts) {
|
||||
set_seq_error(QStringLiteral("invalid range [%1, %2)")
|
||||
.arg(in_ts)
|
||||
.arg(out_ts));
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
olive::Rational tb;
|
||||
if (!time_base_of(sequence, &tb)) {
|
||||
set_seq_error(QStringLiteral("sequence has no valid frame rate"));
|
||||
return OAKENGINE_E_STATE;
|
||||
}
|
||||
// Same as the application's "ripple to playhead" (TimelineWidget::
|
||||
// ripple_to): remove the area on every track and shift the following
|
||||
// content left, one undoable command.
|
||||
push_or_run(new olive::TimelineRippleRemoveAreaCommand(
|
||||
sequence,
|
||||
olive::core::Timecode::timestamp_to_time(in_ts, tb),
|
||||
olive::core::Timecode::timestamp_to_time(out_ts, tb)),
|
||||
QStringLiteral("Ripple Delete Range"));
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
/* ---- Track structure and markers ------------------------------------------ */
|
||||
|
||||
int oakengine_sequence_remove_track(OakEngineSequence *seq, int track_type,
|
||||
@@ -1278,6 +1465,12 @@ int oakengine_track_set_locked(OakEngineSequence *seq, int track_type,
|
||||
|
||||
int oakengine_sequence_marker_add(OakEngineSequence *seq, int64_t time_ts,
|
||||
const char *name)
|
||||
{
|
||||
return oakengine_sequence_marker_add_ex(seq, time_ts, name, 0);
|
||||
}
|
||||
|
||||
int oakengine_sequence_marker_add_ex(OakEngineSequence *seq, int64_t time_ts,
|
||||
const char *name, int color)
|
||||
{
|
||||
set_seq_error(QString());
|
||||
olive::Sequence *sequence = reinterpret_cast<olive::Sequence *>(seq);
|
||||
@@ -1301,7 +1494,7 @@ int oakengine_sequence_marker_add(OakEngineSequence *seq, int64_t time_ts,
|
||||
}
|
||||
push_or_run(new olive::MarkerAddCommand(
|
||||
markers, olive::TimeRange(time, time),
|
||||
QString::fromUtf8(name ? name : ""), 0),
|
||||
QString::fromUtf8(name ? name : ""), color),
|
||||
QStringLiteral("Add Marker"));
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
@@ -220,7 +220,7 @@ static void test_sequence_and_save_load(void)
|
||||
|
||||
// No markers on a fresh sequence.
|
||||
assert(oakengine_sequence_marker_count(seq) == 0);
|
||||
assert(oakengine_sequence_marker_at(seq, 0, NULL, NULL, 0) ==
|
||||
assert(oakengine_sequence_marker_at(seq, 0, NULL, NULL, 0, NULL) ==
|
||||
OAKENGINE_E_NOT_FOUND);
|
||||
|
||||
// Save; the modified flag clears and the filename is adopted.
|
||||
@@ -384,7 +384,7 @@ static void test_null_safety(void)
|
||||
assert(oakengine_sequence_set_workarea(NULL, 0, 0, 0) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
assert(oakengine_sequence_marker_count(NULL) == 0);
|
||||
assert(oakengine_sequence_marker_at(NULL, 0, NULL, NULL, 0) ==
|
||||
assert(oakengine_sequence_marker_at(NULL, 0, NULL, NULL, 0, NULL) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
}
|
||||
|
||||
|
||||
@@ -584,7 +584,7 @@ static void test_markers(void)
|
||||
assert(oakengine_sequence_marker_add(seq, 30, "Chapter 1") ==
|
||||
OAKENGINE_OK);
|
||||
assert(oakengine_sequence_marker_count(seq) == 1);
|
||||
assert(oakengine_sequence_marker_at(seq, 0, &ts, name, sizeof(name)) ==
|
||||
assert(oakengine_sequence_marker_at(seq, 0, &ts, name, sizeof(name), NULL) ==
|
||||
OAKENGINE_OK);
|
||||
assert(ts == 30 && strcmp(name, "Chapter 1") == 0);
|
||||
|
||||
@@ -595,29 +595,29 @@ static void test_markers(void)
|
||||
// Earlier marker sorts in front.
|
||||
assert(oakengine_sequence_marker_add(seq, 10, "Intro") == OAKENGINE_OK);
|
||||
assert(oakengine_sequence_marker_count(seq) == 2);
|
||||
assert(oakengine_sequence_marker_at(seq, 0, &ts, name, sizeof(name)) ==
|
||||
assert(oakengine_sequence_marker_at(seq, 0, &ts, name, sizeof(name), NULL) ==
|
||||
OAKENGINE_OK);
|
||||
assert(ts == 10 && strcmp(name, "Intro") == 0);
|
||||
assert(oakengine_sequence_marker_at(seq, 1, &ts, name, sizeof(name)) ==
|
||||
assert(oakengine_sequence_marker_at(seq, 1, &ts, name, sizeof(name), NULL) ==
|
||||
OAKENGINE_OK);
|
||||
assert(ts == 30 && strcmp(name, "Chapter 1") == 0);
|
||||
assert(oakengine_sequence_marker_at(seq, 2, &ts, name, sizeof(name)) ==
|
||||
assert(oakengine_sequence_marker_at(seq, 2, &ts, name, sizeof(name), NULL) ==
|
||||
OAKENGINE_E_NOT_FOUND);
|
||||
|
||||
// Rename with undo/redo.
|
||||
assert(oakengine_sequence_marker_rename(seq, 30, "Chapter 2") ==
|
||||
OAKENGINE_OK);
|
||||
assert(oakengine_sequence_marker_at(seq, 1, &ts, name, sizeof(name)) ==
|
||||
assert(oakengine_sequence_marker_at(seq, 1, &ts, name, sizeof(name), NULL) ==
|
||||
OAKENGINE_OK);
|
||||
assert(strcmp(name, "Chapter 2") == 0);
|
||||
assert(oakengine_sequence_marker_rename(seq, 77, "nope") ==
|
||||
OAKENGINE_E_NOT_FOUND);
|
||||
assert(oakengine_project_undo(project) == OAKENGINE_OK);
|
||||
assert(oakengine_sequence_marker_at(seq, 1, &ts, name, sizeof(name)) ==
|
||||
assert(oakengine_sequence_marker_at(seq, 1, &ts, name, sizeof(name), NULL) ==
|
||||
OAKENGINE_OK);
|
||||
assert(strcmp(name, "Chapter 1") == 0);
|
||||
assert(oakengine_project_redo(project) == OAKENGINE_OK);
|
||||
assert(oakengine_sequence_marker_at(seq, 1, &ts, name, sizeof(name)) ==
|
||||
assert(oakengine_sequence_marker_at(seq, 1, &ts, name, sizeof(name), NULL) ==
|
||||
OAKENGINE_OK);
|
||||
assert(strcmp(name, "Chapter 2") == 0);
|
||||
|
||||
@@ -810,6 +810,158 @@ static void test_sequence_params(void)
|
||||
oakengine_project_free(p2);
|
||||
}
|
||||
|
||||
static void test_batch_editing(const char *media_path)
|
||||
{
|
||||
OakEngineProject *project = oakengine_project_create();
|
||||
assert(project != NULL);
|
||||
assert(oakengine_project_new(project) == OAKENGINE_OK);
|
||||
OakEngineSequence *seq = oakengine_sequence_new(project, "Batch");
|
||||
assert(seq != NULL);
|
||||
assert(oakengine_sequence_add_track(seq, OAKENGINE_TRACK_TYPE_VIDEO) ==
|
||||
0);
|
||||
OakEngineFootage *footage =
|
||||
oakengine_project_import_footage(project, media_path);
|
||||
assert(footage != NULL);
|
||||
|
||||
int64_t in = -1, out = -1;
|
||||
|
||||
// Two clips on the video track: [0, 100) and [100, 160).
|
||||
OakEngineClip *c0 = oakengine_sequence_add_footage_clip(
|
||||
seq, footage, OAKENGINE_TRACK_TYPE_VIDEO, 0, 0, 100, 0);
|
||||
OakEngineClip *c1 = oakengine_sequence_add_footage_clip(
|
||||
seq, footage, OAKENGINE_TRACK_TYPE_VIDEO, 0, 100, 160, 0);
|
||||
assert(c0 != NULL && c1 != NULL);
|
||||
assert(oakengine_sequence_clip_count(seq, OAKENGINE_TRACK_TYPE_VIDEO,
|
||||
0) == 2);
|
||||
|
||||
// split_clips at 50: the first clip splits in two.
|
||||
{
|
||||
OakEngineClip *arr[1] = { c0 };
|
||||
assert(oakengine_sequence_split_clips(seq, arr, 1, 50) ==
|
||||
OAKENGINE_OK);
|
||||
}
|
||||
assert(oakengine_sequence_clip_count(seq, OAKENGINE_TRACK_TYPE_VIDEO,
|
||||
0) == 3);
|
||||
OakEngineClip *left = oakengine_sequence_clip_at(
|
||||
seq, OAKENGINE_TRACK_TYPE_VIDEO, 0, 0);
|
||||
OakEngineClip *right = oakengine_sequence_clip_at(
|
||||
seq, OAKENGINE_TRACK_TYPE_VIDEO, 0, 1);
|
||||
assert(oakengine_clip_get_range(left, &in, &out, NULL) == OAKENGINE_OK);
|
||||
assert(in == 0 && out == 50);
|
||||
assert(oakengine_clip_get_range(right, &in, &out, NULL) == OAKENGINE_OK);
|
||||
assert(in == 50 && out == 100);
|
||||
|
||||
// Nothing spans 200; invalid arrays are rejected.
|
||||
{
|
||||
OakEngineClip *arr[2] = { left, right };
|
||||
assert(oakengine_sequence_split_clips(seq, arr, 2, 200) ==
|
||||
OAKENGINE_E_NOT_FOUND);
|
||||
assert(oakengine_sequence_split_clips(seq, NULL, 1, 50) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
assert(oakengine_sequence_split_clips(seq, arr, 0, 50) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
assert(oakengine_sequence_split_clips(NULL, arr, 1, 50) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
}
|
||||
|
||||
// Undo the split.
|
||||
assert(oakengine_project_undo(project) == OAKENGINE_OK);
|
||||
assert(oakengine_sequence_clip_count(seq, OAKENGINE_TRACK_TYPE_VIDEO,
|
||||
0) == 2);
|
||||
|
||||
// delete_clips without ripple: a gap is left, the second clip stays.
|
||||
int rippled = -1;
|
||||
{
|
||||
OakEngineClip *arr[1] = { c0 };
|
||||
assert(oakengine_sequence_delete_clips(seq, arr, 1, 0, NULL, 0,
|
||||
&rippled) == OAKENGINE_OK);
|
||||
}
|
||||
assert(rippled == 0);
|
||||
assert(oakengine_sequence_clip_count(seq, OAKENGINE_TRACK_TYPE_VIDEO,
|
||||
0) == 1);
|
||||
assert(oakengine_clip_get_range(c1, &in, &out, NULL) == OAKENGINE_OK);
|
||||
assert(in == 100 && out == 160);
|
||||
assert(oakengine_project_undo(project) == OAKENGINE_OK);
|
||||
assert(oakengine_sequence_clip_count(seq, OAKENGINE_TRACK_TYPE_VIDEO,
|
||||
0) == 2);
|
||||
|
||||
// delete_clips with ripple (auto ranges): the second clip shifts left.
|
||||
rippled = -1;
|
||||
{
|
||||
OakEngineClip *arr[1] = { c0 };
|
||||
assert(oakengine_sequence_delete_clips(seq, arr, 1, 1, NULL, 0,
|
||||
&rippled) == OAKENGINE_OK);
|
||||
}
|
||||
assert(rippled == 1);
|
||||
assert(oakengine_clip_get_range(c1, &in, &out, NULL) == OAKENGINE_OK);
|
||||
assert(in == 0 && out == 60);
|
||||
assert(oakengine_project_undo(project) == OAKENGINE_OK);
|
||||
assert(oakengine_clip_get_range(c1, &in, &out, NULL) == OAKENGINE_OK);
|
||||
assert(in == 100 && out == 160);
|
||||
|
||||
// delete_clips with an explicit ripple range: same shift.
|
||||
rippled = -1;
|
||||
{
|
||||
OakEngineClip *arr[1] = { c0 };
|
||||
const int64_t ranges[4] = { OAKENGINE_TRACK_TYPE_VIDEO, 0, 0, 100 };
|
||||
assert(oakengine_sequence_delete_clips(seq, arr, 1, 1, ranges, 1,
|
||||
&rippled) == OAKENGINE_OK);
|
||||
}
|
||||
assert(rippled == 1);
|
||||
assert(oakengine_clip_get_range(c1, &in, &out, NULL) == OAKENGINE_OK);
|
||||
assert(in == 0 && out == 60);
|
||||
assert(oakengine_project_undo(project) == OAKENGINE_OK);
|
||||
assert(oakengine_sequence_clip_count(seq, OAKENGINE_TRACK_TYPE_VIDEO,
|
||||
0) == 2);
|
||||
|
||||
// A bad explicit range coordinate is rejected without side effects.
|
||||
{
|
||||
OakEngineClip *arr[1] = { c0 };
|
||||
const int64_t bad[4] = { OAKENGINE_TRACK_TYPE_VIDEO, 9, 0, 100 };
|
||||
assert(oakengine_sequence_delete_clips(seq, arr, 1, 1, bad, 1,
|
||||
NULL) == OAKENGINE_E_NOT_FOUND);
|
||||
}
|
||||
assert(oakengine_sequence_clip_count(seq, OAKENGINE_TRACK_TYPE_VIDEO,
|
||||
0) == 2);
|
||||
assert(oakengine_sequence_delete_clips(NULL, NULL, 0, 0, NULL, 0,
|
||||
NULL) == OAKENGINE_E_INVALID);
|
||||
|
||||
// ripple_delete_range over [0, 100): the area is removed from every
|
||||
// track and the following content shifts left.
|
||||
assert(oakengine_sequence_ripple_delete_range(seq, 0, 100) ==
|
||||
OAKENGINE_OK);
|
||||
assert(oakengine_sequence_clip_count(seq, OAKENGINE_TRACK_TYPE_VIDEO,
|
||||
0) == 1);
|
||||
OakEngineClip *remaining = oakengine_sequence_clip_at(
|
||||
seq, OAKENGINE_TRACK_TYPE_VIDEO, 0, 0);
|
||||
assert(oakengine_clip_get_range(remaining, &in, &out, NULL) ==
|
||||
OAKENGINE_OK);
|
||||
assert(in == 0 && out == 60);
|
||||
assert(oakengine_sequence_ripple_delete_range(seq, 100, 100) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
assert(oakengine_sequence_ripple_delete_range(seq, -1, 5) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
assert(oakengine_sequence_ripple_delete_range(NULL, 0, 1) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
assert(oakengine_project_undo(project) == OAKENGINE_OK);
|
||||
assert(oakengine_sequence_clip_count(seq, OAKENGINE_TRACK_TYPE_VIDEO,
|
||||
0) == 2);
|
||||
|
||||
// Marker with an explicit color.
|
||||
assert(oakengine_sequence_marker_add_ex(seq, 10, "colored", 5) ==
|
||||
OAKENGINE_OK);
|
||||
int color = -1;
|
||||
int64_t ts = -1;
|
||||
assert(oakengine_sequence_marker_at(seq, 0, &ts, NULL, 0, &color) ==
|
||||
OAKENGINE_OK);
|
||||
assert(ts == 10 && color == 5);
|
||||
assert(oakengine_project_undo(project) == OAKENGINE_OK);
|
||||
assert(oakengine_sequence_marker_count(seq) == 0);
|
||||
|
||||
oakengine_footage_free(footage);
|
||||
oakengine_project_free(project);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
make_tmpdir();
|
||||
@@ -839,6 +991,7 @@ int main(void)
|
||||
test_track_structure(path);
|
||||
test_markers();
|
||||
test_sequence_params();
|
||||
test_batch_editing(path);
|
||||
|
||||
oakengine_project_free(project);
|
||||
assert(oakengine_shutdown() == OAKENGINE_OK);
|
||||
|
||||
Reference in New Issue
Block a user