engine: timeline edit primitives round 2 (split/ripple-delete/trim/move)
All four are undoable and reuse the timeline/ command classes verbatim (BlockSplitCommand, TrackRippleRemoveAreaCommand, BlockTrimCommand, ReplaceBlockWithGap+PlaceBlock), with undo/redo assertions covering split halves, media-in alignment on trim, ripple shift amounts, and full restoration on undo
This commit is contained in:
@@ -278,6 +278,71 @@ OAKENGINE_API int oakengine_clip_get_range(const OakEngineClip *self,
|
|||||||
int64_t *in, int64_t *out,
|
int64_t *in, int64_t *out,
|
||||||
int64_t *media_in);
|
int64_t *media_in);
|
||||||
|
|
||||||
|
/* ---- Editing primitives, round 2: split / ripple delete / trim / move ----
|
||||||
|
*
|
||||||
|
* All four are undoable like the other editing primitives and report
|
||||||
|
* failures through oakengine_sequence_last_error(). Clips are addressed by
|
||||||
|
* (track_type, track_index, clip_index) exactly like
|
||||||
|
* oakengine_sequence_clip_at() (gap blocks are skipped). All times are
|
||||||
|
* frame timestamps in the sequence's frame-rate timebase.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Split the addressed clip in two at timeline `time` (undoable;
|
||||||
|
* olive::BlockSplitCommand).
|
||||||
|
*
|
||||||
|
* `time` must lie strictly inside the clip's range. The left part keeps the
|
||||||
|
* clip's in-point, the right part starts at `time` with its media in-point
|
||||||
|
* advanced accordingly (the engine's split semantics). Returns OAKENGINE_OK
|
||||||
|
* or a negative code (OAKENGINE_E_NOT_FOUND for a missing clip,
|
||||||
|
* OAKENGINE_E_INVALID for a time outside the clip).
|
||||||
|
*/
|
||||||
|
OAKENGINE_API int oakengine_sequence_split_clip(OakEngineSequence *seq,
|
||||||
|
int track_type,
|
||||||
|
int track_index,
|
||||||
|
int clip_index, int64_t time);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Delete the addressed clip and shift all following clips on the
|
||||||
|
* track left by its length (undoable;
|
||||||
|
* olive::TrackRippleRemoveAreaCommand).
|
||||||
|
*/
|
||||||
|
OAKENGINE_API int oakengine_sequence_ripple_delete_clip(OakEngineSequence *seq,
|
||||||
|
int track_type,
|
||||||
|
int track_index,
|
||||||
|
int clip_index);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Change the clip's timeline range (undoable; olive::BlockTrimCommand,
|
||||||
|
* the application's trim command).
|
||||||
|
*
|
||||||
|
* Pass the current value for the end that should stay unchanged; changing
|
||||||
|
* both ends is applied as an in-trim followed by an out-trim in one
|
||||||
|
* undoable command. Requires new_out > new_in and new_in >= 0. When the
|
||||||
|
* in-point moves, the clip's media in-point moves with it (the engine's
|
||||||
|
* set_length_and_media_in() alignment); adjacent gaps absorb the difference
|
||||||
|
* (the engine's trim semantics, adjacent clips are not rolled). The clip
|
||||||
|
* handle must still be on a track.
|
||||||
|
*/
|
||||||
|
OAKENGINE_API int oakengine_clip_trim(OakEngineClip *clip, int64_t new_in,
|
||||||
|
int64_t new_out);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Move the addressed clip to start at `new_in` on the same track
|
||||||
|
* (undoable).
|
||||||
|
*
|
||||||
|
* Length and media in-point are preserved; the old spot is filled with a
|
||||||
|
* gap (olive::TrackReplaceBlockWithGapCommand) and the clip is placed at
|
||||||
|
* the destination (olive::TrackPlaceBlockCommand, which ripples whatever
|
||||||
|
* was there). Moving across tracks is a later milestone. `new_in` must be
|
||||||
|
* >= 0.
|
||||||
|
*/
|
||||||
|
OAKENGINE_API int oakengine_sequence_move_clip(OakEngineSequence *seq,
|
||||||
|
int track_type,
|
||||||
|
int track_index,
|
||||||
|
int clip_index,
|
||||||
|
int64_t new_in);
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
+195
-14
@@ -35,6 +35,8 @@
|
|||||||
#include "timeline/timelinemarker.h"
|
#include "timeline/timelinemarker.h"
|
||||||
#include "timeline/timelineundogeneral.h"
|
#include "timeline/timelineundogeneral.h"
|
||||||
#include "timeline/timelineundopointer.h"
|
#include "timeline/timelineundopointer.h"
|
||||||
|
#include "timeline/timelineundoripple.h"
|
||||||
|
#include "timeline/timelineundosplit.h"
|
||||||
#include "timeline/timelineworkarea.h"
|
#include "timeline/timelineworkarea.h"
|
||||||
#include "undo/undocommand.h"
|
#include "undo/undocommand.h"
|
||||||
#include "undo/undostack.h"
|
#include "undo/undostack.h"
|
||||||
@@ -137,6 +139,39 @@ OakEngineClip *wrap_clip(olive::ClipBlock *c)
|
|||||||
return reinterpret_cast<OakEngineClip *>(c);
|
return reinterpret_cast<OakEngineClip *>(c);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Push an undoable command onto the global undo stack when the engine is
|
||||||
|
// initialized, otherwise execute it directly (same degradation as the
|
||||||
|
// round-1 primitives).
|
||||||
|
void push_or_run(olive::UndoCommand *command, const QString &name)
|
||||||
|
{
|
||||||
|
if (olive::EngineCore::instance()) {
|
||||||
|
olive::EngineCore::instance()->undo_stack()->push(command, name);
|
||||||
|
} else {
|
||||||
|
command->redo_now();
|
||||||
|
delete command;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The clip at (track_index, clip_index) within the given track list,
|
||||||
|
// skipping gap blocks; nullptr when out of range.
|
||||||
|
olive::ClipBlock *clip_at_index(olive::TrackList *list, int track_index,
|
||||||
|
int clip_index)
|
||||||
|
{
|
||||||
|
if (track_index < 0 || track_index >= list->get_track_count()) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
int seen = 0;
|
||||||
|
for (olive::Block *b : list->get_track_at(track_index)->blocks()) {
|
||||||
|
if (olive::ClipBlock *clip = dynamic_cast<olive::ClipBlock *>(b)) {
|
||||||
|
if (seen == clip_index) {
|
||||||
|
return clip;
|
||||||
|
}
|
||||||
|
seen++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
extern "C"
|
extern "C"
|
||||||
@@ -552,20 +587,7 @@ OakEngineClip *oakengine_sequence_clip_at(OakEngineSequence *self,
|
|||||||
}
|
}
|
||||||
olive::TrackList *list =
|
olive::TrackList *list =
|
||||||
impl(self)->track_list(to_track_type(track_type));
|
impl(self)->track_list(to_track_type(track_type));
|
||||||
if (track_index < 0 || track_index >= list->get_track_count()) {
|
return wrap_clip(clip_at_index(list, track_index, clip_index));
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
// Skip gap blocks: indexes address clips only.
|
|
||||||
int seen = 0;
|
|
||||||
for (olive::Block *b : list->get_track_at(track_index)->blocks()) {
|
|
||||||
if (olive::ClipBlock *clip = dynamic_cast<olive::ClipBlock *>(b)) {
|
|
||||||
if (seen == clip_index) {
|
|
||||||
return wrap_clip(clip);
|
|
||||||
}
|
|
||||||
seen++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nullptr;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int oakengine_clip_get_range(const OakEngineClip *self, int64_t *in,
|
int oakengine_clip_get_range(const OakEngineClip *self, int64_t *in,
|
||||||
@@ -599,4 +621,163 @@ int oakengine_clip_get_range(const OakEngineClip *self, int64_t *in,
|
|||||||
return OAKENGINE_OK;
|
return OAKENGINE_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- Editing primitives, round 2 ----------------------------------------- */
|
||||||
|
|
||||||
|
int oakengine_sequence_split_clip(OakEngineSequence *seq, int track_type,
|
||||||
|
int track_index, int clip_index,
|
||||||
|
int64_t time)
|
||||||
|
{
|
||||||
|
set_seq_error(QString());
|
||||||
|
olive::Sequence *sequence = reinterpret_cast<olive::Sequence *>(seq);
|
||||||
|
if (!sequence || track_type < OAKENGINE_TRACK_TYPE_VIDEO ||
|
||||||
|
track_type > OAKENGINE_TRACK_TYPE_SUBTITLE) {
|
||||||
|
set_seq_error(QStringLiteral("invalid sequence or track type"));
|
||||||
|
return OAKENGINE_E_INVALID;
|
||||||
|
}
|
||||||
|
olive::TrackList *list =
|
||||||
|
sequence->track_list(to_track_type(track_type));
|
||||||
|
olive::ClipBlock *clip = clip_at_index(list, track_index, clip_index);
|
||||||
|
if (!clip) {
|
||||||
|
set_seq_error(QStringLiteral("no clip at track %1 index %2")
|
||||||
|
.arg(track_index)
|
||||||
|
.arg(clip_index));
|
||||||
|
return OAKENGINE_E_NOT_FOUND;
|
||||||
|
}
|
||||||
|
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 point =
|
||||||
|
olive::core::Timecode::timestamp_to_time(time, tb);
|
||||||
|
if (point <= clip->in() || point >= clip->out()) {
|
||||||
|
set_seq_error(QStringLiteral("split time %1 is not strictly inside "
|
||||||
|
"the clip [%2, %3]")
|
||||||
|
.arg(time)
|
||||||
|
.arg(time_to_ts(clip->in(), tb))
|
||||||
|
.arg(time_to_ts(clip->out(), tb)));
|
||||||
|
return OAKENGINE_E_INVALID;
|
||||||
|
}
|
||||||
|
|
||||||
|
push_or_run(new olive::BlockSplitCommand(clip, point),
|
||||||
|
QStringLiteral("Split Clip"));
|
||||||
|
return OAKENGINE_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
int oakengine_sequence_ripple_delete_clip(OakEngineSequence *seq,
|
||||||
|
int track_type, int track_index,
|
||||||
|
int clip_index)
|
||||||
|
{
|
||||||
|
set_seq_error(QString());
|
||||||
|
olive::Sequence *sequence = reinterpret_cast<olive::Sequence *>(seq);
|
||||||
|
if (!sequence || track_type < OAKENGINE_TRACK_TYPE_VIDEO ||
|
||||||
|
track_type > OAKENGINE_TRACK_TYPE_SUBTITLE) {
|
||||||
|
set_seq_error(QStringLiteral("invalid sequence or track type"));
|
||||||
|
return OAKENGINE_E_INVALID;
|
||||||
|
}
|
||||||
|
olive::TrackList *list =
|
||||||
|
sequence->track_list(to_track_type(track_type));
|
||||||
|
olive::ClipBlock *clip = clip_at_index(list, track_index, clip_index);
|
||||||
|
if (!clip || !clip->track()) {
|
||||||
|
set_seq_error(QStringLiteral("no clip at track %1 index %2")
|
||||||
|
.arg(track_index)
|
||||||
|
.arg(clip_index));
|
||||||
|
return OAKENGINE_E_NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
push_or_run(new olive::TrackRippleRemoveAreaCommand(
|
||||||
|
clip->track(), olive::TimeRange(clip->in(), clip->out())),
|
||||||
|
QStringLiteral("Ripple Delete Clip"));
|
||||||
|
return OAKENGINE_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
int oakengine_clip_trim(OakEngineClip *clip, int64_t new_in, int64_t new_out)
|
||||||
|
{
|
||||||
|
set_seq_error(QString());
|
||||||
|
olive::ClipBlock *block = reinterpret_cast<olive::ClipBlock *>(clip);
|
||||||
|
if (!block) {
|
||||||
|
set_seq_error(QStringLiteral("invalid clip handle"));
|
||||||
|
return OAKENGINE_E_INVALID;
|
||||||
|
}
|
||||||
|
olive::Track *track = block->track();
|
||||||
|
if (!track) {
|
||||||
|
set_seq_error(QStringLiteral("clip is not on a track"));
|
||||||
|
return OAKENGINE_E_STATE;
|
||||||
|
}
|
||||||
|
const olive::Sequence *sequence = track->sequence();
|
||||||
|
olive::Rational tb;
|
||||||
|
if (!sequence || !time_base_of(sequence, &tb)) {
|
||||||
|
set_seq_error(QStringLiteral("sequence has no valid frame rate"));
|
||||||
|
return OAKENGINE_E_STATE;
|
||||||
|
}
|
||||||
|
if (new_in < 0 || new_out <= new_in) {
|
||||||
|
set_seq_error(QStringLiteral("invalid trim range (need 0 <= new_in "
|
||||||
|
"< new_out)"));
|
||||||
|
return OAKENGINE_E_INVALID;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int64_t old_in = time_to_ts(block->in(), tb);
|
||||||
|
const int64_t old_out = time_to_ts(block->out(), tb);
|
||||||
|
if (new_in == old_in && new_out == old_out) {
|
||||||
|
return OAKENGINE_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The application's trim command (BlockTrimCommand): one end at a time,
|
||||||
|
// adjacent gaps absorb the difference; both ends are applied as an
|
||||||
|
// in-trim followed by an out-trim within one undoable command.
|
||||||
|
olive::MultiUndoCommand *command = new olive::MultiUndoCommand();
|
||||||
|
if (new_in != old_in) {
|
||||||
|
command->add_child(new olive::BlockTrimCommand(
|
||||||
|
track, block, block->out() -
|
||||||
|
olive::core::Timecode::timestamp_to_time(new_in, tb),
|
||||||
|
olive::Timeline::k_trim_in));
|
||||||
|
}
|
||||||
|
if (new_out != old_out) {
|
||||||
|
command->add_child(new olive::BlockTrimCommand(
|
||||||
|
track, block,
|
||||||
|
olive::core::Timecode::timestamp_to_time(new_out - new_in, tb),
|
||||||
|
olive::Timeline::k_trim_out));
|
||||||
|
}
|
||||||
|
push_or_run(command, QStringLiteral("Trim Clip"));
|
||||||
|
return OAKENGINE_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
int oakengine_sequence_move_clip(OakEngineSequence *seq, int track_type,
|
||||||
|
int track_index, int clip_index,
|
||||||
|
int64_t new_in)
|
||||||
|
{
|
||||||
|
set_seq_error(QString());
|
||||||
|
olive::Sequence *sequence = reinterpret_cast<olive::Sequence *>(seq);
|
||||||
|
if (!sequence || track_type < OAKENGINE_TRACK_TYPE_VIDEO ||
|
||||||
|
track_type > OAKENGINE_TRACK_TYPE_SUBTITLE || new_in < 0) {
|
||||||
|
set_seq_error(QStringLiteral("invalid arguments"));
|
||||||
|
return OAKENGINE_E_INVALID;
|
||||||
|
}
|
||||||
|
olive::TrackList *list =
|
||||||
|
sequence->track_list(to_track_type(track_type));
|
||||||
|
olive::ClipBlock *clip = clip_at_index(list, track_index, clip_index);
|
||||||
|
if (!clip || !clip->track()) {
|
||||||
|
set_seq_error(QStringLiteral("no clip at track %1 index %2")
|
||||||
|
.arg(track_index)
|
||||||
|
.arg(clip_index));
|
||||||
|
return OAKENGINE_E_NOT_FOUND;
|
||||||
|
}
|
||||||
|
olive::Rational tb;
|
||||||
|
if (!time_base_of(sequence, &tb)) {
|
||||||
|
set_seq_error(QStringLiteral("sequence has no valid frame rate"));
|
||||||
|
return OAKENGINE_E_STATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same assembly as the application's drag-move: gap the old spot, then
|
||||||
|
// place the clip at the destination (length and media in-point stay).
|
||||||
|
olive::MultiUndoCommand *command = new olive::MultiUndoCommand();
|
||||||
|
command->add_child(
|
||||||
|
new olive::TrackReplaceBlockWithGapCommand(clip->track(), clip));
|
||||||
|
command->add_child(new olive::TrackPlaceBlockCommand(
|
||||||
|
list, track_index, clip,
|
||||||
|
olive::core::Timecode::timestamp_to_time(new_in, tb)));
|
||||||
|
push_or_run(command, QStringLiteral("Move Clip"));
|
||||||
|
return OAKENGINE_OK;
|
||||||
|
}
|
||||||
|
|
||||||
} // extern "C"
|
} // extern "C"
|
||||||
|
|||||||
@@ -243,6 +243,207 @@ static void test_cross_project_rejected(const char *media_path)
|
|||||||
oakengine_project_free(b);
|
oakengine_project_free(b);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Round-2 primitives: split, trim, move, ripple delete (plus undo/redo),
|
||||||
|
// over a fresh project with two clips on one video track.
|
||||||
|
static void test_edit_round2(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, "Round2");
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Two clips back to back: A [0,30] and B [30,60], both from media 0.
|
||||||
|
OakEngineClip *clip_a = oakengine_sequence_add_footage_clip(
|
||||||
|
seq, footage, OAKENGINE_TRACK_TYPE_VIDEO, 0, 0, 30, 0);
|
||||||
|
assert(clip_a != NULL);
|
||||||
|
OakEngineClip *clip_b = oakengine_sequence_add_footage_clip(
|
||||||
|
seq, footage, OAKENGINE_TRACK_TYPE_VIDEO, 0, 30, 60, 0);
|
||||||
|
assert(clip_b != NULL);
|
||||||
|
assert(oakengine_sequence_clip_count(seq, OAKENGINE_TRACK_TYPE_VIDEO,
|
||||||
|
0) == 2);
|
||||||
|
|
||||||
|
int64_t in = -1, out = -1, media_in = -1;
|
||||||
|
char err[512];
|
||||||
|
|
||||||
|
// ---- split -------------------------------------------------------------
|
||||||
|
// Bad split points: on the edges, outside, missing clip.
|
||||||
|
assert(oakengine_sequence_split_clip(seq, OAKENGINE_TRACK_TYPE_VIDEO, 0,
|
||||||
|
0, 0) == OAKENGINE_E_INVALID);
|
||||||
|
assert(oakengine_sequence_split_clip(seq, OAKENGINE_TRACK_TYPE_VIDEO, 0,
|
||||||
|
0, 30) == OAKENGINE_E_INVALID);
|
||||||
|
assert(oakengine_sequence_split_clip(seq, OAKENGINE_TRACK_TYPE_VIDEO, 0,
|
||||||
|
0, -5) == OAKENGINE_E_INVALID);
|
||||||
|
assert(oakengine_sequence_split_clip(seq, OAKENGINE_TRACK_TYPE_VIDEO, 0,
|
||||||
|
0, 99) == OAKENGINE_E_INVALID);
|
||||||
|
assert(oakengine_sequence_split_clip(seq, OAKENGINE_TRACK_TYPE_VIDEO, 0,
|
||||||
|
99, 15) == OAKENGINE_E_NOT_FOUND);
|
||||||
|
assert(oakengine_sequence_split_clip(NULL, OAKENGINE_TRACK_TYPE_VIDEO, 0,
|
||||||
|
0, 15) == OAKENGINE_E_INVALID);
|
||||||
|
|
||||||
|
// Split A [0,30] at 15 -> [0,15] + [15,30] with aligned media in-points.
|
||||||
|
assert(oakengine_sequence_split_clip(seq, OAKENGINE_TRACK_TYPE_VIDEO, 0,
|
||||||
|
0, 15) == 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(left == clip_a);
|
||||||
|
assert(oakengine_clip_get_range(left, &in, &out, &media_in) ==
|
||||||
|
OAKENGINE_OK);
|
||||||
|
assert(in == 0 && out == 15 && media_in == 0);
|
||||||
|
assert(oakengine_clip_get_range(right, &in, &out, &media_in) ==
|
||||||
|
OAKENGINE_OK);
|
||||||
|
assert(in == 15 && out == 30 && media_in == 15);
|
||||||
|
|
||||||
|
// Undo merges them back, redo splits again.
|
||||||
|
assert(oakengine_project_undo(project) == OAKENGINE_OK);
|
||||||
|
assert(oakengine_sequence_clip_count(seq, OAKENGINE_TRACK_TYPE_VIDEO,
|
||||||
|
0) == 2);
|
||||||
|
assert(oakengine_clip_get_range(clip_a, &in, &out, &media_in) ==
|
||||||
|
OAKENGINE_OK);
|
||||||
|
assert(in == 0 && out == 30 && media_in == 0);
|
||||||
|
assert(oakengine_project_redo(project) == OAKENGINE_OK);
|
||||||
|
assert(oakengine_sequence_clip_count(seq, OAKENGINE_TRACK_TYPE_VIDEO,
|
||||||
|
0) == 3);
|
||||||
|
right = oakengine_sequence_clip_at(seq, OAKENGINE_TRACK_TYPE_VIDEO, 0, 1);
|
||||||
|
assert(oakengine_clip_get_range(right, &in, &out, &media_in) ==
|
||||||
|
OAKENGINE_OK);
|
||||||
|
assert(in == 15 && out == 30 && media_in == 15);
|
||||||
|
|
||||||
|
// ---- trim --------------------------------------------------------------
|
||||||
|
// Bad trim ranges.
|
||||||
|
assert(oakengine_clip_trim(NULL, 0, 10) == OAKENGINE_E_INVALID);
|
||||||
|
assert(oakengine_clip_trim(right, 30, 20) == OAKENGINE_E_INVALID);
|
||||||
|
assert(oakengine_clip_trim(right, -1, 20) == OAKENGINE_E_INVALID);
|
||||||
|
|
||||||
|
// Trim the right part's in-point 15 -> 20: media_in follows 15 -> 20.
|
||||||
|
assert(oakengine_clip_trim(right, 20, 30) == OAKENGINE_OK);
|
||||||
|
assert(oakengine_clip_get_range(right, &in, &out, &media_in) ==
|
||||||
|
OAKENGINE_OK);
|
||||||
|
assert(in == 20 && out == 30 && media_in == 20);
|
||||||
|
// The leading clip is untouched (a gap absorbs the difference).
|
||||||
|
assert(oakengine_clip_get_range(clip_a, &in, &out, &media_in) ==
|
||||||
|
OAKENGINE_OK);
|
||||||
|
assert(in == 0 && out == 15 && media_in == 0);
|
||||||
|
|
||||||
|
assert(oakengine_project_undo(project) == OAKENGINE_OK);
|
||||||
|
assert(oakengine_clip_get_range(right, &in, &out, &media_in) ==
|
||||||
|
OAKENGINE_OK);
|
||||||
|
assert(in == 15 && out == 30 && media_in == 15);
|
||||||
|
assert(oakengine_project_redo(project) == OAKENGINE_OK);
|
||||||
|
assert(oakengine_clip_get_range(right, &in, &out, &media_in) ==
|
||||||
|
OAKENGINE_OK);
|
||||||
|
assert(in == 20 && out == 30 && media_in == 20);
|
||||||
|
|
||||||
|
// Trim the out-point 30 -> 25 (media in-point stays).
|
||||||
|
assert(oakengine_clip_trim(right, 20, 25) == OAKENGINE_OK);
|
||||||
|
assert(oakengine_clip_get_range(right, &in, &out, &media_in) ==
|
||||||
|
OAKENGINE_OK);
|
||||||
|
assert(in == 20 && out == 25 && media_in == 20);
|
||||||
|
assert(oakengine_project_undo(project) == OAKENGINE_OK);
|
||||||
|
assert(oakengine_clip_get_range(right, &in, &out, &media_in) ==
|
||||||
|
OAKENGINE_OK);
|
||||||
|
assert(in == 20 && out == 30 && media_in == 20);
|
||||||
|
|
||||||
|
// No-op trim is accepted and does nothing.
|
||||||
|
assert(oakengine_clip_trim(right, 20, 30) == OAKENGINE_OK);
|
||||||
|
|
||||||
|
// Both ends in one command: [20,30] -> [22,28], media_in follows.
|
||||||
|
assert(oakengine_clip_trim(right, 22, 28) == OAKENGINE_OK);
|
||||||
|
assert(oakengine_clip_get_range(right, &in, &out, &media_in) ==
|
||||||
|
OAKENGINE_OK);
|
||||||
|
assert(in == 22 && out == 28 && media_in == 22);
|
||||||
|
assert(oakengine_project_undo(project) == OAKENGINE_OK);
|
||||||
|
assert(oakengine_clip_get_range(right, &in, &out, &media_in) ==
|
||||||
|
OAKENGINE_OK);
|
||||||
|
assert(in == 20 && out == 30 && media_in == 20);
|
||||||
|
|
||||||
|
// ---- move ---------------------------------------------------------------
|
||||||
|
assert(oakengine_sequence_move_clip(seq, OAKENGINE_TRACK_TYPE_VIDEO, 0, 1,
|
||||||
|
-1) == OAKENGINE_E_INVALID);
|
||||||
|
assert(oakengine_sequence_move_clip(seq, OAKENGINE_TRACK_TYPE_VIDEO, 0,
|
||||||
|
99, 50) == OAKENGINE_E_NOT_FOUND);
|
||||||
|
|
||||||
|
// Move the second clip [20,30] to 70 (past everything): position shifts,
|
||||||
|
// length and media in-point are preserved, the other clips stay put.
|
||||||
|
assert(oakengine_sequence_move_clip(seq, OAKENGINE_TRACK_TYPE_VIDEO, 0, 1,
|
||||||
|
70) == OAKENGINE_OK);
|
||||||
|
// After the move the moved clip is the LAST one on the track.
|
||||||
|
right = oakengine_sequence_clip_at(seq, OAKENGINE_TRACK_TYPE_VIDEO, 0, 2);
|
||||||
|
assert(oakengine_clip_get_range(right, &in, &out, &media_in) ==
|
||||||
|
OAKENGINE_OK);
|
||||||
|
assert(in == 70 && out == 80 && media_in == 20);
|
||||||
|
assert(oakengine_clip_get_range(clip_a, &in, &out, &media_in) ==
|
||||||
|
OAKENGINE_OK);
|
||||||
|
assert(in == 0 && out == 15 && media_in == 0);
|
||||||
|
assert(oakengine_clip_get_range(clip_b, &in, &out, &media_in) ==
|
||||||
|
OAKENGINE_OK);
|
||||||
|
assert(in == 30 && out == 60 && media_in == 0);
|
||||||
|
|
||||||
|
assert(oakengine_project_undo(project) == OAKENGINE_OK);
|
||||||
|
right = oakengine_sequence_clip_at(seq, OAKENGINE_TRACK_TYPE_VIDEO, 0, 1);
|
||||||
|
assert(oakengine_clip_get_range(right, &in, &out, &media_in) ==
|
||||||
|
OAKENGINE_OK);
|
||||||
|
assert(in == 20 && out == 30 && media_in == 20);
|
||||||
|
assert(oakengine_project_redo(project) == OAKENGINE_OK);
|
||||||
|
right = oakengine_sequence_clip_at(seq, OAKENGINE_TRACK_TYPE_VIDEO, 0, 2);
|
||||||
|
assert(oakengine_clip_get_range(right, &in, &out, &media_in) ==
|
||||||
|
OAKENGINE_OK);
|
||||||
|
assert(in == 70 && out == 80 && media_in == 20);
|
||||||
|
|
||||||
|
// ---- ripple delete -------------------------------------------------------
|
||||||
|
assert(oakengine_sequence_ripple_delete_clip(
|
||||||
|
seq, OAKENGINE_TRACK_TYPE_VIDEO, 0, 99) ==
|
||||||
|
OAKENGINE_E_NOT_FOUND);
|
||||||
|
assert(oakengine_sequence_ripple_delete_clip(NULL,
|
||||||
|
OAKENGINE_TRACK_TYPE_VIDEO, 0,
|
||||||
|
0) == OAKENGINE_E_INVALID);
|
||||||
|
|
||||||
|
// Delete the first clip [0,15]: both following clips shift left by 15.
|
||||||
|
assert(oakengine_sequence_ripple_delete_clip(
|
||||||
|
seq, OAKENGINE_TRACK_TYPE_VIDEO, 0, 0) == OAKENGINE_OK);
|
||||||
|
assert(oakengine_sequence_clip_count(seq, OAKENGINE_TRACK_TYPE_VIDEO,
|
||||||
|
0) == 2);
|
||||||
|
right = oakengine_sequence_clip_at(seq, OAKENGINE_TRACK_TYPE_VIDEO, 0, 0);
|
||||||
|
assert(oakengine_clip_get_range(right, &in, &out, &media_in) ==
|
||||||
|
OAKENGINE_OK);
|
||||||
|
assert(in == 15 && out == 45 && media_in == 0); // clip_b was [30,60]
|
||||||
|
right = oakengine_sequence_clip_at(seq, OAKENGINE_TRACK_TYPE_VIDEO, 0, 1);
|
||||||
|
assert(oakengine_clip_get_range(right, &in, &out, &media_in) ==
|
||||||
|
OAKENGINE_OK);
|
||||||
|
assert(in == 55 && out == 65 && media_in == 20); // was [70,80]
|
||||||
|
|
||||||
|
assert(oakengine_project_undo(project) == OAKENGINE_OK);
|
||||||
|
assert(oakengine_sequence_clip_count(seq, OAKENGINE_TRACK_TYPE_VIDEO,
|
||||||
|
0) == 3);
|
||||||
|
right = oakengine_sequence_clip_at(seq, OAKENGINE_TRACK_TYPE_VIDEO, 0, 0);
|
||||||
|
assert(oakengine_clip_get_range(right, &in, &out, &media_in) ==
|
||||||
|
OAKENGINE_OK);
|
||||||
|
assert(in == 0 && out == 15 && media_in == 0);
|
||||||
|
right = oakengine_sequence_clip_at(seq, OAKENGINE_TRACK_TYPE_VIDEO, 0, 1);
|
||||||
|
assert(oakengine_clip_get_range(right, &in, &out, &media_in) ==
|
||||||
|
OAKENGINE_OK);
|
||||||
|
assert(in == 30 && out == 60 && media_in == 0);
|
||||||
|
right = oakengine_sequence_clip_at(seq, OAKENGINE_TRACK_TYPE_VIDEO, 0, 2);
|
||||||
|
assert(oakengine_clip_get_range(right, &in, &out, &media_in) ==
|
||||||
|
OAKENGINE_OK);
|
||||||
|
assert(in == 70 && out == 80 && media_in == 20);
|
||||||
|
|
||||||
|
oakengine_footage_free(footage); // wrapper only; node stays
|
||||||
|
assert(oakengine_project_footage_count(project) == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Footage imported into one project must not be placed into another.
|
||||||
|
|
||||||
int main(void)
|
int main(void)
|
||||||
{
|
{
|
||||||
make_tmpdir();
|
make_tmpdir();
|
||||||
@@ -268,6 +469,7 @@ int main(void)
|
|||||||
test_add_track(project, seq);
|
test_add_track(project, seq);
|
||||||
test_add_clip(project, seq, path);
|
test_add_clip(project, seq, path);
|
||||||
test_cross_project_rejected(path);
|
test_cross_project_rejected(path);
|
||||||
|
test_edit_round2(path);
|
||||||
|
|
||||||
oakengine_project_free(project);
|
oakengine_project_free(project);
|
||||||
assert(oakengine_shutdown() == OAKENGINE_OK);
|
assert(oakengine_shutdown() == OAKENGINE_OK);
|
||||||
|
|||||||
Reference in New Issue
Block a user