pub(crate) mod content_source;
pub(crate) mod driver;
mod engine;
mod output_model;
mod session;
mod slice_pipeline;
pub use session::FrameSessionMetrics;
pub use session::{FrameSession, FrameSessionConfig};
pub(crate) use engine::ColorDelayLine;
#[cfg(all(test, not(feature = "testutils")))]
pub(crate) use engine::PresentationEngine;
#[cfg(feature = "testutils")]
pub use engine::PresentationEngine;
#[cfg(feature = "testutils")]
pub(crate) use slice_pipeline::SlicePipeline;
use crate::config::IdlePolicy;
use crate::point::LaserPoint;
use std::sync::Arc;
use std::time::Duration;
pub(crate) fn duration_to_points(d: Duration, pps: u32) -> usize {
if d.is_zero() || pps == 0 {
return 0;
}
const NANOS_PER_SEC: u128 = 1_000_000_000;
let nanos = u128::from(d.as_secs()) * NANOS_PER_SEC + u128::from(d.subsec_nanos());
let points = (nanos * u128::from(pps)).div_ceil(NANOS_PER_SEC);
usize::try_from(points).unwrap_or(usize::MAX)
}
pub(crate) fn park_point(idle_policy: &IdlePolicy) -> LaserPoint {
match idle_policy {
IdlePolicy::Park { x, y } => LaserPoint::blanked(*x, *y),
_ => LaserPoint::blanked(0.0, 0.0),
}
}
pub(crate) fn blank_prefix(buffer: &mut [LaserPoint]) {
for p in buffer {
p.r = 0;
p.g = 0;
p.b = 0;
p.intensity = 0;
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OutputResetReason {
SessionStart,
Reconnect,
Arm,
Disarm,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PresentedSliceKind {
FifoChunk,
FrameSwapFrame,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct OutputFilterContext {
pub pps: u32,
pub kind: PresentedSliceKind,
pub is_cyclic: bool,
}
pub trait OutputFilter: Send + 'static {
fn reset(&mut self, _reason: OutputResetReason) {}
fn filter(&mut self, points: &mut [LaserPoint], ctx: &OutputFilterContext);
}
#[derive(Clone, Debug)]
pub struct Frame {
points: Arc<Vec<LaserPoint>>,
}
impl Frame {
pub fn new(points: Vec<LaserPoint>) -> Self {
Self {
points: Arc::new(points),
}
}
pub fn points(&self) -> &[LaserPoint] {
&self.points
}
pub fn first_point(&self) -> Option<&LaserPoint> {
self.points.first()
}
pub fn last_point(&self) -> Option<&LaserPoint> {
self.points.last()
}
pub fn len(&self) -> usize {
self.points.len()
}
pub fn is_empty(&self) -> bool {
self.points.is_empty()
}
pub fn from_shared(points: Arc<Vec<LaserPoint>>) -> Self {
Self { points }
}
}
impl From<Vec<LaserPoint>> for Frame {
fn from(points: Vec<LaserPoint>) -> Self {
Self::new(points)
}
}
impl From<Arc<Vec<LaserPoint>>> for Frame {
fn from(points: Arc<Vec<LaserPoint>>) -> Self {
Self { points }
}
}
#[derive(Clone, Debug)]
pub enum TransitionPlan {
Transition(Vec<LaserPoint>),
Coalesce,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct TransitionContext {
pub pps: u32,
}
pub type TransitionFn =
Box<dyn Fn(&LaserPoint, &LaserPoint, &TransitionContext) -> TransitionPlan + Send>;
const END_DWELL_US: f64 = 100.0;
const START_DWELL_US: f64 = 400.0;
pub fn default_transition() -> TransitionFn {
Box::new(
move |from: &LaserPoint, to: &LaserPoint, context: &TransitionContext| {
let end_dwell = (END_DWELL_US * context.pps as f64 / 1_000_000.0).round() as usize;
let start_dwell = (START_DWELL_US * context.pps as f64 / 1_000_000.0).round() as usize;
let dx = to.x - from.x;
let dy = to.y - from.y;
let d_inf = dx.abs().max(dy.abs());
let transit = (32.0 * d_inf).ceil().clamp(0.0, 64.0) as usize;
let total = end_dwell + transit + start_dwell;
let mut points = Vec::with_capacity(total);
for _ in 0..end_dwell {
points.push(LaserPoint::blanked(from.x, from.y));
}
for i in 0..transit {
let t = (i as f32 + 1.0) / (transit as f32 + 1.0);
let t = quintic_ease_in_out(t);
points.push(LaserPoint::blanked(from.x + dx * t, from.y + dy * t));
}
for _ in 0..start_dwell {
points.push(LaserPoint::blanked(to.x, to.y));
}
TransitionPlan::Transition(points)
},
)
}
fn quintic_ease_in_out(t: f32) -> f32 {
if t < 0.5 {
16.0 * t * t * t * t * t
} else {
let u = 2.0 * t - 2.0;
0.5 * u * u * u * u * u + 1.0
}
}
#[cfg(test)]
mod tests;