Exporting (#1495)
This commit is contained in:
parent
99823e952a
commit
fe4b9ef8bb
|
|
@ -319,8 +319,19 @@ impl DocumentMetadata {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Calculates the document bounds in document space
|
/// Calculates the document bounds in document space
|
||||||
pub fn document_bounds_document_space(&self) -> Option<[DVec2; 2]> {
|
pub fn document_bounds_document_space(&self, include_artboards: bool) -> Option<[DVec2; 2]> {
|
||||||
self.all_layers().filter_map(|layer| self.bounding_box_document(layer)).reduce(Quad::combine_bounds)
|
self.all_layers()
|
||||||
|
.filter(|&layer| include_artboards || self.is_artboard(layer))
|
||||||
|
.filter_map(|layer| self.bounding_box_document(layer))
|
||||||
|
.reduce(Quad::combine_bounds)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculates the selected layer bounds in document space
|
||||||
|
pub fn selected_bounds_document_space(&self, include_artboards: bool) -> Option<[DVec2; 2]> {
|
||||||
|
self.selected_layers()
|
||||||
|
.filter(|&layer| include_artboards || self.is_artboard(layer))
|
||||||
|
.filter_map(|layer| self.bounding_box_document(layer))
|
||||||
|
.reduce(Quad::combine_bounds)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn layer_outline(&self, layer: LayerNodeIdentifier) -> graphene_core::vector::Subpath {
|
pub fn layer_outline(&self, layer: LayerNodeIdentifier) -> graphene_core::vector::Subpath {
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,7 @@ impl MessageHandler<DialogMessage, DialogData<'_>> for DialogMessageHandler {
|
||||||
self.export_dialog = ExportDialogMessageHandler {
|
self.export_dialog = ExportDialogMessageHandler {
|
||||||
scale_factor: 1.,
|
scale_factor: 1.,
|
||||||
artboards,
|
artboards,
|
||||||
has_selection: document.selected_layers().next().is_some(),
|
has_selection: document.metadata().selected_layers().next().is_some(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
self.export_dialog.send_dialog_to_frontend(responses);
|
self.export_dialog.send_dialog_to_frontend(responses);
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ impl MessageHandler<ExportDialogMessage, &PortfolioMessageHandler> for ExportDia
|
||||||
ExportDialogMessage::TransparentBackground(transparent_background) => self.transparent_background = transparent_background,
|
ExportDialogMessage::TransparentBackground(transparent_background) => self.transparent_background = transparent_background,
|
||||||
ExportDialogMessage::ExportBounds(export_area) => self.bounds = export_area,
|
ExportDialogMessage::ExportBounds(export_area) => self.bounds = export_area,
|
||||||
|
|
||||||
ExportDialogMessage::Submit => responses.add_front(DocumentMessage::ExportDocument {
|
ExportDialogMessage::Submit => responses.add_front(PortfolioMessage::SubmitDocumentExport {
|
||||||
file_name: portfolio.active_document().map(|document| document.name.clone()).unwrap_or_default(),
|
file_name: portfolio.active_document().map(|document| document.name.clone()).unwrap_or_default(),
|
||||||
file_type: self.file_type,
|
file_type: self.file_type,
|
||||||
scale_factor: self.scale_factor,
|
scale_factor: self.scale_factor,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
use crate::messages::frontend::utility_types::{ExportBounds, FileType};
|
|
||||||
use crate::messages::input_mapper::utility_types::input_keyboard::Key;
|
use crate::messages::input_mapper::utility_types::input_keyboard::Key;
|
||||||
use crate::messages::portfolio::document::utility_types::layer_panel::LayerMetadata;
|
use crate::messages::portfolio::document::utility_types::layer_panel::LayerMetadata;
|
||||||
use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, FlipAxis};
|
use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, FlipAxis};
|
||||||
|
|
@ -74,13 +73,6 @@ pub enum DocumentMessage {
|
||||||
layer_path: Vec<LayerId>,
|
layer_path: Vec<LayerId>,
|
||||||
},
|
},
|
||||||
DuplicateSelectedLayers,
|
DuplicateSelectedLayers,
|
||||||
ExportDocument {
|
|
||||||
file_name: String,
|
|
||||||
file_type: FileType,
|
|
||||||
scale_factor: f64,
|
|
||||||
bounds: ExportBounds,
|
|
||||||
transparent_background: bool,
|
|
||||||
},
|
|
||||||
FlipSelectedLayers {
|
FlipSelectedLayers {
|
||||||
flip_axis: FlipAxis,
|
flip_axis: FlipAxis,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
use super::utility_types::error::EditorError;
|
use super::utility_types::error::EditorError;
|
||||||
use super::utility_types::misc::{DocumentRenderMode, SnappingOptions, SnappingState};
|
use super::utility_types::misc::{SnappingOptions, SnappingState};
|
||||||
use crate::application::generate_uuid;
|
use crate::application::generate_uuid;
|
||||||
use crate::consts::{ASYMPTOTIC_EFFECT, DEFAULT_DOCUMENT_NAME, FILE_SAVE_SUFFIX, GRAPHITE_DOCUMENT_VERSION, SCALE_EFFECT, SCROLLBAR_SPACING, VIEWPORT_ZOOM_TO_FIT_PADDING_SCALE_FACTOR};
|
use crate::consts::{ASYMPTOTIC_EFFECT, DEFAULT_DOCUMENT_NAME, FILE_SAVE_SUFFIX, GRAPHITE_DOCUMENT_VERSION, SCALE_EFFECT, SCROLLBAR_SPACING, VIEWPORT_ZOOM_TO_FIT_PADDING_SCALE_FACTOR};
|
||||||
use crate::messages::frontend::utility_types::ExportBounds;
|
|
||||||
use crate::messages::frontend::utility_types::FileType;
|
|
||||||
use crate::messages::input_mapper::utility_types::macros::action_keys;
|
use crate::messages::input_mapper::utility_types::macros::action_keys;
|
||||||
use crate::messages::layout::utility_types::widget_prelude::*;
|
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||||
use crate::messages::portfolio::document::node_graph::NodeGraphHandlerData;
|
use crate::messages::portfolio::document::node_graph::NodeGraphHandlerData;
|
||||||
|
|
@ -354,43 +352,6 @@ impl MessageHandler<DocumentMessage, DocumentInputs<'_>> for DocumentMessageHand
|
||||||
responses.add(DocumentOperation::DuplicateLayer { path: path.to_vec() });
|
responses.add(DocumentOperation::DuplicateLayer { path: path.to_vec() });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ExportDocument {
|
|
||||||
file_name,
|
|
||||||
file_type,
|
|
||||||
scale_factor,
|
|
||||||
bounds,
|
|
||||||
transparent_background,
|
|
||||||
} => {
|
|
||||||
let old_artwork_transform = self.remove_document_transform();
|
|
||||||
|
|
||||||
// Calculate the bounding box of the region to be exported
|
|
||||||
let bounds = match bounds {
|
|
||||||
ExportBounds::AllArtwork => self.all_layer_bounds(&render_data),
|
|
||||||
ExportBounds::Selection => self.document_legacy.selected_visible_layers_bounding_box_viewport(),
|
|
||||||
ExportBounds::Artboard(id) => self.metadata().bounding_box_document(id),
|
|
||||||
}
|
|
||||||
.unwrap_or_default();
|
|
||||||
let size = bounds[1] - bounds[0];
|
|
||||||
let transform = (DAffine2::from_translation(bounds[0]) * DAffine2::from_scale(size)).inverse();
|
|
||||||
|
|
||||||
let document = self.render_document(size, transform, transparent_background, persistent_data, DocumentRenderMode::Root);
|
|
||||||
|
|
||||||
self.restore_document_transform(old_artwork_transform);
|
|
||||||
|
|
||||||
let file_suffix = &format!(".{file_type:?}").to_lowercase();
|
|
||||||
let name = match file_name.ends_with(FILE_SAVE_SUFFIX) {
|
|
||||||
true => file_name.replace(FILE_SAVE_SUFFIX, file_suffix),
|
|
||||||
false => file_name + file_suffix,
|
|
||||||
};
|
|
||||||
|
|
||||||
if file_type == FileType::Svg {
|
|
||||||
responses.add(FrontendMessage::TriggerDownloadTextFile { document, name });
|
|
||||||
} else {
|
|
||||||
let mime = file_type.to_mime().to_string();
|
|
||||||
let size = (size * scale_factor).into();
|
|
||||||
responses.add(FrontendMessage::TriggerDownloadImage { svg: document, name, mime, size });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
FlipSelectedLayers { flip_axis } => {
|
FlipSelectedLayers { flip_axis } => {
|
||||||
self.backup(responses);
|
self.backup(responses);
|
||||||
let scale = match flip_axis {
|
let scale = match flip_axis {
|
||||||
|
|
@ -878,7 +839,7 @@ impl MessageHandler<DocumentMessage, DocumentInputs<'_>> for DocumentMessageHand
|
||||||
responses.add_front(NavigationMessage::SetCanvasZoom { zoom_factor: 2. });
|
responses.add_front(NavigationMessage::SetCanvasZoom { zoom_factor: 2. });
|
||||||
}
|
}
|
||||||
ZoomCanvasToFitAll => {
|
ZoomCanvasToFitAll => {
|
||||||
if let Some(bounds) = self.metadata().document_bounds_document_space() {
|
if let Some(bounds) = self.metadata().document_bounds_document_space(true) {
|
||||||
responses.add(NavigationMessage::FitViewportToBounds {
|
responses.add(NavigationMessage::FitViewportToBounds {
|
||||||
bounds,
|
bounds,
|
||||||
padding_scale_factor: Some(VIEWPORT_ZOOM_TO_FIT_PADDING_SCALE_FACTOR),
|
padding_scale_factor: Some(VIEWPORT_ZOOM_TO_FIT_PADDING_SCALE_FACTOR),
|
||||||
|
|
@ -902,7 +863,6 @@ impl DocumentMessageHandler {
|
||||||
SelectAllLayers,
|
SelectAllLayers,
|
||||||
DeselectAllLayers,
|
DeselectAllLayers,
|
||||||
RenderDocument,
|
RenderDocument,
|
||||||
ExportDocument,
|
|
||||||
SaveDocument,
|
SaveDocument,
|
||||||
SetSnapping,
|
SetSnapping,
|
||||||
DebugPrintDocument,
|
DebugPrintDocument,
|
||||||
|
|
@ -940,49 +900,6 @@ impl DocumentMessageHandler {
|
||||||
&self.document_legacy.metadata
|
&self.document_legacy.metadata
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Remove the artwork and artboard pan/tilt/zoom to render it without the user's viewport navigation, and save it to be restored at the end
|
|
||||||
pub(crate) fn remove_document_transform(&mut self) -> DAffine2 {
|
|
||||||
let old_artwork_transform = self.metadata().document_to_viewport;
|
|
||||||
self.document_legacy.metadata.document_to_viewport = DAffine2::IDENTITY;
|
|
||||||
DocumentLegacy::mark_children_as_dirty(&mut self.document_legacy.root);
|
|
||||||
|
|
||||||
old_artwork_transform
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Transform the artwork and artboard back to their original scales
|
|
||||||
pub(crate) fn restore_document_transform(&mut self, old_artwork_transform: DAffine2) {
|
|
||||||
self.document_legacy.metadata.document_to_viewport = old_artwork_transform;
|
|
||||||
DocumentLegacy::mark_children_as_dirty(&mut self.document_legacy.root);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn render_document(&mut self, size: DVec2, transform: DAffine2, transparent_background: bool, persistent_data: &PersistentData, render_mode: DocumentRenderMode) -> String {
|
|
||||||
// Render the document SVG code
|
|
||||||
|
|
||||||
let render_data = RenderData::new(&persistent_data.font_cache, ViewMode::Normal, None);
|
|
||||||
|
|
||||||
let (artwork, outside) = match render_mode {
|
|
||||||
DocumentRenderMode::Root => (self.document_legacy.render_root(&render_data), None),
|
|
||||||
DocumentRenderMode::OnlyBelowLayerInFolder(below_layer_path) => (self.document_legacy.render_layers_below(below_layer_path, &render_data).unwrap(), None),
|
|
||||||
DocumentRenderMode::LayerCutout(layer_path, background) => (self.document_legacy.render_layer(layer_path, &render_data).unwrap(), Some(background)),
|
|
||||||
};
|
|
||||||
let canvas_background_color = outside.map_or_else(|| "222222".to_string(), |col| col.rgba_hex());
|
|
||||||
let canvas_background = match transparent_background {
|
|
||||||
false => format!(r##"<rect x="0" y="0" width="100%" height="100%" fill="#{canvas_background_color}" />"##),
|
|
||||||
true => "".into(),
|
|
||||||
};
|
|
||||||
let matrix = transform
|
|
||||||
.to_cols_array()
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.fold(String::new(), |acc, (i, entry)| acc + &(entry.to_string() + if i == 5 { "" } else { "," }));
|
|
||||||
let svg = format!(
|
|
||||||
r#"<svg xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="none" viewBox="0 0 1 1" width="{}" height="{}">{}{canvas_background}<g transform="matrix({matrix})">{artwork}</g></svg>"#,
|
|
||||||
size.x, size.y, "\n",
|
|
||||||
);
|
|
||||||
|
|
||||||
svg
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn serialize_document(&self) -> String {
|
pub fn serialize_document(&self) -> String {
|
||||||
let val = serde_json::to_string(self);
|
let val = serde_json::to_string(self);
|
||||||
// We fully expect the serialization to succeed
|
// We fully expect the serialization to succeed
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
|
use crate::messages::frontend::utility_types::{ExportBounds, FileType};
|
||||||
use crate::messages::portfolio::document::utility_types::clipboards::Clipboard;
|
use crate::messages::portfolio::document::utility_types::clipboards::Clipboard;
|
||||||
use crate::messages::prelude::*;
|
use crate::messages::prelude::*;
|
||||||
|
|
||||||
use document_legacy::document_metadata::LayerNodeIdentifier;
|
use document_legacy::document_metadata::LayerNodeIdentifier;
|
||||||
use document_legacy::LayerId;
|
use document_legacy::LayerId;
|
||||||
use graph_craft::document::NodeId;
|
use graph_craft::document::NodeId;
|
||||||
|
|
@ -110,6 +110,13 @@ pub enum PortfolioMessage {
|
||||||
blob_url: String,
|
blob_url: String,
|
||||||
resolution: (f64, f64),
|
resolution: (f64, f64),
|
||||||
},
|
},
|
||||||
|
SubmitDocumentExport {
|
||||||
|
file_name: String,
|
||||||
|
file_type: FileType,
|
||||||
|
scale_factor: f64,
|
||||||
|
bounds: ExportBounds,
|
||||||
|
transparent_background: bool,
|
||||||
|
},
|
||||||
SubmitGraphRender {
|
SubmitGraphRender {
|
||||||
document_id: u64,
|
document_id: u64,
|
||||||
layer_path: Vec<LayerId>,
|
layer_path: Vec<LayerId>,
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ use crate::messages::portfolio::document::utility_types::clipboards::{Clipboard,
|
||||||
use crate::messages::portfolio::document::DocumentInputs;
|
use crate::messages::portfolio::document::DocumentInputs;
|
||||||
use crate::messages::prelude::*;
|
use crate::messages::prelude::*;
|
||||||
use crate::messages::tool::utility_types::{HintData, HintGroup};
|
use crate::messages::tool::utility_types::{HintData, HintGroup};
|
||||||
use crate::node_graph_executor::NodeGraphExecutor;
|
use crate::node_graph_executor::{ExportConfig, NodeGraphExecutor};
|
||||||
|
|
||||||
use document_legacy::layers::style::RenderData;
|
use document_legacy::layers::style::RenderData;
|
||||||
use graph_craft::document::NodeId;
|
use graph_craft::document::NodeId;
|
||||||
|
|
@ -511,6 +511,31 @@ impl MessageHandler<PortfolioMessage, (&InputPreprocessorMessageHandler, &Prefer
|
||||||
};
|
};
|
||||||
responses.add(PortfolioMessage::DocumentPassMessage { document_id, message });
|
responses.add(PortfolioMessage::DocumentPassMessage { document_id, message });
|
||||||
}
|
}
|
||||||
|
PortfolioMessage::SubmitDocumentExport {
|
||||||
|
file_name,
|
||||||
|
file_type,
|
||||||
|
scale_factor,
|
||||||
|
bounds,
|
||||||
|
transparent_background,
|
||||||
|
} => {
|
||||||
|
let document = self.active_document_id.and_then(|id| self.documents.get_mut(&id)).expect("Tried to render no existent Document");
|
||||||
|
let export_config = ExportConfig {
|
||||||
|
file_name,
|
||||||
|
file_type,
|
||||||
|
scale_factor,
|
||||||
|
bounds,
|
||||||
|
transparent_background,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let result = self.executor.submit_document_export(document, export_config);
|
||||||
|
|
||||||
|
if let Err(description) = result {
|
||||||
|
responses.add(DialogMessage::DisplayDialogError {
|
||||||
|
title: "Unable to export document".to_string(),
|
||||||
|
description,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
PortfolioMessage::SubmitGraphRender { document_id, layer_path } => {
|
PortfolioMessage::SubmitGraphRender { document_id, layer_path } => {
|
||||||
let result = self.executor.submit_node_graph_evaluation(
|
let result = self.executor.submit_node_graph_evaluation(
|
||||||
self.documents.get_mut(&document_id).expect("Tried to render no existent Document"),
|
self.documents.get_mut(&document_id).expect("Tried to render no existent Document"),
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
|
use crate::consts::FILE_SAVE_SUFFIX;
|
||||||
use crate::messages::frontend::utility_types::FrontendImageData;
|
use crate::messages::frontend::utility_types::FrontendImageData;
|
||||||
|
use crate::messages::frontend::utility_types::{ExportBounds, FileType};
|
||||||
use crate::messages::portfolio::document::node_graph::wrap_network_in_scope;
|
use crate::messages::portfolio::document::node_graph::wrap_network_in_scope;
|
||||||
use crate::messages::portfolio::document::utility_types::misc::{LayerMetadata, LayerPanelEntry};
|
use crate::messages::portfolio::document::utility_types::misc::{LayerMetadata, LayerPanelEntry};
|
||||||
use crate::messages::prelude::*;
|
use crate::messages::prelude::*;
|
||||||
|
|
||||||
use document_legacy::document::Document as DocumentLegacy;
|
use document_legacy::document::Document as DocumentLegacy;
|
||||||
use document_legacy::document_metadata::LayerNodeIdentifier;
|
use document_legacy::document_metadata::LayerNodeIdentifier;
|
||||||
use document_legacy::layers::layer_info::{LayerDataType, LayerDataTypeDiscriminant};
|
use document_legacy::layers::layer_info::{LayerDataType, LayerDataTypeDiscriminant};
|
||||||
use document_legacy::{LayerId, Operation};
|
use document_legacy::{LayerId, Operation};
|
||||||
|
|
||||||
use graph_craft::document::value::TaggedValue;
|
use graph_craft::document::value::TaggedValue;
|
||||||
use graph_craft::document::{generate_uuid, DocumentNodeImplementation, NodeId, NodeNetwork};
|
use graph_craft::document::{generate_uuid, DocumentNodeImplementation, NodeId, NodeNetwork};
|
||||||
use graph_craft::graphene_compiler::Compiler;
|
use graph_craft::graphene_compiler::Compiler;
|
||||||
|
|
@ -69,6 +69,16 @@ enum NodeRuntimeMessage {
|
||||||
ImaginatePreferencesUpdate(ImaginatePreferences),
|
ImaginatePreferencesUpdate(ImaginatePreferences),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone)]
|
||||||
|
pub struct ExportConfig {
|
||||||
|
pub file_name: String,
|
||||||
|
pub file_type: FileType,
|
||||||
|
pub scale_factor: f64,
|
||||||
|
pub bounds: ExportBounds,
|
||||||
|
pub transparent_background: bool,
|
||||||
|
pub size: DVec2,
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) struct GenerationRequest {
|
pub(crate) struct GenerationRequest {
|
||||||
generation_id: u64,
|
generation_id: u64,
|
||||||
graph: NodeNetwork,
|
graph: NodeNetwork,
|
||||||
|
|
@ -269,7 +279,7 @@ impl NodeRuntime {
|
||||||
let graphic_element = &io_data.output;
|
let graphic_element = &io_data.output;
|
||||||
use graphene_core::renderer::*;
|
use graphene_core::renderer::*;
|
||||||
let bounds = graphic_element.bounding_box(DAffine2::IDENTITY);
|
let bounds = graphic_element.bounding_box(DAffine2::IDENTITY);
|
||||||
let render_params = RenderParams::new(ViewMode::Normal, ImageRenderMode::BlobUrl, bounds, true);
|
let render_params = RenderParams::new(ViewMode::Normal, ImageRenderMode::BlobUrl, bounds, true, false, false);
|
||||||
let mut render = SvgRender::new();
|
let mut render = SvgRender::new();
|
||||||
graphic_element.render_svg(&mut render, &render_params);
|
graphic_element.render_svg(&mut render, &render_params);
|
||||||
let [min, max] = bounds.unwrap_or_default();
|
let [min, max] = bounds.unwrap_or_default();
|
||||||
|
|
@ -370,6 +380,7 @@ pub struct NodeGraphExecutor {
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
struct ExecutionContext {
|
struct ExecutionContext {
|
||||||
layer_path: Vec<LayerId>,
|
layer_path: Vec<LayerId>,
|
||||||
|
export_config: Option<ExportConfig>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for NodeGraphExecutor {
|
impl Default for NodeGraphExecutor {
|
||||||
|
|
@ -502,16 +513,85 @@ impl NodeGraphExecutor {
|
||||||
#[cfg(not(any(feature = "resvg", feature = "vello")))]
|
#[cfg(not(any(feature = "resvg", feature = "vello")))]
|
||||||
export_format: graphene_core::application_io::ExportFormat::Svg,
|
export_format: graphene_core::application_io::ExportFormat::Svg,
|
||||||
view_mode: document.view_mode,
|
view_mode: document.view_mode,
|
||||||
|
hide_artboards: false,
|
||||||
|
for_export: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Execute the node graph
|
// Execute the node graph
|
||||||
let generation_id = self.queue_execution(network, layer_path.clone(), render_config);
|
let generation_id = self.queue_execution(network, layer_path.clone(), render_config);
|
||||||
|
|
||||||
self.futures.insert(generation_id, ExecutionContext { layer_path });
|
self.futures.insert(generation_id, ExecutionContext { layer_path, export_config: None });
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Evaluates a node graph for export
|
||||||
|
pub fn submit_document_export(&mut self, document: &mut DocumentMessageHandler, mut export_config: ExportConfig) -> Result<(), String> {
|
||||||
|
let network = document.network().clone();
|
||||||
|
|
||||||
|
// Calculate the bounding box of the region to be exported
|
||||||
|
let bounds = match export_config.bounds {
|
||||||
|
ExportBounds::AllArtwork => document.metadata().document_bounds_document_space(!export_config.transparent_background),
|
||||||
|
ExportBounds::Selection => document.metadata().selected_bounds_document_space(!export_config.transparent_background),
|
||||||
|
ExportBounds::Artboard(id) => document.metadata().bounding_box_document(id),
|
||||||
|
}
|
||||||
|
.ok_or_else(|| "No bounding box".to_string())?;
|
||||||
|
let size = bounds[1] - bounds[0];
|
||||||
|
let transform = DAffine2::from_translation(bounds[0]).inverse();
|
||||||
|
|
||||||
|
let render_config = RenderConfig {
|
||||||
|
viewport: Footprint {
|
||||||
|
transform,
|
||||||
|
resolution: (size * export_config.scale_factor).as_uvec2(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
export_format: graphene_core::application_io::ExportFormat::Svg,
|
||||||
|
view_mode: document.view_mode,
|
||||||
|
hide_artboards: export_config.transparent_background,
|
||||||
|
for_export: true,
|
||||||
|
};
|
||||||
|
export_config.size = size;
|
||||||
|
|
||||||
|
// Execute the node graph
|
||||||
|
let generation_id = self.queue_execution(network, Vec::new(), render_config);
|
||||||
|
let execution_context = ExecutionContext {
|
||||||
|
layer_path: Vec::new(),
|
||||||
|
export_config: Some(export_config),
|
||||||
|
};
|
||||||
|
self.futures.insert(generation_id, execution_context);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn export(&self, node_graph_output: TaggedValue, export_config: ExportConfig, responses: &mut VecDeque<Message>) -> Result<(), String> {
|
||||||
|
let TaggedValue::RenderOutput(graphene_std::wasm_application_io::RenderOutput::Svg(svg)) = node_graph_output else {
|
||||||
|
return Err("Incorrect render type for exportign (expected RenderOutput::Svg)".to_string());
|
||||||
|
};
|
||||||
|
|
||||||
|
let ExportConfig {
|
||||||
|
file_type,
|
||||||
|
file_name,
|
||||||
|
size,
|
||||||
|
scale_factor,
|
||||||
|
..
|
||||||
|
} = export_config;
|
||||||
|
|
||||||
|
let file_suffix = &format!(".{file_type:?}").to_lowercase();
|
||||||
|
let name = match file_name.ends_with(FILE_SAVE_SUFFIX) {
|
||||||
|
true => file_name.replace(FILE_SAVE_SUFFIX, file_suffix),
|
||||||
|
false => file_name + file_suffix,
|
||||||
|
};
|
||||||
|
|
||||||
|
if file_type == FileType::Svg {
|
||||||
|
responses.add(FrontendMessage::TriggerDownloadTextFile { document: svg, name });
|
||||||
|
} else {
|
||||||
|
let mime = file_type.to_mime().to_string();
|
||||||
|
let size = (size * scale_factor).into();
|
||||||
|
responses.add(FrontendMessage::TriggerDownloadImage { svg, name, mime, size });
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn poll_node_graph_evaluation(&mut self, document: &mut DocumentLegacy, responses: &mut VecDeque<Message>) -> Result<(), String> {
|
pub fn poll_node_graph_evaluation(&mut self, document: &mut DocumentLegacy, responses: &mut VecDeque<Message>) -> Result<(), String> {
|
||||||
let results = self.receiver.try_iter().collect::<Vec<_>>();
|
let results = self.receiver.try_iter().collect::<Vec<_>>();
|
||||||
for response in results {
|
for response in results {
|
||||||
|
|
@ -525,6 +605,13 @@ impl NodeGraphExecutor {
|
||||||
new_upstream_transforms,
|
new_upstream_transforms,
|
||||||
transform,
|
transform,
|
||||||
}) => {
|
}) => {
|
||||||
|
let node_graph_output = result.map_err(|e| format!("Node graph evaluation failed: {e:?}"))?;
|
||||||
|
let execution_context = self.futures.remove(&generation_id).ok_or_else(|| "Invalid generation ID".to_string())?;
|
||||||
|
|
||||||
|
if let Some(export_config) = execution_context.export_config {
|
||||||
|
return self.export(node_graph_output, export_config, responses);
|
||||||
|
}
|
||||||
|
|
||||||
for (&node_id, svg) in &new_thumbnails {
|
for (&node_id, svg) in &new_thumbnails {
|
||||||
if !document.document_network.nodes.contains_key(&node_id) {
|
if !document.document_network.nodes.contains_key(&node_id) {
|
||||||
warn!("Missing node");
|
warn!("Missing node");
|
||||||
|
|
@ -555,8 +642,6 @@ impl NodeGraphExecutor {
|
||||||
self.thumbnails = new_thumbnails;
|
self.thumbnails = new_thumbnails;
|
||||||
document.metadata.update_transforms(new_upstream_transforms);
|
document.metadata.update_transforms(new_upstream_transforms);
|
||||||
document.metadata.update_click_targets(new_click_targets);
|
document.metadata.update_click_targets(new_click_targets);
|
||||||
let node_graph_output = result.map_err(|e| format!("Node graph evaluation failed: {e:?}"))?;
|
|
||||||
let execution_context = self.futures.remove(&generation_id).ok_or_else(|| "Invalid generation ID".to_string())?;
|
|
||||||
responses.extend(updates);
|
responses.extend(updates);
|
||||||
self.process_node_graph_output(node_graph_output, execution_context.layer_path.clone(), transform, responses)?;
|
self.process_node_graph_output(node_graph_output, execution_context.layer_path.clone(), transform, responses)?;
|
||||||
responses.add(DocumentMessage::LayerChanged {
|
responses.add(DocumentMessage::LayerChanged {
|
||||||
|
|
@ -581,13 +666,13 @@ impl NodeGraphExecutor {
|
||||||
|
|
||||||
// Setup rendering
|
// Setup rendering
|
||||||
let mut render = SvgRender::new();
|
let mut render = SvgRender::new();
|
||||||
let render_params = RenderParams::new(ViewMode::Normal, ImageRenderMode::BlobUrl, None, false);
|
let render_params = RenderParams::new(ViewMode::Normal, ImageRenderMode::BlobUrl, None, false, false, false);
|
||||||
|
|
||||||
// Render SVG
|
// Render SVG
|
||||||
render_object.render_svg(&mut render, &render_params);
|
render_object.render_svg(&mut render, &render_params);
|
||||||
|
|
||||||
// Concatenate the defs and the SVG into one string
|
// Concatenate the defs and the SVG into one string
|
||||||
render.wrap_with_transform(transform);
|
render.wrap_with_transform(transform, None);
|
||||||
let svg = render.svg.to_string();
|
let svg = render.svg.to_string();
|
||||||
|
|
||||||
// Send to frontend
|
// Send to frontend
|
||||||
|
|
|
||||||
|
|
@ -153,6 +153,8 @@ pub struct RenderConfig {
|
||||||
pub viewport: Footprint,
|
pub viewport: Footprint,
|
||||||
pub export_format: ExportFormat,
|
pub export_format: ExportFormat,
|
||||||
pub view_mode: ViewMode,
|
pub view_mode: ViewMode,
|
||||||
|
pub hide_artboards: bool,
|
||||||
|
pub for_export: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct EditorApi<'a, Io> {
|
pub struct EditorApi<'a, Io> {
|
||||||
|
|
|
||||||
|
|
@ -90,10 +90,17 @@ impl SvgRender {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Wraps the SVG with `<svg><g transform="...">`, which allows for rotation
|
/// Wraps the SVG with `<svg><g transform="...">`, which allows for rotation
|
||||||
pub fn wrap_with_transform(&mut self, transform: DAffine2) {
|
pub fn wrap_with_transform(&mut self, transform: DAffine2, size: Option<DVec2>) {
|
||||||
let defs = &self.svg_defs;
|
let defs = &self.svg_defs;
|
||||||
|
let view_box = size
|
||||||
|
.map(|size| format!("viewbox=\"0 0 {} {}\" width=\"{}\" height=\"{}\"", size.x, size.y, size.x, size.y))
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
let svg_header = format!(r#"<svg xmlns="http://www.w3.org/2000/svg"><defs>{defs}</defs><g transform="{}">"#, format_transform_matrix(transform));
|
let svg_header = format!(
|
||||||
|
r#"<svg xmlns="http://www.w3.org/2000/svg" {}><defs>{defs}</defs><g transform="{}">"#,
|
||||||
|
view_box,
|
||||||
|
format_transform_matrix(transform)
|
||||||
|
);
|
||||||
self.svg.insert(0, svg_header.into());
|
self.svg.insert(0, svg_header.into());
|
||||||
self.svg.push("</g></svg>");
|
self.svg.push("</g></svg>");
|
||||||
}
|
}
|
||||||
|
|
@ -154,15 +161,21 @@ pub struct RenderParams {
|
||||||
pub image_render_mode: ImageRenderMode,
|
pub image_render_mode: ImageRenderMode,
|
||||||
pub culling_bounds: Option<[DVec2; 2]>,
|
pub culling_bounds: Option<[DVec2; 2]>,
|
||||||
pub thumbnail: bool,
|
pub thumbnail: bool,
|
||||||
|
/// Don't render the rectangle for an artboard to allow exporting with a transparent background.
|
||||||
|
pub hide_artboards: bool,
|
||||||
|
/// Are we exporting? Causes the text above an artboard to be hidden.
|
||||||
|
pub for_export: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderParams {
|
impl RenderParams {
|
||||||
pub fn new(view_mode: crate::vector::style::ViewMode, image_render_mode: ImageRenderMode, culling_bounds: Option<[DVec2; 2]>, thumbnail: bool) -> Self {
|
pub fn new(view_mode: crate::vector::style::ViewMode, image_render_mode: ImageRenderMode, culling_bounds: Option<[DVec2; 2]>, thumbnail: bool, hide_artboards: bool, for_export: bool) -> Self {
|
||||||
Self {
|
Self {
|
||||||
view_mode,
|
view_mode,
|
||||||
image_render_mode,
|
image_render_mode,
|
||||||
culling_bounds,
|
culling_bounds,
|
||||||
thumbnail,
|
thumbnail,
|
||||||
|
hide_artboards,
|
||||||
|
for_export,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -192,7 +205,7 @@ pub trait GraphicElementRendered {
|
||||||
fn add_click_targets(&self, click_targets: &mut Vec<ClickTarget>);
|
fn add_click_targets(&self, click_targets: &mut Vec<ClickTarget>);
|
||||||
fn to_usvg_node(&self) -> usvg::Node {
|
fn to_usvg_node(&self) -> usvg::Node {
|
||||||
let mut render = SvgRender::new();
|
let mut render = SvgRender::new();
|
||||||
let render_params = RenderParams::new(crate::vector::style::ViewMode::Normal, ImageRenderMode::BlobUrl, None, false);
|
let render_params = RenderParams::new(crate::vector::style::ViewMode::Normal, ImageRenderMode::BlobUrl, None, false, false, false);
|
||||||
self.render_svg(&mut render, &render_params);
|
self.render_svg(&mut render, &render_params);
|
||||||
render.format_svg(DVec2::ZERO, DVec2::ONE);
|
render.format_svg(DVec2::ZERO, DVec2::ONE);
|
||||||
let svg = render.svg.to_string();
|
let svg = render.svg.to_string();
|
||||||
|
|
@ -335,31 +348,34 @@ impl GraphicElementRendered for VectorData {
|
||||||
|
|
||||||
impl GraphicElementRendered for Artboard {
|
impl GraphicElementRendered for Artboard {
|
||||||
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
|
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
|
||||||
// Background
|
if !render_params.hide_artboards {
|
||||||
render.leaf_tag("rect", |attributes| {
|
// Background
|
||||||
attributes.push("class", "artboard-bg");
|
render.leaf_tag("rect", |attributes| {
|
||||||
attributes.push("fill", format!("#{}", self.background.rgba_hex()));
|
attributes.push("class", "artboard-bg");
|
||||||
attributes.push("x", self.location.x.min(self.location.x + self.dimensions.x).to_string());
|
attributes.push("fill", format!("#{}", self.background.rgba_hex()));
|
||||||
attributes.push("y", self.location.y.min(self.location.y + self.dimensions.y).to_string());
|
attributes.push("x", self.location.x.min(self.location.x + self.dimensions.x).to_string());
|
||||||
attributes.push("width", self.dimensions.x.abs().to_string());
|
attributes.push("y", self.location.y.min(self.location.y + self.dimensions.y).to_string());
|
||||||
attributes.push("height", self.dimensions.y.abs().to_string());
|
attributes.push("width", self.dimensions.x.abs().to_string());
|
||||||
});
|
attributes.push("height", self.dimensions.y.abs().to_string());
|
||||||
|
});
|
||||||
// Label
|
}
|
||||||
render.parent_tag(
|
if !render_params.hide_artboards && !render_params.for_export {
|
||||||
"text",
|
// Label
|
||||||
|attributes| {
|
render.parent_tag(
|
||||||
attributes.push("class", "artboard-label");
|
"text",
|
||||||
attributes.push("fill", "white");
|
|attributes| {
|
||||||
attributes.push("x", (self.location.x.min(self.location.x + self.dimensions.x)).to_string());
|
attributes.push("class", "artboard-label");
|
||||||
attributes.push("y", (self.location.y.min(self.location.y + self.dimensions.y) - 4).to_string());
|
attributes.push("fill", "white");
|
||||||
attributes.push("font-size", "14px");
|
attributes.push("x", (self.location.x.min(self.location.x + self.dimensions.x)).to_string());
|
||||||
},
|
attributes.push("y", (self.location.y.min(self.location.y + self.dimensions.y) - 4).to_string());
|
||||||
|render| {
|
attributes.push("font-size", "14px");
|
||||||
// TODO: Use the artboard's layer name
|
},
|
||||||
render.svg.push("Artboard");
|
|render| {
|
||||||
},
|
// TODO: Use the artboard's layer name
|
||||||
);
|
render.svg.push("Artboard");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Contents group (includes the artwork but not the background)
|
// Contents group (includes the artwork but not the background)
|
||||||
render.parent_tag(
|
render.parent_tag(
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,9 @@ use std::cell::RefCell;
|
||||||
|
|
||||||
use core::future::Future;
|
use core::future::Future;
|
||||||
use dyn_any::StaticType;
|
use dyn_any::StaticType;
|
||||||
use graphene_core::application_io::{ApplicationError, ApplicationIo, ExportFormat, ResourceFuture, SurfaceHandle, SurfaceHandleFrame, SurfaceId};
|
use graphene_core::application_io::{ApplicationError, ApplicationIo, ExportFormat, RenderConfig, ResourceFuture, SurfaceHandle, SurfaceHandleFrame, SurfaceId};
|
||||||
use graphene_core::raster::Image;
|
use graphene_core::raster::Image;
|
||||||
use graphene_core::renderer::{GraphicElementRendered, RenderParams, SvgRender};
|
use graphene_core::renderer::{GraphicElementRendered, ImageRenderMode, RenderParams, SvgRender};
|
||||||
use graphene_core::transform::Footprint;
|
use graphene_core::transform::Footprint;
|
||||||
use graphene_core::Color;
|
use graphene_core::Color;
|
||||||
use graphene_core::{
|
use graphene_core::{
|
||||||
|
|
@ -292,7 +292,7 @@ pub struct RenderNode<Data, Surface, Parameter> {
|
||||||
|
|
||||||
fn render_svg(data: impl GraphicElementRendered, mut render: SvgRender, render_params: RenderParams, footprint: Footprint) -> RenderOutput {
|
fn render_svg(data: impl GraphicElementRendered, mut render: SvgRender, render_params: RenderParams, footprint: Footprint) -> RenderOutput {
|
||||||
data.render_svg(&mut render, &render_params);
|
data.render_svg(&mut render, &render_params);
|
||||||
render.wrap_with_transform(footprint.transform);
|
render.wrap_with_transform(footprint.transform, Some(footprint.resolution.as_dvec2()));
|
||||||
RenderOutput::Svg(render.svg.to_string())
|
RenderOutput::Svg(render.svg.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -363,7 +363,9 @@ where
|
||||||
fn eval(&'input self, editor: WasmEditorApi<'a>) -> Self::Output {
|
fn eval(&'input self, editor: WasmEditorApi<'a>) -> Self::Output {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let footprint = editor.render_config.viewport;
|
let footprint = editor.render_config.viewport;
|
||||||
let render_params = RenderParams::new(editor.render_config.view_mode, graphene_core::renderer::ImageRenderMode::Base64, None, false);
|
|
||||||
|
let RenderConfig { hide_artboards, for_export, .. } = editor.render_config;
|
||||||
|
let render_params = RenderParams::new(editor.render_config.view_mode, ImageRenderMode::Base64, None, false, hide_artboards, for_export);
|
||||||
|
|
||||||
let output_format = editor.render_config.export_format;
|
let output_format = editor.render_config.export_format;
|
||||||
match output_format {
|
match output_format {
|
||||||
|
|
@ -388,10 +390,10 @@ where
|
||||||
#[inline]
|
#[inline]
|
||||||
fn eval(&'input self, editor: WasmEditorApi<'a>) -> Self::Output {
|
fn eval(&'input self, editor: WasmEditorApi<'a>) -> Self::Output {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
use graphene_core::renderer::ImageRenderMode;
|
|
||||||
|
|
||||||
let footprint = editor.render_config.viewport;
|
let footprint = editor.render_config.viewport;
|
||||||
let render_params = RenderParams::new(editor.render_config.view_mode, ImageRenderMode::Base64, None, false);
|
|
||||||
|
let RenderConfig { hide_artboards, for_export, .. } = editor.render_config;
|
||||||
|
let render_params = RenderParams::new(editor.render_config.view_mode, ImageRenderMode::Base64, None, false, hide_artboards, for_export);
|
||||||
|
|
||||||
let output_format = editor.render_config.export_format;
|
let output_format = editor.render_config.export_format;
|
||||||
match output_format {
|
match output_format {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue