shadertoy-cli 2.1.3

Agent-friendly ShaderToy project, rendering, debugging, and live-preview CLI
use super::*;

#[allow(clippy::too_many_arguments)]
pub(super) fn reload(
    root: &Path,
    runtime: &mut Runtime<'_>,
    loaded: &mut Option<LoadedManifest>,
    width: &mut u32,
    height: &mut u32,
    fps: &mut f32,
    view: &mut String,
    preserve_state: bool,
) -> Result<()> {
    let saved = if preserve_state {
        loaded
            .as_ref()
            .map(|current| save_runtime_state(runtime, current, *width, *height, *fps))
            .transpose()?
    } else {
        None
    };

    let candidate = LoadedManifest::load(root)?;
    ensure_source_files_exist(&candidate)?;
    let project = build_native_project(&candidate)?;
    runtime.load_project(&project)?;

    if loaded.is_none() {
        *width = candidate.manifest.render.width;
        *height = candidate.manifest.render.height;
        *fps = candidate.manifest.render.fps;
    }

    if let Some(saved) = saved {
        runtime.set_fixed_state(saved.time, saved.frame, saved.fps)?;
        for (name, data) in saved.buffers {
            if candidate
                .manifest
                .passes
                .iter()
                .any(|pass| pass.name == name && pass.kind == PassKind::Buffer)
            {
                let _ = runtime.restore_pass_rgba32f(&name, *width, *height, &data);
            }
        }
    }

    let final_pass = candidate.manifest.final_pass().name.clone();
    if !candidate
        .manifest
        .passes
        .iter()
        .any(|pass| pass.name == *view)
    {
        *view = final_pass;
    }
    *loaded = Some(candidate);
    Ok(())
}

fn save_runtime_state(
    runtime: &mut Runtime<'_>,
    loaded: &LoadedManifest,
    width: u32,
    height: u32,
    fps: f32,
) -> Result<SavedRuntimeState> {
    let mut buffers = BTreeMap::new();
    for pass in &loaded.manifest.passes {
        if pass.kind == PassKind::Buffer
            && let Ok(data) = runtime.snapshot_pass_rgba32f(&pass.name, width, height)
        {
            buffers.insert(pass.name.clone(), data);
        }
    }
    Ok(SavedRuntimeState {
        time: runtime.time(),
        frame: runtime.frame(),
        fps,
        buffers,
    })
}

#[allow(clippy::too_many_arguments)]
pub(super) fn update_status(
    shared: &Shared,
    loaded: Option<&LoadedManifest>,
    runtime: &Runtime<'_>,
    width: u32,
    height: u32,
    fps: f32,
    paused: bool,
    view: &str,
    error: Option<String>,
    increment_sequence: bool,
) {
    let mut status = shared.status.write().expect("preview status lock poisoned");
    if let Some(loaded) = loaded {
        status.project = loaded.manifest.project.name.clone();
        status.final_pass = loaded.manifest.final_pass().name.clone();
        status.passes = loaded
            .manifest
            .passes
            .iter()
            .filter(|pass| pass.kind != PassKind::Cubemap)
            .map(|pass| pass.name.clone())
            .collect();
    }
    status.frame = runtime.frame();
    status.time = runtime.time();
    status.paused = paused;
    status.time_scale = runtime.time_scale();
    status.width = width;
    status.height = height;
    status.fps = fps;
    status.view = view.to_string();
    if error.is_some() {
        status.error = error;
    } else if increment_sequence {
        status.error = None;
    }
    if increment_sequence {
        status.sequence = status.sequence.wrapping_add(1);
    }
    broadcast_status(shared, &status);
}

pub(super) fn set_error(shared: &Shared, message: String) {
    let mut status = shared.status.write().expect("preview status lock poisoned");
    status.error = Some(message);
    broadcast_status(shared, &status);
}

pub(super) fn clear_error(shared: &Shared) {
    let mut status = shared.status.write().expect("preview status lock poisoned");
    status.error = None;
    broadcast_status(shared, &status);
}

fn broadcast_status(shared: &Shared, status: &PreviewStatus) {
    if let Ok(message) = serde_json::to_string(status) {
        let _ = shared.updates.send(message);
    }
}

pub(super) fn reload_event_kind(kind: &notify::EventKind) -> bool {
    matches!(
        kind,
        notify::EventKind::Any
            | notify::EventKind::Create(_)
            | notify::EventKind::Modify(_)
            | notify::EventKind::Remove(_)
    )
}

pub(super) fn relevant_watch_path(root: &Path, path: &Path) -> bool {
    let Ok(relative) = path.strip_prefix(root) else {
        return false;
    };
    let Some(first) = relative.components().next() else {
        return false;
    };
    let first = first.as_os_str().to_string_lossy();
    if matches!(first.as_ref(), "target" | ".git" | ".shadertoy") {
        return false;
    }
    true
}