Implement animation export

This commit is contained in:
Keavon Chambers
2026-05-17 11:44:57 -04:00
parent 21d2994059
commit 1d3d32c169
18 changed files with 541 additions and 58 deletions

View File

@@ -31,6 +31,15 @@ pub(super) fn handle_desktop_wrapper_message(dispatcher: &mut DesktopWrapperMess
SaveFileDialogContext::File { content } => {
dispatcher.respond(DesktopFrontendMessage::WriteFile { path, content });
}
SaveFileDialogContext::MultipleFiles { files } => {
// Treat the chosen path as the folder name; strip any extension the user typed (e.g. "MyAnim.png" → "MyAnim").
// The `WriteFile` handler creates parent directories if they don't exist, so the folder is materialized on first write.
let folder = path.with_extension("");
for (filename, content) in files {
let file_path = folder.join(&filename);
dispatcher.respond(DesktopFrontendMessage::WriteFile { path: file_path, content });
}
}
},
DesktopWrapperMessage::OpenFile { path, content } => {
let message = PortfolioMessage::OpenFile { path, content };

View File

@@ -1,3 +1,4 @@
use graphite_editor::messages::frontend::utility_types::ExportAnimationFrame;
#[cfg(target_os = "macos")]
use graphite_editor::messages::layout::utility_types::layout_widget::LayoutTarget;
use graphite_editor::messages::prelude::FrontendMessage;
@@ -59,6 +60,52 @@ pub(super) fn intercept_frontend_message(dispatcher: &mut DesktopWrapperMessageD
context: SaveFileDialogContext::File { content },
});
}
FrontendMessage::TriggerExportAnimation {
name,
extension,
mime,
size,
folder,
frames,
} => {
// Materialize each frame to bytes; SVG strings are encoded as UTF-8.
// Raster-needs-canvas-rasterize frames can't be encoded here without a Rust SVG rasterizer,
// so fall through to the frontend zip path in that case.
let mut needs_frontend_rasterization = false;
let mut materialized = Vec::with_capacity(frames.len());
for (index, frame) in frames.iter().enumerate() {
let filename = format!("{name}_{:04}.{extension}", index + 1);
let bytes = match frame {
ExportAnimationFrame::Svg(svg) if extension == "svg" => svg.as_bytes().to_vec(),
ExportAnimationFrame::Bytes(bytes) => bytes.to_vec(),
ExportAnimationFrame::Svg(_) => {
needs_frontend_rasterization = true;
break;
}
};
materialized.push((filename, bytes));
}
if needs_frontend_rasterization {
return Some(FrontendMessage::TriggerExportAnimation {
name,
extension,
mime,
size,
folder,
frames,
});
}
// The dialog name is the folder the frames go into (analogous to the .zip on web).
dispatcher.respond(DesktopFrontendMessage::SaveFileDialog {
title: "Save Animation Frames Folder As".to_string(),
default_filename: name.clone(),
default_folder: folder,
filters: Vec::new(),
context: SaveFileDialogContext::MultipleFiles { files: materialized },
});
}
FrontendMessage::TriggerVisitLink { url } => {
dispatcher.respond(DesktopFrontendMessage::OpenUrl(url));
}

View File

@@ -111,8 +111,19 @@ pub enum OpenFileDialogContext {
}
pub enum SaveFileDialogContext {
Document { document_id: DocumentId, content: Vec<u8> },
File { content: Vec<u8> },
Document {
document_id: DocumentId,
content: Vec<u8>,
},
File {
content: Vec<u8>,
},
/// Multiple files written into a folder whose path is the user-chosen path with any extension stripped
/// (e.g. picking `MyAnim.png` yields a `MyAnim/` folder). Each `(filename, content)` entry is written
/// inside that folder, which is created if it doesn't exist.
MultipleFiles {
files: Vec<(String, Vec<u8>)>,
},
}
pub enum MenuItem {