diff --git a/app/node/audio/pan/pan.cpp b/app/node/audio/pan/pan.cpp index f09077296..44386de1a 100644 --- a/app/node/audio/pan/pan.cpp +++ b/app/node/audio/pan/pan.cpp @@ -2,15 +2,12 @@ PanNode::PanNode() { - samples_input_ = new NodeInput("samples_in"); - samples_input_->set_data_type(NodeParam::kSamples); + samples_input_ = new NodeInput("samples_in", NodeParam::kSamples); AddInput(samples_input_); - panning_input_ = new NodeInput("panning_in"); - panning_input_->set_data_type(NodeParam::kFloat); + panning_input_ = new NodeInput("panning_in", NodeParam::kFloat); panning_input_->set_minimum(-1); panning_input_->set_maximum(1); - panning_input_->set_value_at_time(0, 0); AddInput(panning_input_); } diff --git a/app/node/audio/volume/volume.cpp b/app/node/audio/volume/volume.cpp index b2d19899b..143e24dc8 100644 --- a/app/node/audio/volume/volume.cpp +++ b/app/node/audio/volume/volume.cpp @@ -2,15 +2,12 @@ VolumeNode::VolumeNode() { - samples_input_ = new NodeInput("samples_in"); - samples_input_->set_data_type(NodeParam::kSamples); + samples_input_ = new NodeInput("samples_in", NodeParam::kSamples); AddInput(samples_input_); - volume_input_ = new NodeInput("volume_in"); - volume_input_->set_data_type(NodeParam::kFloat); + volume_input_ = new NodeInput("volume_in", NodeParam::kFloat, 1); volume_input_->set_minimum(0); volume_input_->set_maximum(1); - volume_input_->set_value_at_time(0, 1); AddInput(volume_input_); } diff --git a/app/node/block/block.cpp b/app/node/block/block.cpp index 2ba6046a9..efe476cc3 100644 --- a/app/node/block/block.cpp +++ b/app/node/block/block.cpp @@ -28,20 +28,20 @@ Block::Block() : previous_(nullptr), next_(nullptr) { - length_input_ = new NodeInput("length_in"); + length_input_ = new NodeInput("length_in", NodeParam::kRational); length_input_->SetConnectable(false); - length_input_->set_data_type(NodeParam::kRational); + length_input_->set_is_keyframable(false); AddInput(length_input_); connect(length_input_, SIGNAL(ValueChanged(const rational&, const rational&)), this, SLOT(LengthInputChanged())); - media_in_input_ = new NodeInput("media_in_in"); + media_in_input_ = new NodeInput("media_in_in", NodeParam::kRational); media_in_input_->SetConnectable(false); - media_in_input_->set_data_type(NodeParam::kRational); + media_in_input_->set_is_keyframable(false); AddInput(media_in_input_); - media_out_input_ = new NodeInput("media_out_in"); + media_out_input_ = new NodeInput("media_out_in", NodeParam::kRational); media_out_input_->SetConnectable(false); - media_out_input_->set_data_type(NodeParam::kRational); + media_out_input_->set_is_keyframable(false); AddInput(media_out_input_); // A block's length must be greater than 0 @@ -86,7 +86,7 @@ void Block::set_length(const rational &length) return; } - length_input_->set_value_at_time(0, QVariant::fromValue(length)); + length_input_->set_override_value(QVariant::fromValue(length)); } void Block::set_length_and_media_out(const rational &length) @@ -158,7 +158,7 @@ rational Block::media_in() const void Block::set_media_in(const rational &media_in) { - media_in_input_->set_value_at_time(0, QVariant::fromValue(media_in)); + media_in_input_->set_override_value(QVariant::fromValue(media_in)); } rational Block::media_out() const @@ -168,7 +168,7 @@ rational Block::media_out() const void Block::set_media_out(const rational &media_out) { - media_out_input_->set_value_at_time(0, QVariant::fromValue(media_out)); + media_out_input_->set_override_value(QVariant::fromValue(media_out)); } rational Block::media_length() const diff --git a/app/node/block/clip/clip.cpp b/app/node/block/clip/clip.cpp index 3aad277f9..6517757c8 100644 --- a/app/node/block/clip/clip.cpp +++ b/app/node/block/clip/clip.cpp @@ -22,8 +22,7 @@ ClipBlock::ClipBlock() { - texture_input_ = new NodeInput("buffer_in"); - texture_input_->set_data_type(NodeInput::kBuffer); + texture_input_ = new NodeInput("buffer_in", NodeInput::kBuffer); AddInput(texture_input_); } diff --git a/app/node/block/transition/transition.cpp b/app/node/block/transition/transition.cpp index f6237fc45..eec961f78 100644 --- a/app/node/block/transition/transition.cpp +++ b/app/node/block/transition/transition.cpp @@ -2,12 +2,10 @@ TransitionBlock::TransitionBlock() { - out_block_input_ = new NodeInput("out_block_in"); - out_block_input_->set_data_type(NodeParam::kBuffer); + out_block_input_ = new NodeInput("out_block_in", NodeParam::kBuffer); AddInput(out_block_input_); - in_block_input_ = new NodeInput("in_block_in"); - in_block_input_->set_data_type(NodeParam::kBuffer); + in_block_input_ = new NodeInput("in_block_in", NodeParam::kBuffer); AddInput(in_block_input_); // A block's length must be greater than 0 diff --git a/app/node/distort/transform/transform.cpp b/app/node/distort/transform/transform.cpp index fba2d2a35..098918f26 100644 --- a/app/node/distort/transform/transform.cpp +++ b/app/node/distort/transform/transform.cpp @@ -25,21 +25,16 @@ TransformDistort::TransformDistort() { - position_input_ = new NodeInput("pos_in"); - position_input_->set_data_type(NodeParam::kVec2); + position_input_ = new NodeInput("pos_in", NodeParam::kVec2); AddInput(position_input_); - rotation_input_ = new NodeInput("rot_in"); - rotation_input_->set_data_type(NodeParam::kFloat); + rotation_input_ = new NodeInput("rot_in", NodeParam::kFloat); AddInput(rotation_input_); - scale_input_ = new NodeInput("scale_in"); - scale_input_->set_data_type(NodeParam::kVec2); - scale_input_->set_value_at_time(0, QVector2D(100.0f, 100.0f)); + scale_input_ = new NodeInput("scale_in", NodeParam::kVec2, QVector2D(100.0f, 100.0f)); AddInput(scale_input_); - anchor_input_ = new NodeInput("anchor_in"); - anchor_input_->set_data_type(NodeParam::kVec2); + anchor_input_ = new NodeInput("anchor_in", NodeParam::kVec2); AddInput(anchor_input_); } diff --git a/app/node/input.cpp b/app/node/input.cpp index e7bbf73fc..1160c71cc 100644 --- a/app/node/input.cpp +++ b/app/node/input.cpp @@ -25,8 +25,9 @@ #include "output.h" #include "inputarray.h" -NodeInput::NodeInput(const QString& id) : +NodeInput::NodeInput(const QString& id, const DataType &type, const QVariant &default_value) : NodeParam(id), + data_type_(type), keyframable_(true), keyframing_(false), dependent_(true), @@ -34,7 +35,7 @@ NodeInput::NodeInput(const QString& id) : has_maximum_(false) { // Have at least one keyframe/value active at any time - keyframes_.append(NodeKeyframe()); + insert_keyframe(std::make_shared(0, default_value, NodeKeyframe::kLinear)); } bool NodeInput::IsArray() @@ -61,11 +62,6 @@ const NodeParam::DataType &NodeInput::data_type() const return data_type_; } -void NodeInput::set_data_type(const NodeParam::DataType &type) -{ - data_type_ = type; -} - NodeOutput *NodeInput::get_connected_output() const { if (!edges_.isEmpty()) { @@ -89,41 +85,41 @@ Node *NodeInput::get_connected_node() const QVariant NodeInput::get_value_at_time(const rational &time) const { if (is_keyframing()) { - if (keyframes_.first().time() >= time) { + if (keyframes_.first()->time() >= time) { // This time precedes any keyframe, so we just return the first value - return keyframes_.first().value(); + return keyframes_.first()->value(); } - if (keyframes_.last().time() <= time) { + if (keyframes_.last()->time() <= time) { // This time is after any keyframes so we return the last value - return keyframes_.last().value(); + return keyframes_.last()->value(); } // If we're here, the time must be somewhere in between the keyframes for (int i=0;itime() == time || data_type() != kFloat // FIXME: Expand this to other types that can be interpolated - || (before.time() < time && before.type() == NodeKeyframe::kHold)) { + || (before->time() < time && before->type() == NodeKeyframe::kHold)) { // Time == keyframe time, so value is precise - return before.value(); + return before->value(); - } else if (before.time() < time && after.time() > time) { + } else if (before->time() < time && after->time() > time) { // We must interpolate between these keyframes - if (before.type() == NodeKeyframe::kBezier && after.type() == NodeKeyframe::kBezier) { + if (before->type() == NodeKeyframe::kBezier && after->type() == NodeKeyframe::kBezier) { // FIXME: Perform a cubic bezier interpolation - } else if (before.type() == NodeKeyframe::kLinear && after.type() == NodeKeyframe::kBezier) { + } else if (before->type() == NodeKeyframe::kLinear && after->type() == NodeKeyframe::kBezier) { // FIXME: Perform a quadratic bezier interpolation with anchors from the AFTER keyframe - } else if (before.type() == NodeKeyframe::kLinear && after.type() == NodeKeyframe::kBezier) { + } else if (before->type() == NodeKeyframe::kLinear && after->type() == NodeKeyframe::kBezier) { // FIXME: Perform a quadratic bezier interpolation with anchors from the BEFORE keyframe } else { // To have arrived here, the keyframes must both be linear - qreal period_progress = (time.toDouble() - before.time().toDouble()) / (after.time().toDouble() - before.time().toDouble()); - qreal interpolated_value = lerp(before.value().toDouble(), after.value().toDouble(), period_progress); + qreal period_progress = (time.toDouble() - before->time().toDouble()) / (after->time().toDouble() - before->time().toDouble()); + qreal interpolated_value = lerp(before->value().toDouble(), after->value().toDouble(), period_progress); return interpolated_value; } @@ -131,14 +127,167 @@ QVariant NodeInput::get_value_at_time(const rational &time) const } } - return keyframes_.first().value(); + return keyframes_.first()->value(); } -void NodeInput::set_value_at_time(const rational &time, const QVariant &value) +NodeKeyframePtr NodeInput::get_keyframe_at_time(const rational &time) const +{ + if (!is_keyframing()) { + return nullptr; + } + + foreach (NodeKeyframePtr key, keyframes_) { + if (key->time() == time) { + return key; + } + } + + return nullptr; +} + +NodeKeyframePtr NodeInput::get_closest_keyframe_to_time(const rational &time) const +{ + if (!is_keyframing()) { + return nullptr; + } + + if (time <= keyframes_.first()->time()) { + return keyframes_.first(); + } + + if (time >= keyframes_.last()->time()) { + return keyframes_.last(); + } + + for (int i=1;itime() <= time && next_key->time() >= time) { + // Return whichever is closer + rational prev_diff = time - prev_key->time(); + rational next_diff = next_key->time() - time; + + if (next_diff < prev_diff) { + return next_key; + } else { + return prev_key; + } + } + } + + return nullptr; +} + +void NodeInput::insert_keyframe(NodeKeyframePtr key) +{ + insert_keyframe_internal(key); + + connect(key.get(), &NodeKeyframe::TimeChanged, this, &NodeInput::KeyframeTimeChanged); + connect(key.get(), &NodeKeyframe::ValueChanged, this, &NodeInput::KeyframeValueChanged); + + emit KeyframeAdded(key); +} + +void NodeInput::remove_keyframe(NodeKeyframePtr key) +{ + Q_ASSERT(keyframes_.size() > 1); + + disconnect(key.get(), &NodeKeyframe::TimeChanged, this, &NodeInput::KeyframeTimeChanged); + disconnect(key.get(), &NodeKeyframe::ValueChanged, this, &NodeInput::KeyframeValueChanged); + + keyframes_.removeOne(key); + + emit KeyframeRemoved(key); +} + +void NodeInput::KeyframeTimeChanged() +{ + NodeKeyframe* key = static_cast(sender()); + int keyframe_index = FindIndexOfKeyframeFromRawPtr(key); + + Q_ASSERT(keyframe_index > -1); + + if ((keyframe_index > 0 && keyframes_.at(keyframe_index - 1)->time() > key->time()) + || (keyframe_index < keyframes_.size() - 1 && keyframes_.at(keyframe_index + 1)->time() < key->time())) { + // This keyframe needs resorting, store it and remove it from the list + NodeKeyframePtr key_shared_ptr = keyframes_.at(keyframe_index); + keyframes_.removeAt(keyframe_index); + + // Automatically insertion sort + insert_keyframe_internal(key_shared_ptr); + } +} + +void NodeInput::KeyframeValueChanged() +{ + //NodeKeyframe* key = static_cast(sender()); + //int keyframe_index = FindIndexOfKeyframeFromRawPtr(key); + + // FIXME: Finish this partial stub + emit ValueChanged(RATIONAL_MIN, RATIONAL_MAX); +} + +int NodeInput::FindIndexOfKeyframeFromRawPtr(NodeKeyframe *raw_ptr) const +{ + for (int i=0;itime() != key->time()); + + if (compare->time() > key->time()) { + keyframes_.insert(i, key); + return; + } + } + + keyframes_.append(key); +} + +bool NodeInput::has_keyframe_at_time(const rational &time) const +{ + // If we aren't keyframing, there definitely isn't a keyframe at a given time + if (!is_keyframing()) { + return false; + } + + // Loop through keyframes to see if any match + foreach (NodeKeyframePtr key, keyframes_) { + if (key->time() == time) { + return true; + } + } + + // None match + return false; +} + +/*void NodeInput::set_default_value(const QVariant &value) { if (parentNode() != nullptr) parentNode()->LockUserInput(); + set_is_keyframing(false); + keyframes_.clear(); + keyframes_.append(NodeKeyframe(0, value, NodeKeyframe::kLinear)); + + if (parentNode() != nullptr) + parentNode()->UnlockUserInput(); + + emit ValueChanged(RATIONAL_MIN, RATIONAL_MAX); + // We set up these variables in advance (see end of function) bool signal_vc = false; TimeRange signal_vc_range; @@ -230,8 +379,49 @@ void NodeInput::set_value_at_time(const rational &time, const QVariant &value) // tries to relock this node before it's unlocked here if (signal_vc) emit ValueChanged(signal_vc_range.in(), signal_vc_range.out()); + +}*/ + +/*void NodeInput::set_keyframe_data(const QList keyframes) +{ + keyframes_ = keyframes; } +void NodeInput::insert_keyframe(const NodeKeyframe &key) +{ + Q_ASSERT(is_keyframing()); + + for (int i=0;i key.time()) { + keyframes_.insert(i, key); + emit KeyframeAdded(key); + return; + } + } + + keyframes_.append(key); + emit KeyframeAdded(key); +} + +void NodeInput::remove_keyframe_at_time(const rational &time) +{ + for (int i=0;iset_value(value); +} + +const QList &NodeInput::keyframes() const +{ + return keyframes_; +} + void NodeInput::set_is_keyframable(bool k) { keyframable_ = k; diff --git a/app/node/input.h b/app/node/input.h index 35b967f5a..1b8b9fb6d 100644 --- a/app/node/input.h +++ b/app/node/input.h @@ -40,7 +40,7 @@ public: * saving/loading data from this Node so that parameter order can be changed without issues loading data saved by an * older version. This of course assumes that parameters don't change their ID. */ - NodeInput(const QString &id); + NodeInput(const QString &id, const DataType& type, const QVariant& default_value = 0); virtual bool IsArray(); @@ -58,7 +58,6 @@ public: * connected to it. */ const DataType& data_type() const; - void set_data_type(const DataType& type); /** * @brief If this input is connected to an output, retrieve the output parameter @@ -84,9 +83,37 @@ public: QVariant get_value_at_time(const rational& time) const; /** - * @brief Sets what value should be seen at a specific time + * @brief Retrieve the keyframe object at a given time + * + * @return + * + * The keyframe object at this time or nullptr if there isn't one or if is_keyframing() is false. */ - void set_value_at_time(const rational& time, const QVariant& value); + NodeKeyframePtr get_keyframe_at_time(const rational& time) const; + + /** + * @brief Gets the closest keyframe to a time + * + * If is_keyframing() is false, this will return nullptr. Otherwise, this is guaranteed to return a keyframe. + */ + NodeKeyframePtr get_closest_keyframe_to_time(const rational& time) const; + + /** + * @brief Inserts a keyframe at the given time and returns a reference to it + */ + void insert_keyframe(NodeKeyframePtr key); + + /** + * @brief Removes the keyframe + */ + void remove_keyframe(NodeKeyframePtr key); + + /** + * @brief Return whether a keyframe exists at this time + * + * If is_keyframing() is false, this will always return false. + */ + bool has_keyframe_at_time(const rational &time) const; /** * @brief Return whether keyframing is enabled on this input or not @@ -103,6 +130,18 @@ public: */ bool is_keyframable() const; + /** + * @brief Replaces all values with this value + * + * This can only be used on inputs where keyframing is disabled. + */ + void set_override_value(const QVariant& value); + + /** + * @brief Return list of keyframes in this parameter + */ + const QList& keyframes() const; + /** * @brief Set whether this input can be keyframed or not */ @@ -124,7 +163,24 @@ public: signals: void ValueChanged(const rational& start, const rational& end); + void KeyframeEnableChanged(bool); + + void KeyframeAdded(NodeKeyframePtr key); + + void KeyframeRemoved(NodeKeyframePtr key); + private: + /** + * @brief We use Qt signals/slots for keyframe communication but store them as shared ptrs. This function converts + * a raw ptr to a list index + */ + int FindIndexOfKeyframeFromRawPtr(NodeKeyframe* raw_ptr) const; + + /** + * @brief Internal insert function, automatically does an insertion sort based on the keyframe's time + */ + void insert_keyframe_internal(NodeKeyframePtr key); + /** * @brief Internal list of accepted data types * @@ -143,7 +199,7 @@ private: * All internal/user-defined data is stored in this array. Even if keyframing is not enabled, this array will contain * one entry which will be used, and its time value will be ignored. */ - QList keyframes_; + QList keyframes_; /** * @brief Internal keyframing enabled setting @@ -175,6 +231,17 @@ private: */ QVariant maximum_; +private slots: + /** + * @brief Slot when a keyframe's time changes to keep the keyframes correctly sorted by time + */ + void KeyframeTimeChanged(); + + /** + * @brief Slot when a keyframe's value changes to signal that the cache needs updating + */ + void KeyframeValueChanged(); + }; #endif // NODEINPUT_H diff --git a/app/node/input/media/media.cpp b/app/node/input/media/media.cpp index eaa07d801..624068e13 100644 --- a/app/node/input/media/media.cpp +++ b/app/node/input/media/media.cpp @@ -23,9 +23,9 @@ MediaInput::MediaInput() : connected_footage_(nullptr) { - footage_input_ = new NodeInput("footage_in"); - footage_input_->set_data_type(NodeInput::kFootage); + footage_input_ = new NodeInput("footage_in", NodeInput::kFootage); footage_input_->SetConnectable(false); + footage_input_->set_is_keyframable(false); connect(footage_input_, SIGNAL(ValueChanged(const rational&, const rational&)), this, SLOT(FootageChanged())); AddInput(footage_input_); } @@ -37,7 +37,7 @@ StreamPtr MediaInput::footage() void MediaInput::SetFootage(StreamPtr f) { - footage_input_->set_value_at_time(0, QVariant::fromValue(f)); + footage_input_->set_override_value(QVariant::fromValue(f)); } void MediaInput::Retranslate() diff --git a/app/node/input/media/video/video.cpp b/app/node/input/media/video/video.cpp index 002892921..abc5bf9bd 100644 --- a/app/node/input/media/video/video.cpp +++ b/app/node/input/media/video/video.cpp @@ -11,8 +11,7 @@ VideoInput::VideoInput() { - matrix_input_ = new NodeInput("matrix_in"); - matrix_input_->set_data_type(NodeInput::kMatrix); + matrix_input_ = new NodeInput("matrix_in", NodeInput::kMatrix); AddInput(matrix_input_); } diff --git a/app/node/inputarray.cpp b/app/node/inputarray.cpp index af5680bb7..413b1d6fc 100644 --- a/app/node/inputarray.cpp +++ b/app/node/inputarray.cpp @@ -2,8 +2,9 @@ #include "node.h" -NodeInputArray::NodeInputArray(const QString &id) : - NodeInput(id) +NodeInputArray::NodeInputArray(const QString &id, const DataType &type, const QVariant &default_value) : + NodeInput(id, type, default_value), + default_value_(default_value) { } @@ -51,7 +52,7 @@ void NodeInputArray::SetSize(int size, bool lock) Q_ASSERT(!parentNode()->HasParamWithID(sub_id)); - NodeInput* new_param = new NodeInput(sub_id); + NodeInput* new_param = new NodeInput(sub_id, data_type(), default_value_); new_param->setParent(this); sub_params_.replace(i, new_param); diff --git a/app/node/inputarray.h b/app/node/inputarray.h index 51035d7df..02f0eb125 100644 --- a/app/node/inputarray.h +++ b/app/node/inputarray.h @@ -7,7 +7,7 @@ class NodeInputArray : public NodeInput { Q_OBJECT public: - NodeInputArray(const QString &id); + NodeInputArray(const QString &id, const DataType& type, const QVariant& default_value = 0); virtual bool IsArray() override; @@ -34,6 +34,8 @@ signals: private: QVector sub_params_; + QVariant default_value_; + }; #endif // INPUTARRAY_H diff --git a/app/node/metareader.cpp b/app/node/metareader.cpp index 36e50b377..aeba47dbb 100644 --- a/app/node/metareader.cpp +++ b/app/node/metareader.cpp @@ -193,12 +193,11 @@ void NodeMetaReader::XMLReadParam(QXmlStreamReader *reader) return; } - NodeInput* input = new NodeInput(param_id); - input->set_data_type(param_type); - - if (is_iterative) { - iteration_input_ = input; - } + QVariant default_val; + QVariant min_val; + bool has_min = false; + QVariant max_val; + bool has_max = false; // Traverse through param contents for more data while (!reader->atEnd() && !(reader->isEndElement() && reader->name() == "param")) { @@ -218,15 +217,31 @@ void NodeMetaReader::XMLReadParam(QXmlStreamReader *reader) XMLReadLanguageString(reader, ¶m_names_[param_id]); } else if (reader->name() == "default") { - input->set_value_at_time(0, reader->readElementText()); + default_val = reader->readElementText(); } else if (reader->name() == "min") { - input->set_minimum(reader->readElementText()); + has_min = true; + min_val = reader->readElementText(); } else if (reader->name() == "max") { - input->set_maximum(reader->readElementText()); + has_max = true; + max_val = reader->readElementText(); } } } + NodeInput* input = new NodeInput(param_id, param_type, default_val); + + if (has_min) { + input->set_minimum(min_val); + } + + if (has_max) { + input->set_maximum(max_val); + } + + if (is_iterative) { + iteration_input_ = input; + } + inputs_.append(input); } diff --git a/app/node/output/timeline/timeline.cpp b/app/node/output/timeline/timeline.cpp index 03603c403..59f79c40e 100644 --- a/app/node/output/timeline/timeline.cpp +++ b/app/node/output/timeline/timeline.cpp @@ -34,7 +34,7 @@ TimelineOutput::TimelineOutput() for (int i=0;iset_data_type(NodeParam::kBoolean); + muted_input_ = new NodeInput("muted_in", NodeParam::kBoolean); + muted_input_->set_is_keyframable(false); AddInput(muted_input_); // Set default height @@ -365,7 +365,7 @@ void TrackOutput::SetTrackName(const QString &name) void TrackOutput::SetMuted(bool e) { - muted_input_->set_value_at_time(0, e); + muted_input_->set_override_value(e); InvalidateCache(0, track_length()); } diff --git a/app/node/output/viewer/viewer.cpp b/app/node/output/viewer/viewer.cpp index f5b527182..b0a583669 100644 --- a/app/node/output/viewer/viewer.cpp +++ b/app/node/output/viewer/viewer.cpp @@ -22,16 +22,13 @@ ViewerOutput::ViewerOutput() { - texture_input_ = new NodeInput("tex_in"); - texture_input_->set_data_type(NodeInput::kTexture); + texture_input_ = new NodeInput("tex_in", NodeInput::kTexture); AddInput(texture_input_); - samples_input_ = new NodeInput("samples_in"); - samples_input_->set_data_type(NodeInput::kSamples); + samples_input_ = new NodeInput("samples_in", NodeInput::kSamples); AddInput(samples_input_); - length_input_ = new NodeInput("length_in"); - length_input_->set_data_type(NodeInput::kRational); + length_input_ = new NodeInput("length_in", NodeInput::kRational); AddInput(length_input_); // Create UUID for this node