engine: keyframe properties dialog migrates; keyframeviewundo deleted

- new primitives: keyframes_set_time_many (conflict-safe batch time
  move), keyframes_set_value_many (captured or explicit old values),
  keyframes_set_bezier_many (double precision), and
  keyframe_set_bezier_point (single handle with NaN-capture fallback)
- dialog and curveview drag finalization go through the facade; the
  tests exercise the same global undo stack via oakengine_project_undo
- keyframeview/keyframeviewundo.{h,cpp} removed with zero remaining
  references
This commit is contained in:
2026-07-20 15:41:36 +08:00
parent 8311128f4c
commit 37aa859cd0
10 changed files with 752 additions and 241 deletions
+66
View File
@@ -437,6 +437,72 @@ OAKENGINE_API int oakengine_node_keyframes_set_type_many(
OakEngineNode *self, const char *input_id, int element,
const int64_t *times_ts, const int *tracks, int count, int type);
/**
* @brief Move several keyframes of one input to `new_time_ts` (undoable,
* ONE command; olive::NodeParamSetKeyframeTimeCommand per key, like the
* application's keyframe properties dialog).
*
* Keyframes are addressed individually by (`old_times_ts`[i],
* `tracks`[i]); `element` addresses the input's array element (-1 for
* non-array). Every old address must name an existing keyframe
* (OAKENGINE_E_NOT_FOUND otherwise), and no other keyframe may already
* sit at `new_time_ts` on a target track (OAKENGINE_E_STATE; moving to
* the key's own current time is allowed). On any failure nothing is
* pushed. Returns the number of moved keyframes (>= 0) or a negative
* code.
*/
OAKENGINE_API int oakengine_node_keyframes_set_time_many(
OakEngineNode *self, const char *input_id, int element,
const int64_t *old_times_ts, const int *tracks, int count,
int64_t new_time_ts);
/**
* @brief Change the value of several keyframes of one input (undoable,
* ONE command; olive::NodeParamSetKeyframeValueCommand per key).
*
* `values`[i] is the new per-track component (mapped like
* oakengine_node_set_input_at_time(); the type must match the input's
* declared type). When `old_values` is not NULL, `old_values`[i] is
* recorded as the undo value -- for callers that already live-set the
* new values (the curve view's drag release); when NULL, each key's
* current value is captured at apply time. Every address must name an
* existing keyframe (OAKENGINE_E_NOT_FOUND; nothing pushed on failure).
* Returns the number of changed keyframes (>= 0) or a negative code.
*/
OAKENGINE_API int oakengine_node_keyframes_set_value_many(
OakEngineNode *self, const char *input_id, int element,
const int64_t *times_ts, const int *tracks, int count,
const oak_node_value *values, const oak_node_value *old_values);
/**
* @brief Set both bezier control points of several keyframes of one
* input (undoable, ONE command; two point commands per key, like the
* keyframe properties dialog). The previous points are captured per
* key. Every address must name an existing keyframe
* (OAKENGINE_E_NOT_FOUND; nothing pushed on failure). Returns the
* number of affected keyframes (>= 0) or a negative code.
*/
OAKENGINE_API int oakengine_node_keyframes_set_bezier_many(
OakEngineNode *self, const char *input_id, int element,
const int64_t *times_ts, const int *tracks, int count, double in_x,
double in_y, double out_x, double out_y);
/**
* @brief Set one bezier control point of one keyframe (undoable;
* the curve view's bezier-handle drag release).
*
* `point_index` is 0 for the in-handle and 1 for the out-handle. The
* new point is (x, y); (`old_x`, `old_y`) is the undo point recorded
* for callers that already live-set the new point during the drag --
* pass NaN for either old component to capture the key's current point
* at apply time instead. OAKENGINE_E_NOT_FOUND when no keyframe exists
* at that time/track.
*/
OAKENGINE_API int oakengine_node_keyframe_set_bezier_point(
OakEngineNode *self, const char *input_id, int element,
int64_t time_ts, int track, int point_index, double x, double y,
double old_x, double old_y);
/**
* @brief Remove all keyframes from the input (undoable,
* olive::NodeImmediateRemoveAllKeyframesCommand). A no-op (OAKENGINE_OK)
+260 -5
View File
@@ -347,9 +347,9 @@ bool kf_value_to_c(olive::NodeValue::Type type, const QVariant &component,
}
// Undo commands for easing changes. The engine's undo stack has no
// keyframe type/bezier commands -- they live in the application layer
// (app/widget/keyframeviewundo.h), so the facade carries minimal
// equivalents with the same old/new semantics.
// keyframe type/bezier commands (the application layer used to carry
// them in app/widget/keyframeviewundo.h, since migrated here), so the
// facade carries minimal equivalents with the same old/new semantics.
class KeyframeSetTypeCommand : public olive::UndoCommand {
public:
KeyframeSetTypeCommand(olive::NodeKeyframe *key,
@@ -393,6 +393,20 @@ public:
{
}
// Explicit old point for callers that already live-set the new one
// (mirrors the application class's second constructor).
KeyframeSetBezierPointCommand(olive::NodeKeyframe *key,
olive::NodeKeyframe::BezierType mode,
const QPointF &new_point,
const QPointF &old_point)
: key_(key)
, mode_(mode)
, has_old_(true)
, old_point_(old_point)
, new_point_(new_point)
{
}
virtual olive::Project *get_relevant_project() const override
{
return key_->parent() ? key_->parent()->project() : nullptr;
@@ -401,7 +415,10 @@ public:
protected:
virtual void redo() override
{
old_point_ = key_->bezier_control(mode_);
if (!has_old_) {
old_point_ = key_->bezier_control(mode_);
has_old_ = true;
}
key_->set_bezier_control(mode_, new_point_);
}
@@ -413,6 +430,7 @@ protected:
private:
olive::NodeKeyframe *key_;
olive::NodeKeyframe::BezierType mode_;
bool has_old_ = false;
QPointF old_point_;
QPointF new_point_;
};
@@ -435,6 +453,27 @@ olive::NodeValue::Type checked_keyframe_input(const olive::Node *self,
return type;
}
// Time-exact keyframe lookup that does not depend on the input's
// keyframing-enabled flag (Node::get_keyframe_at_time_on_track reports
// nothing when keyframing is off, but the application's keyframe editing
// operates on the keyframe objects regardless of the flag).
olive::NodeKeyframe *find_keyframe(const olive::Node *node,
const olive::NodeInput &input,
const olive::Rational &time, int track)
{
const QVector<olive::NodeKeyframeTrack> &tracks =
node->get_keyframe_tracks(input.input(), input.element());
if (track < 0 || track >= tracks.size()) {
return nullptr;
}
for (olive::NodeKeyframe *key : tracks.at(track)) {
if (key->time() == time) {
return key;
}
}
return nullptr;
}
} // namespace
extern "C"
@@ -1231,7 +1270,7 @@ int oakengine_node_keyframes_set_type_many(OakEngineNode *self,
const olive::Rational time =
olive::core::Timecode::timestamp_to_time(times_ts[i], tb);
olive::NodeKeyframe *key =
node->get_keyframe_at_time_on_track(input, time, tracks[i]);
find_keyframe(node, input, time, tracks[i]);
if (!key) {
set_error(QStringLiteral("no keyframe at time %1 track %2 on "
"\"%3\"")
@@ -1248,6 +1287,222 @@ int oakengine_node_keyframes_set_type_many(OakEngineNode *self,
return count;
}
int oakengine_node_keyframes_set_time_many(OakEngineNode *self,
const char *input_id, int element,
const int64_t *old_times_ts,
const int *tracks, int count,
int64_t new_time_ts)
{
set_error(QString());
olive::Node *node = impl(self);
if (checked_keyframe_input(node, input_id) == olive::NodeValue::k_none) {
return self && input_id ? OAKENGINE_E_NOT_FOUND : OAKENGINE_E_INVALID;
}
if (count < 0 || (count > 0 && (!old_times_ts || !tracks)) ||
new_time_ts < 0) {
set_error(QStringLiteral("invalid arguments"));
return OAKENGINE_E_INVALID;
}
if (count == 0) {
return 0;
}
const QString id = QString::fromUtf8(input_id);
const olive::Rational tb = project_time_base(node);
const olive::NodeInput input(node, id, element);
const olive::Rational new_time =
olive::core::Timecode::timestamp_to_time(new_time_ts, tb);
// Resolve and conflict-check every key first so a failure has no side
// effects.
olive::MultiUndoCommand *command = new olive::MultiUndoCommand();
for (int i = 0; i < count; i++) {
const olive::Rational old_time =
olive::core::Timecode::timestamp_to_time(old_times_ts[i], tb);
olive::NodeKeyframe *key =
find_keyframe(node, input, old_time, tracks[i]);
if (!key) {
set_error(QStringLiteral("no keyframe at time %1 track %2 on "
"\"%3\"")
.arg(old_times_ts[i])
.arg(tracks[i])
.arg(id));
delete command;
return OAKENGINE_E_NOT_FOUND;
}
olive::NodeKeyframe *occupant =
find_keyframe(node, input, new_time, tracks[i]);
if (occupant && occupant != key) {
set_error(QStringLiteral("a keyframe already exists at time %1 "
"on track %2")
.arg(new_time_ts)
.arg(tracks[i]));
delete command;
return OAKENGINE_E_STATE;
}
command->add_child(new olive::NodeParamSetKeyframeTimeCommand(
key, new_time, key->time()));
}
push_or_run(command, QStringLiteral("Set Keyframe Time"));
return count;
}
int oakengine_node_keyframes_set_value_many(OakEngineNode *self,
const char *input_id, int element,
const int64_t *times_ts,
const int *tracks, int count,
const oak_node_value *values,
const oak_node_value *old_values)
{
set_error(QString());
olive::Node *node = impl(self);
const olive::NodeValue::Type declared =
checked_keyframe_input(node, input_id);
if (declared == olive::NodeValue::k_none) {
return self && input_id ? OAKENGINE_E_NOT_FOUND : OAKENGINE_E_INVALID;
}
if (count < 0 || (count > 0 && (!times_ts || !tracks || !values))) {
set_error(QStringLiteral("invalid arguments"));
return OAKENGINE_E_INVALID;
}
if (count == 0) {
return 0;
}
const QString id = QString::fromUtf8(input_id);
const olive::Rational tb = project_time_base(node);
const olive::NodeInput input(node, id, element);
olive::MultiUndoCommand *command = new olive::MultiUndoCommand();
for (int i = 0; i < count; i++) {
const olive::Rational time =
olive::core::Timecode::timestamp_to_time(times_ts[i], tb);
olive::NodeKeyframe *key =
find_keyframe(node, input, time, tracks[i]);
if (!key) {
set_error(QStringLiteral("no keyframe at time %1 track %2 on "
"\"%3\"")
.arg(times_ts[i])
.arg(tracks[i])
.arg(id));
delete command;
return OAKENGINE_E_NOT_FOUND;
}
QVariant new_value;
if (!component_from_c(&values[i], declared, 0, &new_value)) {
set_error(QStringLiteral(
"value type does not match the declared input type"));
delete command;
return OAKENGINE_E_INVALID;
}
if (old_values) {
QVariant old_value;
if (!component_from_c(&old_values[i], declared, 0, &old_value)) {
set_error(QStringLiteral(
"old value type does not match the declared input type"));
delete command;
return OAKENGINE_E_INVALID;
}
command->add_child(new olive::NodeParamSetKeyframeValueCommand(
key, new_value, old_value));
} else {
command->add_child(new olive::NodeParamSetKeyframeValueCommand(
key, new_value, key->value()));
}
}
push_or_run(command, QStringLiteral("Set Keyframe Value"));
return count;
}
int oakengine_node_keyframes_set_bezier_many(OakEngineNode *self,
const char *input_id,
int element,
const int64_t *times_ts,
const int *tracks, int count,
double in_x, double in_y,
double out_x, double out_y)
{
set_error(QString());
olive::Node *node = impl(self);
if (checked_keyframe_input(node, input_id) == olive::NodeValue::k_none) {
return self && input_id ? OAKENGINE_E_NOT_FOUND : OAKENGINE_E_INVALID;
}
if (count < 0 || (count > 0 && (!times_ts || !tracks))) {
set_error(QStringLiteral("invalid arguments"));
return OAKENGINE_E_INVALID;
}
if (count == 0) {
return 0;
}
const QString id = QString::fromUtf8(input_id);
const olive::Rational tb = project_time_base(node);
const olive::NodeInput input(node, id, element);
olive::MultiUndoCommand *command = new olive::MultiUndoCommand();
for (int i = 0; i < count; i++) {
const olive::Rational time =
olive::core::Timecode::timestamp_to_time(times_ts[i], tb);
olive::NodeKeyframe *key =
find_keyframe(node, input, time, tracks[i]);
if (!key) {
set_error(QStringLiteral("no keyframe at time %1 track %2 on "
"\"%3\"")
.arg(times_ts[i])
.arg(tracks[i])
.arg(id));
delete command;
return OAKENGINE_E_NOT_FOUND;
}
command->add_child(new KeyframeSetBezierPointCommand(
key, olive::NodeKeyframe::k_in_handle, QPointF(in_x, in_y)));
command->add_child(new KeyframeSetBezierPointCommand(
key, olive::NodeKeyframe::k_out_handle, QPointF(out_x, out_y)));
}
push_or_run(command, QStringLiteral("Set Keyframe Bezier Points"));
return count;
}
int oakengine_node_keyframe_set_bezier_point(
OakEngineNode *self, const char *input_id, int element, int64_t time_ts,
int track, int point_index, double x, double y, double old_x,
double old_y)
{
set_error(QString());
olive::Node *node = impl(self);
if (checked_keyframe_input(node, input_id) == olive::NodeValue::k_none) {
return self && input_id ? OAKENGINE_E_NOT_FOUND : OAKENGINE_E_INVALID;
}
if (point_index < 0 || point_index > 1) {
set_error(QStringLiteral("invalid bezier point index %1")
.arg(point_index));
return OAKENGINE_E_INVALID;
}
const QString id = QString::fromUtf8(input_id);
const olive::Rational tb = project_time_base(node);
const olive::Rational time =
olive::core::Timecode::timestamp_to_time(time_ts, tb);
olive::NodeKeyframe *key = find_keyframe(
node, olive::NodeInput(node, id, element), time, track);
if (!key) {
set_error(QStringLiteral("no keyframe at time %1 track %2 on \"%3\"")
.arg(time_ts)
.arg(track)
.arg(id));
return OAKENGINE_E_NOT_FOUND;
}
const olive::NodeKeyframe::BezierType mode =
(point_index == 0) ? olive::NodeKeyframe::k_in_handle :
olive::NodeKeyframe::k_out_handle;
if (std::isnan(old_x) || std::isnan(old_y)) {
push_or_run(new KeyframeSetBezierPointCommand(key, mode,
QPointF(x, y)),
QStringLiteral("Set Keyframe Bezier Point"));
} else {
push_or_run(new KeyframeSetBezierPointCommand(
key, mode, QPointF(x, y), QPointF(old_x, old_y)),
QStringLiteral("Set Keyframe Bezier Point"));
}
return OAKENGINE_OK;
}
int oakengine_node_keyframes_clear(OakEngineNode *self, const char *input_id)
{
set_error(QString());
+145
View File
@@ -418,6 +418,150 @@ static void test_panel_paths(OakEngineProject *project,
assert(oakengine_node_keyframe_count(opacity, "opacity_in") == 3);
}
static void test_keyframe_properties(OakEngineProject *project,
OakEngineNode *opacity)
{
// `opacity` has three keys at 0/15/30 (values 0.0/0.5/1.0) with
// easings hold/bezier/linear from the earlier tests.
oak_node_value out;
int64_t ts = -1;
const int tr1[1] = { 0 };
// set_time_many: move 15 -> 20 and undo.
const int64_t olds[1] = { 15 };
assert(oakengine_node_keyframes_set_time_many(opacity, "opacity_in", -1,
olds, tr1, 1, 20) == 1);
assert(oakengine_node_keyframe_at(opacity, "opacity_in", 1, &ts, NULL) ==
OAKENGINE_OK);
assert(ts == 20);
assert(oakengine_project_undo(project) == OAKENGINE_OK);
assert(oakengine_node_keyframe_at(opacity, "opacity_in", 1, &ts, NULL) ==
OAKENGINE_OK);
assert(ts == 15);
// Moving onto an occupied time is a conflict; onto the key's own time
// is allowed; a missing key is NOT_FOUND. Failures push nothing.
assert(oakengine_node_keyframes_set_time_many(opacity, "opacity_in", -1,
olds, tr1, 1,
30) == OAKENGINE_E_STATE);
assert(oakengine_node_keyframes_set_time_many(opacity, "opacity_in", -1,
olds, tr1, 1, 15) == 1);
assert(oakengine_project_undo(project) == OAKENGINE_OK);
const int64_t miss[1] = { 99 };
assert(oakengine_node_keyframes_set_time_many(opacity, "opacity_in", -1,
miss, tr1, 1,
40) == OAKENGINE_E_NOT_FOUND);
assert(oakengine_node_keyframes_set_time_many(NULL, "opacity_in", -1,
olds, tr1, 1,
40) == OAKENGINE_E_INVALID);
assert(oakengine_node_keyframes_set_time_many(opacity, "opacity_in", -1,
olds, tr1, 0, 40) == 0);
assert(oakengine_node_keyframe_count(opacity, "opacity_in") == 3);
// set_value_many: captured old values are restored by undo.
const int64_t times2[2] = { 0, 15 };
const int tr2[2] = { 0, 0 };
oak_node_value nv[2];
nv[0] = float_value(0.7);
nv[1] = float_value(0.6);
assert(oakengine_node_keyframes_set_value_many(opacity, "opacity_in", -1,
times2, tr2, 2, nv,
NULL) == 2);
assert(oakengine_node_keyframe_at(opacity, "opacity_in", 0, &ts, &out) ==
OAKENGINE_OK);
assert(fabs(out.f[0] - 0.7) < 1e-9);
assert(oakengine_node_keyframe_at(opacity, "opacity_in", 1, &ts, &out) ==
OAKENGINE_OK);
assert(fabs(out.f[0] - 0.6) < 1e-9);
assert(oakengine_project_undo(project) == OAKENGINE_OK);
assert(oakengine_node_keyframe_at(opacity, "opacity_in", 0, &ts, &out) ==
OAKENGINE_OK);
assert(fabs(out.f[0] - 0.0) < 1e-9);
assert(oakengine_node_keyframe_at(opacity, "opacity_in", 1, &ts, &out) ==
OAKENGINE_OK);
assert(fabs(out.f[0] - 0.5) < 1e-9);
// Explicit old values (the live-set drag pattern): undo restores the
// recorded old, redo the new.
oak_node_value ov[2];
ov[0] = float_value(9.9);
ov[1] = float_value(9.8);
assert(oakengine_node_keyframes_set_value_many(opacity, "opacity_in", -1,
times2, tr2, 2, nv,
ov) == 2);
assert(oakengine_project_undo(project) == OAKENGINE_OK);
assert(oakengine_node_keyframe_at(opacity, "opacity_in", 0, &ts, &out) ==
OAKENGINE_OK);
assert(fabs(out.f[0] - 9.9) < 1e-9);
assert(oakengine_project_redo(project) == OAKENGINE_OK);
oak_node_value orig[2];
orig[0] = float_value(0.0);
orig[1] = float_value(0.5);
assert(oakengine_node_keyframes_set_value_many(opacity, "opacity_in", -1,
times2, tr2, 2, orig,
NULL) == 2);
// bezier_many: both control points on both keys, undo restores each
// key's own previous points (key 15 had 0.1/0.2/0.3/0.4).
assert(oakengine_node_keyframes_set_bezier_many(
opacity, "opacity_in", -1, times2, tr2, 2, 0.11f, 0.22f, 0.33f,
0.44f) == 2);
float x1 = 0, y1 = 0, x2 = 0, y2 = 0;
int type = -1;
assert(oakengine_node_keyframe_get_easing(opacity, "opacity_in", 1, &x1,
&y1, &x2, &y2,
&type) == OAKENGINE_OK);
assert(fabsf(x1 - 0.11f) < 1e-6f && fabsf(y1 - 0.22f) < 1e-6f &&
fabsf(x2 - 0.33f) < 1e-6f && fabsf(y2 - 0.44f) < 1e-6f);
assert(oakengine_project_undo(project) == OAKENGINE_OK);
assert(oakengine_node_keyframe_get_easing(opacity, "opacity_in", 1, &x1,
&y1, &x2, &y2,
&type) == OAKENGINE_OK);
assert(fabsf(x1 - 0.1f) < 1e-6f && fabsf(y1 - 0.2f) < 1e-6f);
assert(oakengine_node_keyframes_set_bezier_many(
opacity, "opacity_in", -1, miss, tr1, 1, 0.f, 0.f, 0.f,
0.f) == OAKENGINE_E_NOT_FOUND);
// set_bezier_point: NaN old captures the current point.
assert(oakengine_node_keyframe_set_bezier_point(
opacity, "opacity_in", -1, 15, 0, 0, 0.5f, 0.6f, NAN,
NAN) == OAKENGINE_OK);
assert(oakengine_node_keyframe_get_easing(opacity, "opacity_in", 1, &x1,
&y1, &x2, &y2,
&type) == OAKENGINE_OK);
assert(fabsf(x1 - 0.5f) < 1e-6f && fabsf(y1 - 0.6f) < 1e-6f);
assert(oakengine_project_undo(project) == OAKENGINE_OK);
assert(oakengine_node_keyframe_get_easing(opacity, "opacity_in", 1, &x1,
&y1, &x2, &y2,
&type) == OAKENGINE_OK);
assert(fabsf(x1 - 0.1f) < 1e-6f && fabsf(y1 - 0.2f) < 1e-6f);
// Explicit old restores the recorded point on undo.
assert(oakengine_node_keyframe_set_bezier_point(
opacity, "opacity_in", -1, 15, 0, 0, 0.7f, 0.8f, 9.0f,
9.0f) == OAKENGINE_OK);
assert(oakengine_project_undo(project) == OAKENGINE_OK);
assert(oakengine_node_keyframe_get_easing(opacity, "opacity_in", 1, &x1,
&y1, &x2, &y2,
&type) == OAKENGINE_OK);
assert(fabsf(x1 - 9.0f) < 1e-6f);
// Put the point back so later state matches the earlier tests.
assert(oakengine_node_keyframe_set_bezier_point(
opacity, "opacity_in", -1, 15, 0, 0, 0.1f, 0.2f, NAN,
NAN) == OAKENGINE_OK);
// Errors: bad point index, missing key, NULL node.
assert(oakengine_node_keyframe_set_bezier_point(
opacity, "opacity_in", -1, 15, 0, 2, 0.f, 0.f, 0.f,
0.f) == OAKENGINE_E_INVALID);
assert(oakengine_node_keyframe_set_bezier_point(
opacity, "opacity_in", -1, 99, 0, 0, 0.f, 0.f, 0.f,
0.f) == OAKENGINE_E_NOT_FOUND);
assert(oakengine_node_keyframe_set_bezier_point(
NULL, "opacity_in", -1, 15, 0, 0, 0.f, 0.f, 0.f,
0.f) == OAKENGINE_E_INVALID);
}
int main(void)
{
make_tmpdir();
@@ -452,6 +596,7 @@ int main(void)
test_easing_and_remove(project, opacity);
test_rational_and_color(project, timeremap, solid);
test_panel_paths(project, opacity, solid);
test_keyframe_properties(project, opacity);
oakengine_project_free(project);
assert(oakengine_shutdown() == OAKENGINE_OK);