///|
/// A named scalar channel with editor-friendly activation and weighting.
pub struct ScalarChannel {
name : String
track : ScalarTrack
enabled : Bool
weight : Double
offset : Double
} derive(Debug)
///|
pub fn scalar_channel(
name : String,
track : ScalarTrack,
enabled? : Bool = true,
weight? : Double = 1.0,
offset? : Double = 0.0,
) -> ScalarChannel raise MotionError {
if name.is_empty() {
raise MotionError::InvalidChannelName
}
ensure_finite(weight)
ensure_finite(offset)
if weight < 0.0 {
raise MotionError::InvalidThreshold(weight)
}
{ name, track, enabled, weight, offset }
}
///|
pub fn ScalarChannel::name(self : ScalarChannel) -> String {
self.name
}
///|
pub fn ScalarChannel::track(self : ScalarChannel) -> ScalarTrack {
self.track
}
///|
pub fn ScalarChannel::enabled(self : ScalarChannel) -> Bool {
self.enabled
}
///|
pub fn ScalarChannel::weight(self : ScalarChannel) -> Double {
self.weight
}
///|
pub fn ScalarChannel::offset(self : ScalarChannel) -> Double {
self.offset
}
///|
pub fn ScalarChannel::sample(
self : ScalarChannel,
time : Double,
mode? : Extrapolation = Clamp,
) -> Double {
self.track.sample(time, mode~) + self.offset
}
///|
pub fn ScalarChannel::with_enabled(
self : ScalarChannel,
enabled : Bool,
) -> ScalarChannel {
{ ..self, enabled, }
}
///|
pub fn ScalarChannel::with_weight(
self : ScalarChannel,
weight : Double,
) -> ScalarChannel raise MotionError {
ensure_finite(weight)
if weight < 0.0 {
raise MotionError::InvalidThreshold(weight)
}
{ ..self, weight, }
}
///|
pub fn ScalarChannel::with_offset(
self : ScalarChannel,
offset : Double,
) -> ScalarChannel raise MotionError {
ensure_finite(offset)
{ ..self, offset, }
}
///|
pub fn ScalarChannel::duration(self : ScalarChannel) -> Double {
self.track.duration()
}
///|
pub fn ScalarChannel::start_time(self : ScalarChannel) -> Double {
self.track.start_time()
}
///|
pub fn ScalarChannel::end_time(self : ScalarChannel) -> Double {
self.track.end_time()
}
///|
/// The combined result at one timestamp. `weighted_value` is the normalized
/// blend of enabled channels; individual values remain available for output.
pub(all) struct BundleSample {
time : Double
names : Array[String]
values : Array[Double]
weighted_value : Double
active_weight : Double
} derive(Debug)
///|
pub fn BundleSample::time(self : BundleSample) -> Double {
self.time
}
///|
pub fn BundleSample::names(self : BundleSample) -> Array[String] {
self.names.copy()
}
///|
pub fn BundleSample::values(self : BundleSample) -> Array[Double] {
self.values.copy()
}
///|
pub fn BundleSample::length(self : BundleSample) -> Int {
self.values.length()
}
///|
pub fn BundleSample::value_at(self : BundleSample, index : Int) -> Double? {
if index < 0 || index >= self.values.length() {
None
} else {
Some(self.values[index])
}
}
///|
pub fn BundleSample::value_named(self : BundleSample, name : String) -> Double? {
for index in 0.. Double {
self.weighted_value
}
///|
pub fn BundleSample::active_weight(self : BundleSample) -> Double {
self.active_weight
}
///|
/// A group of synchronized scalar tracks for UI properties, camera channels,
/// or a compact control stream exported to a runtime.
pub struct ScalarTrackBundle {
channels : Array[ScalarChannel]
start_time : Double
end_time : Double
} derive(Debug)
///|
fn validate_channel_names(
channels : Array[ScalarChannel],
) -> Unit raise MotionError {
for left in 0.. ScalarTrackBundle raise MotionError {
if channels.length() == 0 {
raise MotionError::EmptyTrack
}
validate_channel_names(channels)
let mut start = channels[0].start_time()
let mut finish = channels[0].end_time()
for channel in channels {
start = start.min(channel.start_time())
finish = finish.max(channel.end_time())
}
{ channels: channels.copy(), start_time: start, end_time: finish }
}
///|
pub fn ScalarTrackBundle::channels(
self : ScalarTrackBundle,
) -> Array[ScalarChannel] {
self.channels.copy()
}
///|
pub fn ScalarTrackBundle::length(self : ScalarTrackBundle) -> Int {
self.channels.length()
}
///|
pub fn ScalarTrackBundle::start_time(self : ScalarTrackBundle) -> Double {
self.start_time
}
///|
pub fn ScalarTrackBundle::end_time(self : ScalarTrackBundle) -> Double {
self.end_time
}
///|
pub fn ScalarTrackBundle::duration(self : ScalarTrackBundle) -> Double {
self.end_time - self.start_time
}
///|
pub fn ScalarTrackBundle::find(
self : ScalarTrackBundle,
name : String,
) -> ScalarChannel? {
for channel in self.channels {
if channel.name == name {
return Some(channel)
}
}
None
}
///|
pub fn ScalarTrackBundle::sample(
self : ScalarTrackBundle,
time : Double,
mode? : Extrapolation = Clamp,
) -> BundleSample {
let names : Array[String] = []
let values : Array[Double] = []
let mut weighted_total = 0.0
let mut active_weight = 0.0
for channel in self.channels {
let value = channel.sample(time, mode~)
names.push(channel.name)
values.push(value)
if channel.enabled && channel.weight > 0.0 {
weighted_total = weighted_total + value * channel.weight
active_weight = active_weight + channel.weight
}
}
{
time,
names,
values,
weighted_value: if active_weight == 0.0 {
0.0
} else {
weighted_total / active_weight
},
active_weight,
}
}
///|
pub fn ScalarTrackBundle::sample_many(
self : ScalarTrackBundle,
count : Int,
mode? : Extrapolation = Clamp,
) -> Array[BundleSample] raise MotionError {
if count < 2 {
raise MotionError::InvalidSampleCount(count)
}
let result : Array[BundleSample] = []
for index in 0.. ScalarTrackBundle raise MotionError {
let result : Array[ScalarChannel] = []
let mut found = false
for channel in self.channels {
if channel.name == name {
result.push(channel.with_enabled(enabled))
found = true
} else {
result.push(channel)
}
}
if !found {
raise MotionError::InvalidChannelName
}
ScalarTrackBundle::new(result)
}
///|
pub fn ScalarTrackBundle::set_weight(
self : ScalarTrackBundle,
name : String,
weight : Double,
) -> ScalarTrackBundle raise MotionError {
let result : Array[ScalarChannel] = []
let mut found = false
for channel in self.channels {
if channel.name == name {
result.push(channel.with_weight(weight))
found = true
} else {
result.push(channel)
}
}
if !found {
raise MotionError::InvalidChannelName
}
ScalarTrackBundle::new(result)
}
///|
pub fn ScalarTrackBundle::with_channel(
self : ScalarTrackBundle,
channel : ScalarChannel,
) -> ScalarTrackBundle raise MotionError {
let result : Array[ScalarChannel] = []
for existing in self.channels {
if existing.name != channel.name {
result.push(existing)
}
}
result.push(channel)
ScalarTrackBundle::new(result)
}
///|
pub fn ScalarTrackBundle::without(
self : ScalarTrackBundle,
name : String,
) -> ScalarTrackBundle raise MotionError {
let result : Array[ScalarChannel] = []
for channel in self.channels {
if channel.name != name {
result.push(channel)
}
}
ScalarTrackBundle::new(result)
}
///|
/// Blend two bundles by channel name. Missing channels are carried through
/// from the bundle that contains them.
pub fn blend_bundles(
first : ScalarTrackBundle,
second : ScalarTrackBundle,
amount : Double,
) -> ScalarTrackBundle raise MotionError {
let weight = clamp01(amount)
let result : Array[ScalarChannel] = []
for left in first.channels {
match second.find(left.name) {
Some(right) => {
let frames : Array[Keyframe] = []
let count = left.track
.keyframes()
.length()
.max(right.track.keyframes().length())
let left_frames = left.track.keyframes()
let right_frames = right.track.keyframes()
for index in 0.. result.push(left)
}
}
for right in second.channels {
if first.find(right.name) is None {
result.push(right)
}
}
ScalarTrackBundle::new(result)
}
///|
pub struct ScalarTrackBundleBuilder {
channels : Array[ScalarChannel]
} derive(Debug)
///|
pub fn ScalarTrackBundleBuilder::new() -> ScalarTrackBundleBuilder {
{ channels: [] }
}
///|
pub fn ScalarTrackBundleBuilder::add(
self : ScalarTrackBundleBuilder,
channel : ScalarChannel,
) -> ScalarTrackBundleBuilder raise MotionError {
for existing in self.channels {
if existing.name == channel.name {
raise MotionError::InvalidChannelName
}
}
self.channels.push(channel)
self
}
///|
pub fn ScalarTrackBundleBuilder::remove(
self : ScalarTrackBundleBuilder,
name : String,
) -> Bool {
for index in 0.. Int {
self.channels.length()
}
///|
pub fn ScalarTrackBundleBuilder::build(
self : ScalarTrackBundleBuilder,
) -> ScalarTrackBundle raise MotionError {
ScalarTrackBundle::new(self.channels)
}