Files
effectcraft/crates/export/src/out.rs
T
Brandon ThomasandClaude Opus 5.5 31f192e2a9 M15.1: web build (eframe wasm) + xtask web
- apps/effectcraft-web (L6): eframe web runner booting the shared EffectcraftApp with a browser
  session: in-memory file table, <input type=file> pickers and drag-and-drop for open/import,
  downloads for Save and Render Queue outputs (several files as one stored .zip), and the
  window.effectcraft promise API over the control channel. Demo project on start.
- wasm32 runtime fixes: web-time for Instant/SystemTime (render, engine, export, ui-egui);
  Frames renders queued jobs on the UI thread (Frames::pump) instead of a thread pool;
  Render Queue runs inline; relative outputs resolve under / ; control PNGs inline.
- effectcraft-export: per-job output Sink (files to memory instead of disk); host FileExporter
  carries it; test that sink exports touch no file.
- xtask web [--dev] [--serve PORT] (wasm-bindgen 0.2.129, optional wasm-opt, COOP/COEP server);
  effectcraft-web in WEB_CRATES. docs/web.md, headless Chrome smoke test, favicon (+attribution).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 00:19:35 -04:00

78 lines
2.6 KiB
Rust

//! Output files: the file system, or an in-memory [`Sink`] chosen per job (the web app, which
//! offers every written file as a download; tests).
use std::io::{BufWriter, Cursor, Seek, SeekFrom, Write};
use crate::{ExportError, Result, io};
/// Receives each finished output file (path as resolved, complete contents) instead of the file
/// system. Called once per file; image sequences call it from the render threads.
pub type Sink = dyn Fn(&str, Vec<u8>) + Send + Sync;
/// One output file being written.
pub(crate) enum Out<'a> {
File(BufWriter<std::fs::File>),
Mem { path: String, cur: Cursor<Vec<u8>>, sink: &'a Sink },
}
/// Create `path` (parent directories included) or an in-memory file for `sink`.
pub(crate) fn create<'a>(sink: Option<&'a Sink>, path: &str) -> Result<Out<'a>> {
match sink {
Some(sink) => Ok(Out::Mem { path: path.to_string(), cur: Cursor::new(Vec::new()), sink }),
None => {
if let Some(dir) = std::path::Path::new(path).parent().filter(|d| !d.as_os_str().is_empty()) {
std::fs::create_dir_all(dir).map_err(io)?;
}
Ok(Out::File(BufWriter::new(std::fs::File::create(path).map_err(io)?)))
}
}
}
/// Whether `path` already exists (never for a sink: it has no memory of earlier jobs).
pub(crate) fn exists(sink: Option<&Sink>, path: &str) -> bool {
sink.is_none() && std::path::Path::new(path).exists()
}
impl Out<'_> {
/// Flush and close; a sink receives the contents now. Returns the file size.
pub(crate) fn finish(self) -> Result<u64> {
match self {
Out::File(mut w) => {
w.flush().map_err(io)?;
let f = w.into_inner().map_err(|e| ExportError::Io(e.to_string()))?;
Ok(f.metadata().map(|m| m.len()).unwrap_or(0))
}
Out::Mem { path, cur, sink } => {
let data = cur.into_inner();
let n = data.len() as u64;
sink(&path, data);
Ok(n)
}
}
}
}
impl Write for Out<'_> {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
match self {
Out::File(w) => w.write(buf),
Out::Mem { cur, .. } => cur.write(buf),
}
}
fn flush(&mut self) -> std::io::Result<()> {
match self {
Out::File(w) => w.flush(),
Out::Mem { .. } => Ok(()),
}
}
}
impl Seek for Out<'_> {
fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
match self {
Out::File(w) => w.seek(pos),
Out::Mem { cur, .. } => cur.seek(pos),
}
}
}