Implement download/copy ImageFrame layer output (#1194)

* Implement download/copy ImageFrame layer output

* Add note about black transparency when copying

* Introspect node graph output type to conditionally disable the download button

---------

Co-authored-by: Dennis Kobert <dennis@kobert.dev>
This commit is contained in:
Keavon Chambers
2023-05-03 02:09:07 -07:00
committed by GitHub
parent 83b382c50e
commit 7c115b3b26
17 changed files with 197 additions and 52 deletions

View File

@@ -50,7 +50,23 @@ pub enum FrontendMessage {
#[serde(rename = "commitDate")]
commit_date: String,
},
TriggerFileDownload {
TriggerCopyToClipboardBlobUrl {
#[serde(rename = "blobUrl")]
blob_url: String,
},
TriggerDownloadBlobUrl {
#[serde(rename = "layerName")]
layer_name: String,
#[serde(rename = "blobUrl")]
blob_url: String,
},
TriggerDownloadRaster {
svg: String,
name: String,
mime: String,
size: (f64, f64),
},
TriggerDownloadTextFile {
document: String,
name: String,
},
@@ -107,12 +123,6 @@ pub enum FrontendMessage {
TriggerLoadPreferences,
TriggerOpenDocument,
TriggerPaste,
TriggerRasterDownload {
svg: String,
name: String,
mime: String,
size: (f64, f64),
},
TriggerRasterizeRegionBelowLayer {
#[serde(rename = "documentId")]
document_id: u64,

View File

@@ -58,6 +58,9 @@ pub enum DocumentMessage {
BooleanOperation(BooleanOperationType),
ClearLayerTree,
CommitTransaction,
CopyToClipboardLayerImageOutput {
layer_path: Vec<LayerId>,
},
CreateEmptyFolder {
container_path: Vec<LayerId>,
},
@@ -72,6 +75,9 @@ pub enum DocumentMessage {
DocumentHistoryBackward,
DocumentHistoryForward,
DocumentStructureChanged,
DownloadLayerImageOutput {
layer_path: Vec<LayerId>,
},
DuplicateSelectedLayers,
ExportDocument {
file_name: String,

View File

@@ -278,6 +278,15 @@ impl MessageHandler<DocumentMessage, (u64, &InputPreprocessorMessageHandler, &Pe
});
}
CommitTransaction => (),
CopyToClipboardLayerImageOutput { layer_path } => {
let layer = self.document_legacy.layer(&layer_path).ok();
let blob_url = layer.and_then(|layer| layer.as_layer().ok()).and_then(|layer_layer| layer_layer.as_blob_url()).cloned();
if let Some(blob_url) = blob_url {
responses.add(FrontendMessage::TriggerCopyToClipboardBlobUrl { blob_url });
}
}
CreateEmptyFolder { mut container_path } => {
let id = generate_uuid();
container_path.push(id);
@@ -326,6 +335,17 @@ impl MessageHandler<DocumentMessage, (u64, &InputPreprocessorMessageHandler, &Pe
let data_buffer: RawBuffer = self.serialize_root().as_slice().into();
responses.add(FrontendMessage::UpdateDocumentLayerTreeStructure { data_buffer })
}
DownloadLayerImageOutput { layer_path } => {
let layer = self.document_legacy.layer(&layer_path).ok();
let layer_name = layer.map(|layer| layer.name.clone().unwrap_or_else(|| "Untitled Layer".to_string()));
let blob_url = layer.and_then(|layer| layer.as_layer().ok()).and_then(|layer_layer| layer_layer.as_blob_url()).cloned();
if let (Some(layer_name), Some(blob_url)) = (layer_name, blob_url) {
responses.add(FrontendMessage::TriggerDownloadBlobUrl { layer_name, blob_url });
}
}
DuplicateSelectedLayers => {
self.backup(responses);
responses.add_front(SetSelectedLayers { replacement_selected_layers: vec![] });
@@ -353,6 +373,7 @@ impl MessageHandler<DocumentMessage, (u64, &InputPreprocessorMessageHandler, &Pe
let transform = (DAffine2::from_translation(bounds[0]) * DAffine2::from_scale(size)).inverse();
let document = self.render_document(size, transform, persistent_data, DocumentRenderMode::Root);
self.restore_document_transform(old_transforms);
let file_suffix = &format!(".{file_type:?}").to_lowercase();
@@ -362,11 +383,11 @@ impl MessageHandler<DocumentMessage, (u64, &InputPreprocessorMessageHandler, &Pe
};
if file_type == FileType::Svg {
responses.add(FrontendMessage::TriggerFileDownload { document, name });
responses.add(FrontendMessage::TriggerDownloadTextFile { document, name });
} else {
let mime = file_type.to_mime().to_string();
let size = (size * scale_factor).into();
responses.add(FrontendMessage::TriggerRasterDownload { svg: document, name, mime, size });
responses.add(FrontendMessage::TriggerDownloadRaster { svg: document, name, mime, size });
}
}
FlipSelectedLayers { flip_axis } => {
@@ -702,7 +723,7 @@ impl MessageHandler<DocumentMessage, (u64, &InputPreprocessorMessageHandler, &Pe
true => self.name.clone(),
false => self.name.clone() + FILE_SAVE_SUFFIX,
};
responses.add(FrontendMessage::TriggerFileDownload {
responses.add(FrontendMessage::TriggerDownloadTextFile {
document: self.serialize_document(),
name,
})
@@ -968,7 +989,7 @@ impl DocumentMessageHandler {
) -> Option<Message> {
// Prepare the node graph input image
let Some(node_network) = self.document_legacy.layer(&layer_path).ok().and_then(|layer| layer.as_node_graph().ok()) else {
let Some(node_network) = self.document_legacy.layer(&layer_path).ok().and_then(|layer| layer.as_layer_network().ok()) else {
return None;
};

View File

@@ -24,7 +24,7 @@ struct ModifyInputsContext<'a> {
impl<'a> ModifyInputsContext<'a> {
/// Get the node network from the document
fn new(layer: &'a [LayerId], document: &'a mut Document, node_graph: &'a mut NodeGraphMessageHandler, responses: &'a mut VecDeque<Message>) -> Option<Self> {
document.layer_mut(layer).ok().and_then(|layer| layer.as_node_graph_mut().ok()).map(|network| Self {
document.layer_mut(layer).ok().and_then(|layer| layer.as_layer_network_mut().ok()).map(|network| Self {
network,
node_graph,
responses,

View File

@@ -53,7 +53,7 @@ impl LayerBounds {
pub fn new(document: &Document, layer_path: &[u64]) -> Self {
let layer = document.layer(layer_path).ok();
let bounds = layer
.and_then(|layer| layer.as_graph_frame().ok())
.and_then(|layer| layer.as_layer().ok())
.and_then(|frame| frame.as_vector_data().as_ref().map(|vector| vector.nonzero_bounding_box()))
.unwrap_or([DVec2::ZERO, DVec2::ONE]);
let bounds_transform = DAffine2::IDENTITY;

View File

@@ -118,11 +118,14 @@ pub struct NodeGraphMessageHandler {
impl NodeGraphMessageHandler {
fn get_root_network<'a>(&self, document: &'a Document) -> Option<&'a graph_craft::document::NodeNetwork> {
self.layer_path.as_ref().and_then(|path| document.layer(path).ok()).and_then(|layer| layer.as_node_graph().ok())
self.layer_path.as_ref().and_then(|path| document.layer(path).ok()).and_then(|layer| layer.as_layer_network().ok())
}
fn get_root_network_mut<'a>(&self, document: &'a mut Document) -> Option<&'a mut graph_craft::document::NodeNetwork> {
self.layer_path.as_ref().and_then(|path| document.layer_mut(path).ok()).and_then(|layer| layer.as_node_graph_mut().ok())
self.layer_path
.as_ref()
.and_then(|path| document.layer_mut(path).ok())
.and_then(|layer| layer.as_layer_network_mut().ok())
}
/// Get the active graph_craft NodeNetwork struct
@@ -726,7 +729,7 @@ impl MessageHandler<NodeGraphMessage, (&mut Document, &mut dyn Iterator<Item = &
let network = document
.layer_mut(&layer_path)
.ok()
.and_then(|layer| layer.as_node_graph_mut().ok())
.and_then(|layer| layer.as_layer_network_mut().ok())
.and_then(|network| network.nested_network_mut(node_path));
if let Some(network) = network {

View File

@@ -274,7 +274,7 @@ fn static_nodes() -> Vec<DocumentNodeType> {
default: NodeInput::value(TaggedValue::ImageFrame(ImageFrame::empty()), true),
}],
outputs: vec![],
properties: |_document_node, _node_id, _context| node_properties::string_properties("The graph's output is drawn in the layer"),
properties: node_properties::output_properties,
},
DocumentNodeType {
name: "Image Frame",

View File

@@ -7,11 +7,12 @@ use document_legacy::Operation;
use glam::DVec2;
use graph_craft::document::value::TaggedValue;
use graph_craft::document::{DocumentNode, NodeId, NodeInput};
use graph_craft::imaginate_input::*;
use graph_craft::{concrete, imaginate_input::*};
use graphene_core::raster::{BlendMode, Color, ImageFrame, LuminanceCalculation, RedGreenBlue, RelativeAbsolute, SelectiveColorChoice};
use graphene_core::text::Font;
use graphene_core::vector::style::{FillType, GradientType, LineCap, LineJoin};
use graphene_core::EditorApi;
use graphene_core::{Cow, Type, TypeDescriptor};
use super::document_node_types::NodePropertiesContext;
use super::{FrontendGraphDataType, IMAGINATE_NODE};
@@ -512,6 +513,37 @@ pub fn blend_properties(document_node: &DocumentNode, node_id: NodeId, _context:
vec![backdrop, blend_mode, LayoutGroup::Row { widgets: opacity }]
}
pub fn output_properties(_document_node: &DocumentNode, _node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
let output_type = context.executor.previous_output_type(context.layer_path);
let raster_output_type = concrete!(ImageFrame<Color>);
let disabled = match output_type {
Some(output_type) => output_type != raster_output_type,
None => true,
};
let layer_path_1 = context.layer_path.to_vec();
let layer_path_2 = context.layer_path.to_vec();
let label = TextLabel::new("The graph's output is drawn in the layer").widget_holder();
let download_button = TextButton::new("Download Render Output")
.tooltip("Download the rendered image output as a PNG file")
.disabled(disabled)
.on_update(move |_| DocumentMessage::DownloadLayerImageOutput { layer_path: layer_path_1.clone() }.into())
.widget_holder();
let copy_button = TextButton::new("Copy Render Output")
.tooltip("Copy the rendered image output to the clipboard")
.disabled(disabled)
.on_update(move |_| DocumentMessage::CopyToClipboardLayerImageOutput { layer_path: layer_path_2.clone() }.into())
.widget_holder();
vec![
LayoutGroup::Row { widgets: vec![label] },
LayoutGroup::Row {
widgets: vec![download_button, WidgetHolder::related_separator(), copy_button],
},
]
}
pub fn mask_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
let mask = color_widget(document_node, node_id, 1, "Stencil", ColorInput::default(), true);

View File

@@ -322,7 +322,7 @@ fn load_existing_points(document: &DocumentMessageHandler) -> Option<(Vec<LayerI
return None;
}
let layer_path = document.selected_layers().next()?.to_vec();
let network = document.document_legacy.layer(&layer_path).ok().and_then(|layer| layer.as_node_graph().ok())?;
let network = document.document_legacy.layer(&layer_path).ok().and_then(|layer| layer.as_layer_network().ok())?;
let brush_node = network.nodes.get(&0)?;
if brush_node.implementation != DocumentNodeImplementation::Unresolved("graphene_std::brush::BrushNode".into()) {
return None;

View File

@@ -843,7 +843,7 @@ impl Fsm for SelectToolFsmState {
// Check that only one layer is selected
if selected_layers.next().is_none() {
if let Ok(layer) = document.document_legacy.layer(layer_path) {
if let Ok(network) = layer.as_node_graph() {
if let Ok(network) = layer.as_layer_network() {
if network.nodes.values().any(|node| node.name == "Text") {
responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Text });
responses.add(TextToolMessage::EditSelected);

View File

@@ -380,7 +380,7 @@ fn resize_overlays(overlays: &mut Vec<Vec<LayerId>>, responses: &mut VecDeque<Me
fn update_overlays(document: &DocumentMessageHandler, tool_data: &mut TextToolData, responses: &mut VecDeque<Message>, render_data: &RenderData) {
let get_bounds = |layer: &Layer, path: &[LayerId], document: &DocumentMessageHandler, render_data: &RenderData| {
let node_graph = layer.as_node_graph().ok()?;
let node_graph = layer.as_layer_network().ok()?;
let node_id = get_text_node_id(node_graph)?;
let document_node = node_graph.nodes.get(&node_id)?;
let (text, font, font_size) = TextToolData::extract_text_node_inputs(document_node)?;
@@ -410,7 +410,7 @@ fn update_overlays(document: &DocumentMessageHandler, tool_data: &mut TextToolDa
fn get_network<'a>(layer_path: &[LayerId], document: &'a DocumentMessageHandler) -> Option<&'a NodeNetwork> {
let layer = document.document_legacy.layer(layer_path).ok()?;
layer.as_node_graph().ok()
layer.as_layer_network().ok()
}
fn get_text_node_id(network: &NodeNetwork) -> Option<NodeId> {

View File

@@ -20,7 +20,9 @@ use std::borrow::Cow;
#[derive(Debug, Clone, Default)]
pub struct NodeGraphExecutor {
executor: DynamicExecutor,
pub(crate) executor: DynamicExecutor,
// TODO: This is a memory leak since layers are never removed
pub(crate) last_output_type: HashMap<Vec<LayerId>, Option<Type>>,
}
impl NodeGraphExecutor {
@@ -55,6 +57,10 @@ impl NodeGraphExecutor {
self.executor.introspect(path).flatten()
}
pub fn previous_output_type(&self, path: &[LayerId]) -> Option<Type> {
self.last_output_type.get(path).cloned().flatten()
}
/// Computes an input for a node in the graph
pub fn compute_input<T: dyn_any::StaticType>(&mut self, old_network: &NodeNetwork, node_path: &[NodeId], mut input_index: usize, editor_api: Cow<EditorApi<'_>>) -> Result<T, String> {
let mut network = old_network.clone();
@@ -272,11 +278,13 @@ impl NodeGraphExecutor {
// Update the cached vector data on the layer
let vector_data: VectorData = dyn_any::downcast(boxed_node_graph_output).map(|v| *v)?;
let transform = vector_data.transform.to_cols_array();
self.last_output_type.insert(layer_path.clone(), Some(concrete!(VectorData)));
responses.add(Operation::SetLayerTransform { path: layer_path.clone(), transform });
responses.add(Operation::SetVectorData { path: layer_path, vector_data });
} else {
// Attempt to downcast to an image frame
let ImageFrame { image, transform } = dyn_any::downcast(boxed_node_graph_output).map(|image_frame| *image_frame)?;
self.last_output_type.insert(layer_path.clone(), Some(concrete!(ImageFrame<Color>)));
// Don't update the frame's transform if the new transform is DAffine2::ZERO.
let transform = (!transform.abs_diff_eq(DAffine2::ZERO, f64::EPSILON)).then_some(transform.to_cols_array());