pulse-pixelstream-types 0.14.1

Shared Myko entity and command types for the Pulse Pixelstream recording cell.
Documentation
use myko::prelude::*;
use myko::TS;
use serde::{Deserialize, Serialize};

use crate::ShotEntryMode;

/// The shared shot library used when no production-specific library is chosen.
pub const DEFAULT_SHOT_LIBRARY_ID: &str = "shared";
pub const DEFAULT_SHOT_TRANSLATION_SPEED_CM_S: f32 = 10.0;
pub const DEFAULT_SHOT_ROTATION_SPEED_DEG_S: f32 = 2.0;
pub const DEFAULT_SHOT_HOLD_DURATION_MS: u64 = 5_000;
/// Initial estimate for a moving shot. Unreal currently reports normalized
/// travel position but not authored rail length, so speed alone cannot produce
/// an honest duration. Operators can tune this persisted value per shot and the
/// recorder refines RecordingJob overhead estimates from observed phases.
pub const DEFAULT_SHOT_TRAVEL_DURATION_MS: u64 = 30_000;

pub fn default_shot_travel_duration_ms() -> u64 {
    DEFAULT_SHOT_TRAVEL_DURATION_MS
}

#[derive(
    Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, TS, PartialOrd, Ord, Hash,
)]
#[serde(rename_all = "snake_case")]
pub enum ShotKind {
    #[serde(alias = "rig", alias = "rig_move", alias = "moving")]
    Moving,
    #[default]
    #[serde(alias = "preset", alias = "preset_hold", alias = "stationary")]
    Static,
}

/// One camera target reported by the live StreamCamera pawn. Discovery carries
/// identity only; the server owns default parameters and creates a persistent
/// Shot only when that target is not already represented in the library.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase")]
pub struct ShotDiscovery {
    pub name: String,
    pub kind: ShotKind,
    pub target_name: String,
}

/// A persistent, shared camera shot. A shot is the unit the operator
/// names, indexes, tunes, places in timelines, and eventually records. Its kind
/// describes whether the camera remains stationary or moves during capture.
#[myko_macros::myko_item]
pub struct Shot {
    /// Shared definition scope. Empty on legacy rows and interpreted as the
    /// default shared library.
    #[serde(default)]
    pub library_id: String,
    /// Legacy provenance retained for wire and persisted-row compatibility.
    /// Capture execution chooses a stream at job start; definitions do not.
    #[serde(default)]
    pub streamer_id: String,
    pub name: String,
    #[serde(alias = "target_kind")]
    pub kind: ShotKind,
    pub target_name: String,
    pub translation_speed_cm_s: f32,
    pub rotation_speed_deg_s: f32,
    pub hold_duration_ms: u64,
    #[serde(default)]
    pub travel_duration_ms: u64,
    /// Default used when creating a new ShotEntry. Existing entries own their
    /// direction independently.
    #[serde(
        default,
        alias = "defaultClipMode",
        alias = "defaultListMode",
        alias = "batchMode"
    )]
    pub default_entry_mode: ShotEntryMode,
    /// Stable operator-facing label for the shot. This is persisted with the
    /// Shot but does not determine timeline capture or resume order.
    #[serde(rename = "sortOrder", alias = "shotIndex")]
    pub shot_index: u32,
}

impl Shot {
    pub fn stable_id(library_id: &str, kind: &ShotKind, target_name: &str) -> String {
        let kind = match kind {
            ShotKind::Moving => "moving",
            ShotKind::Static => "static",
        };
        format!("{library_id}:shot:{kind}:{target_name}")
    }

    pub fn effective_library_id(&self) -> &str {
        effective_library_id(&self.library_id)
    }

    pub fn effective_travel_duration_ms(&self) -> u64 {
        if self.travel_duration_ms == 0 {
            DEFAULT_SHOT_TRAVEL_DURATION_MS
        } else {
            self.travel_duration_ms
        }
    }
}

pub fn effective_library_id(library_id: &str) -> &str {
    if library_id.trim().is_empty() {
        DEFAULT_SHOT_LIBRARY_ID
    } else {
        library_id
    }
}