Add viewing/editing layer names, add Blend Mode node, and clean up Layer node (#1489)

This commit is contained in:
Keavon Chambers
2023-12-07 15:10:47 -08:00
committed by GitHub
parent b7e304a708
commit 60a9c27bf1
43 changed files with 437 additions and 463 deletions
+4 -1
View File
@@ -43,5 +43,8 @@
// VS Code config // VS Code config
"html.format.wrapLineLength": 200, "html.format.wrapLineLength": 200,
"files.eol": "\n", "files.eol": "\n",
"files.insertFinalNewline": true "files.insertFinalNewline": true,
"files.associations": {
"*.graphite": "json"
}
} }
@@ -267,60 +267,6 @@ def update_layer(layer, indent, layer_node_id, next_id, opacity):
"lambda": False "lambda": False
} }
}, },
{
"Network": {
"Concrete": {
"name": "alloc::string::String",
"size": 12,
"align": 4
}
}
},
{
"Network": {
"Concrete": {
"name": "graphene_core::raster::adjustments::BlendMode",
"size": 4,
"align": 4
}
}
},
{
"Network": {
"Concrete": {
"name": "f32",
"size": 4,
"align": 4
}
}
},
{
"Network": {
"Concrete": {
"name": "bool",
"size": 1,
"align": 1
}
}
},
{
"Network": {
"Concrete": {
"name": "bool",
"size": 1,
"align": 1
}
}
},
{
"Network": {
"Concrete": {
"name": "bool",
"size": 1,
"align": 1
}
}
},
{ {
"Network": { "Network": {
"Fn": [ "Fn": [
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-7
View File
@@ -1031,13 +1031,6 @@ impl Document {
layer.visible = visible; layer.visible = visible;
Some([vec![DocumentChanged], update_thumbnails_upstream(&path)].concat()) Some([vec![DocumentChanged], update_thumbnails_upstream(&path)].concat())
} }
Operation::SetLayerName { path, name } => {
self.mark_as_dirty(&path)?;
let layer = self.layer_mut(&path)?;
layer.name = if name.as_str() == "" { None } else { Some(name) };
Some(vec![LayerChanged { path }])
}
Operation::SetLayerBlendMode { path, blend_mode } => { Operation::SetLayerBlendMode { path, blend_mode } => {
self.mark_as_dirty(&path)?; self.mark_as_dirty(&path)?;
self.layer_mut(&path)?.blend_mode = blend_mode; self.layer_mut(&path)?.blend_mode = blend_mode;
+6 -4
View File
@@ -215,10 +215,12 @@ impl DocumentMetadata {
} }
fn first_child_layer<'a>(graph: &'a NodeNetwork, node: &DocumentNode) -> Option<(&'a DocumentNode, NodeId)> { fn first_child_layer<'a>(graph: &'a NodeNetwork, node: &DocumentNode) -> Option<(&'a DocumentNode, NodeId)> {
graph.primary_flow_from_node(Some(node.inputs[0].as_node()?)).find(|(node, _)| node.name == "Layer") graph.primary_flow_from_node(Some(node.inputs[0].as_node()?)).find(|(node, _)| node.is_layer())
} }
fn sibling_below<'a>(graph: &'a NodeNetwork, node: &DocumentNode) -> Option<(&'a DocumentNode, NodeId)> { fn sibling_below<'a>(graph: &'a NodeNetwork, node: &DocumentNode) -> Option<(&'a DocumentNode, NodeId)> {
node.inputs[7].as_node().and_then(|id| graph.nodes.get(&id).filter(|node| node.name == "Layer").map(|node| (node, id))) let construct_layer_node = &node.inputs[1];
construct_layer_node.as_node().and_then(|id| graph.nodes.get(&id).filter(|node| node.is_layer()).map(|node| (node, id)))
} }
// transforms // transforms
@@ -265,7 +267,7 @@ pub fn is_folder(layer: LayerNodeIdentifier, network: &NodeNetwork) -> bool {
|| network || network
.primary_flow_from_node(Some(layer.to_node())) .primary_flow_from_node(Some(layer.to_node()))
.skip(1) .skip(1)
.any(|(node, _)| node.name == "Artboard" || node.name == "Layer") .any(|(node, _)| node.name == "Artboard" || node.is_layer())
} }
// click targets // click targets
@@ -640,7 +642,7 @@ pub struct NodeRelations {
} }
fn is_layer_node(node: NodeId, network: &NodeNetwork) -> bool { fn is_layer_node(node: NodeId, network: &NodeNetwork) -> bool {
node == LayerNodeIdentifier::ROOT.to_node() || network.nodes.get(&node).is_some_and(|node| node.name == "Layer") node == LayerNodeIdentifier::ROOT.to_node() || network.nodes.get(&node).is_some_and(|node| node.is_layer())
} }
#[test] #[test]
-4
View File
@@ -107,10 +107,6 @@ pub enum Operation {
path: Vec<LayerId>, path: Vec<LayerId>,
visible: bool, visible: bool,
}, },
SetLayerName {
path: Vec<LayerId>,
name: String,
},
SetLayerPreserveAspect { SetLayerPreserveAspect {
layer_path: Vec<LayerId>, layer_path: Vec<LayerId>,
preserve_aspect: bool, preserve_aspect: bool,
@@ -159,10 +159,6 @@ pub enum DocumentMessage {
layer_path: Vec<LayerId>, layer_path: Vec<LayerId>,
set_expanded: bool, set_expanded: bool,
}, },
SetLayerName {
layer_path: Vec<LayerId>,
name: String,
},
SetOpacityForSelectedLayers { SetOpacityForSelectedLayers {
opacity: f64, opacity: f64,
}, },
@@ -82,7 +82,7 @@ impl Default for DocumentMessageHandler {
document_legacy: DocumentLegacy::default(), document_legacy: DocumentLegacy::default(),
saved_document_identifier: 0, saved_document_identifier: 0,
auto_saved_document_identifier: 0, auto_saved_document_identifier: 0,
name: String::from("Untitled Document"), name: DEFAULT_DOCUMENT_NAME.to_string(),
version: GRAPHITE_DOCUMENT_VERSION.to_string(), version: GRAPHITE_DOCUMENT_VERSION.to_string(),
commit_hash: crate::application::GRAPHITE_GIT_COMMIT_HASH.to_string(), commit_hash: crate::application::GRAPHITE_GIT_COMMIT_HASH.to_string(),
@@ -211,7 +211,7 @@ impl MessageHandler<DocumentMessage, DocumentInputs<'_>> for DocumentMessageHand
// Messages // Messages
AbortTransaction => { AbortTransaction => {
if !self.undo_in_progress { if !self.undo_in_progress {
self.undo(responses).unwrap_or_else(|e| warn!("{e}")); self.undo(responses);
responses.extend([RenderDocument.into(), DocumentStructureChanged.into()]); responses.extend([RenderDocument.into(), DocumentStructureChanged.into()]);
} }
} }
@@ -329,8 +329,8 @@ impl MessageHandler<DocumentMessage, DocumentInputs<'_>> for DocumentMessageHand
responses.add_front(DocumentMessage::DirtyRenderDocument); responses.add_front(DocumentMessage::DirtyRenderDocument);
} }
} }
DocumentHistoryBackward => self.undo(responses).unwrap_or_else(|e| warn!("{e}")), DocumentHistoryBackward => self.undo(responses),
DocumentHistoryForward => self.redo(responses).unwrap_or_else(|e| warn!("{e}")), DocumentHistoryForward => self.redo(responses),
DocumentStructureChanged => { DocumentStructureChanged => {
let data_buffer: RawBuffer = self.serialize_root().as_slice().into(); let data_buffer: RawBuffer = self.serialize_root().as_slice().into();
responses.add(FrontendMessage::UpdateDocumentLayerTreeStructure { data_buffer }) responses.add(FrontendMessage::UpdateDocumentLayerTreeStructure { data_buffer })
@@ -577,7 +577,11 @@ impl MessageHandler<DocumentMessage, DocumentInputs<'_>> for DocumentMessageHand
responses.add(DocumentMessage::StartTransaction); responses.add(DocumentMessage::StartTransaction);
let image_frame = ImageFrame { image, transform: DAffine2::IDENTITY }; let image_frame = ImageFrame {
image,
transform: DAffine2::IDENTITY,
blend_mode: BlendMode::Normal,
};
use crate::messages::tool::common_functionality::graph_modification_utils; use crate::messages::tool::common_functionality::graph_modification_utils;
let layer = graph_modification_utils::new_image_layer(image_frame, generate_uuid(), self.new_layer_parent(), responses); let layer = graph_modification_utils::new_image_layer(image_frame, generate_uuid(), self.new_layer_parent(), responses);
@@ -649,7 +653,7 @@ impl MessageHandler<DocumentMessage, DocumentInputs<'_>> for DocumentMessageHand
}); });
} }
RollbackTransaction => { RollbackTransaction => {
self.rollback(responses).unwrap_or_else(|e| warn!("{e}")); self.rollback(responses);
responses.extend([RenderDocument.into(), DocumentStructureChanged.into()]); responses.extend([RenderDocument.into(), DocumentStructureChanged.into()]);
} }
SaveDocument => { SaveDocument => {
@@ -771,15 +775,6 @@ impl MessageHandler<DocumentMessage, DocumentInputs<'_>> for DocumentMessageHand
responses.add(DocumentStructureChanged); responses.add(DocumentStructureChanged);
responses.add(LayerChanged { affected_layer_path: layer_path }) responses.add(LayerChanged { affected_layer_path: layer_path })
} }
SetLayerName { layer_path, name } => {
if let Some(layer) = self.layer_panel_entry_from_path(&layer_path, &render_data) {
// Only save the history state if the name actually changed to something different
if layer.name != name {
self.backup(responses);
responses.add(DocumentOperation::SetLayerName { path: layer_path, name });
}
}
}
SetOpacityForSelectedLayers { opacity } => { SetOpacityForSelectedLayers { opacity } => {
self.backup(responses); self.backup(responses);
let opacity = opacity.clamp(0., 1.); let opacity = opacity.clamp(0., 1.);
@@ -1238,9 +1233,9 @@ impl DocumentMessageHandler {
}); });
} }
pub fn rollback(&mut self, responses: &mut VecDeque<Message>) -> Result<(), EditorError> { pub fn rollback(&mut self, responses: &mut VecDeque<Message>) {
self.backup(responses); self.backup(responses);
self.undo(responses) self.undo(responses);
// TODO: Consider if we should check if the document is saved // TODO: Consider if we should check if the document is saved
} }
@@ -1257,72 +1252,62 @@ impl DocumentMessageHandler {
DocumentSave { document, layer_metadata } DocumentSave { document, layer_metadata }
} }
pub fn undo(&mut self, responses: &mut VecDeque<Message>) -> Result<(), EditorError> { pub fn undo(&mut self, responses: &mut VecDeque<Message>) {
// Push the UpdateOpenDocumentsList message to the bus in order to update the save status of the open documents // Push the UpdateOpenDocumentsList message to the bus in order to update the save status of the open documents
responses.add(PortfolioMessage::UpdateOpenDocumentsList); responses.add(PortfolioMessage::UpdateOpenDocumentsList);
let selected_paths: Vec<Vec<LayerId>> = self.selected_layers().map(|path| path.to_vec()).collect(); let selected_paths: Vec<Vec<LayerId>> = self.selected_layers().map(|path| path.to_vec()).collect();
match self.document_undo_history.pop_back() { if let Some(DocumentSave { document, layer_metadata }) = self.document_undo_history.pop_back() {
Some(DocumentSave { document, layer_metadata }) => { // Update the currently displayed layer on the Properties panel if the selection changes after an undo action
// Update the currently displayed layer on the Properties panel if the selection changes after an undo action // Also appropriately update the Properties panel if an undo action results in a layer being deleted
// Also appropriately update the Properties panel if an undo action results in a layer being deleted let prev_selected_paths: Vec<Vec<LayerId>> = layer_metadata.iter().filter_map(|(layer_id, metadata)| metadata.selected.then_some(layer_id.clone())).collect();
let prev_selected_paths: Vec<Vec<LayerId>> = layer_metadata.iter().filter_map(|(layer_id, metadata)| metadata.selected.then_some(layer_id.clone())).collect();
if prev_selected_paths != selected_paths { if prev_selected_paths != selected_paths {
responses.add(BroadcastEvent::SelectionChanged); responses.add(BroadcastEvent::SelectionChanged);
}
let document_save = self.replace_document(DocumentSave { document, layer_metadata });
self.document_redo_history.push_back(document_save);
if self.document_redo_history.len() > crate::consts::MAX_UNDO_HISTORY_LEN {
self.document_redo_history.pop_front();
}
for layer in self.layer_metadata.keys() {
responses.add(DocumentMessage::LayerChanged { affected_layer_path: layer.clone() })
}
responses.add(NodeGraphMessage::SendGraph { should_rerender: true });
Ok(())
} }
None => Err(EditorError::NoTransactionInProgress),
let document_save = self.replace_document(DocumentSave { document, layer_metadata });
self.document_redo_history.push_back(document_save);
if self.document_redo_history.len() > crate::consts::MAX_UNDO_HISTORY_LEN {
self.document_redo_history.pop_front();
}
for layer in self.layer_metadata.keys() {
responses.add(DocumentMessage::LayerChanged { affected_layer_path: layer.clone() })
}
responses.add(NodeGraphMessage::SendGraph { should_rerender: true });
} }
} }
pub fn redo(&mut self, responses: &mut VecDeque<Message>) -> Result<(), EditorError> { pub fn redo(&mut self, responses: &mut VecDeque<Message>) {
// Push the UpdateOpenDocumentsList message to the bus in order to update the save status of the open documents // Push the UpdateOpenDocumentsList message to the bus in order to update the save status of the open documents
responses.add(PortfolioMessage::UpdateOpenDocumentsList); responses.add(PortfolioMessage::UpdateOpenDocumentsList);
let selected_paths: Vec<Vec<LayerId>> = self.selected_layers().map(|path| path.to_vec()).collect(); let selected_paths: Vec<Vec<LayerId>> = self.selected_layers().map(|path| path.to_vec()).collect();
match self.document_redo_history.pop_back() { if let Some(DocumentSave { document, layer_metadata }) = self.document_redo_history.pop_back() {
Some(DocumentSave { document, layer_metadata }) => { // Update currently displayed layer on property panel if selection changes after redo action
// Update currently displayed layer on property panel if selection changes after redo action // Also appropriately update property panel if redo action results in a layer being added
// Also appropriately update property panel if redo action results in a layer being added let next_selected_paths: Vec<Vec<LayerId>> = layer_metadata.iter().filter_map(|(layer_id, metadata)| metadata.selected.then_some(layer_id.clone())).collect();
let next_selected_paths: Vec<Vec<LayerId>> = layer_metadata.iter().filter_map(|(layer_id, metadata)| metadata.selected.then_some(layer_id.clone())).collect();
if next_selected_paths != selected_paths { if next_selected_paths != selected_paths {
responses.add(BroadcastEvent::SelectionChanged); responses.add(BroadcastEvent::SelectionChanged);
}
let document_save = self.replace_document(DocumentSave { document, layer_metadata });
self.document_undo_history.push_back(document_save);
if self.document_undo_history.len() > crate::consts::MAX_UNDO_HISTORY_LEN {
self.document_undo_history.pop_front();
}
for layer in self.layer_metadata.keys() {
responses.add(DocumentMessage::LayerChanged { affected_layer_path: layer.clone() })
}
responses.add(NodeGraphMessage::SendGraph { should_rerender: true });
Ok(())
} }
None => Err(EditorError::NoTransactionInProgress),
let document_save = self.replace_document(DocumentSave { document, layer_metadata });
self.document_undo_history.push_back(document_save);
if self.document_undo_history.len() > crate::consts::MAX_UNDO_HISTORY_LEN {
self.document_undo_history.pop_front();
}
for layer in self.layer_metadata.keys() {
responses.add(DocumentMessage::LayerChanged { affected_layer_path: layer.clone() })
}
responses.add(NodeGraphMessage::SendGraph { should_rerender: true });
} }
} }
@@ -52,7 +52,7 @@ impl<'a> ModifyInputsContext<'a> {
error!("Tried to modify root layer"); error!("Tried to modify root layer");
return None; return None;
}; };
while document.network.nodes.get(&id)?.name != "Layer" { while !document.network.nodes.get(&id)?.is_layer() {
id = document.outwards_links.get(&id)?.first().copied()?; id = document.outwards_links.get(&id)?.first().copied()?;
} }
document.layer_node = Some(id); document.layer_node = Some(id);
@@ -112,7 +112,7 @@ impl<'a> ModifyInputsContext<'a> {
// Locate the node output of the first sibling layer to the new layer // Locate the node output of the first sibling layer to the new layer
if let Some((node_id, output_index)) = self.skip_artboards(&mut output) { if let Some((node_id, output_index)) = self.skip_artboards(&mut output) {
let sibling_node = self.network.nodes.get(&node_id)?; let sibling_node = self.network.nodes.get(&node_id)?;
if sibling_node.name == "Layer" { if sibling_node.is_layer() {
// There is already a layer node // There is already a layer node
sibling_layer = Some(NodeOutput::new(node_id, 0)); sibling_layer = Some(NodeOutput::new(node_id, 0));
} else { } else {
@@ -125,8 +125,8 @@ impl<'a> ModifyInputsContext<'a> {
// Skip some layer nodes // Skip some layer nodes
for _ in 0..skip_layer_nodes { for _ in 0..skip_layer_nodes {
if let Some(old_sibling) = &sibling_layer { if let Some(old_sibling) = &sibling_layer {
output = NodeOutput::new(old_sibling.node_id, 7); output = NodeOutput::new(old_sibling.node_id, 1);
sibling_layer = self.network.nodes.get(&old_sibling.node_id)?.inputs[7].as_node().map(|node| NodeOutput::new(node, 0)); sibling_layer = self.network.nodes.get(&old_sibling.node_id)?.inputs[1].as_node().map(|node| NodeOutput::new(node, 0));
shift = IVec2::new(0, 3); shift = IVec2::new(0, 3);
} }
} }
@@ -139,14 +139,14 @@ impl<'a> ModifyInputsContext<'a> {
// Create node // Create node
let layer_node = resolve_document_node_type("Layer").expect("Layer node").default_document_node(); let layer_node = resolve_document_node_type("Layer").expect("Layer node").default_document_node();
let new_id = if let Some(sibling_layer) = sibling_layer { let new_id = if let Some(sibling_layer) = sibling_layer {
self.insert_between(new_id, sibling_layer, output, layer_node, 7, 0, shift) self.insert_between(new_id, sibling_layer, output, layer_node, 1, 0, shift)
} else { } else {
self.insert_node_before(new_id, output.node_id, output.node_output_index, layer_node, shift) self.insert_node_before(new_id, output.node_id, output.node_output_index, layer_node, shift)
}; };
// Update the document metadata structure // Update the document metadata structure
if let Some(new_id) = new_id { if let Some(new_id) = new_id {
let parent = if self.network.nodes.get(&output_node_id).is_some_and(|node| node.name == "Layer") { let parent = if self.network.nodes.get(&output_node_id).is_some_and(|node| node.is_layer()) {
LayerNodeIdentifier::new(output_node_id, self.network) LayerNodeIdentifier::new(output_node_id, self.network)
} else { } else {
LayerNodeIdentifier::ROOT LayerNodeIdentifier::ROOT
@@ -498,7 +498,7 @@ impl<'a> ModifyInputsContext<'a> {
LayerNodeIdentifier::new(id, self.network).delete(self.document_metadata); LayerNodeIdentifier::new(id, self.network).delete(self.document_metadata);
let new_input = node.inputs[7].clone(); let new_input = node.inputs[1].clone();
let deleted_position = node.metadata.position; let deleted_position = node.metadata.position;
for post_node in self.outwards_links.get(&id).unwrap_or(&Vec::new()) { for post_node in self.outwards_links.get(&id).unwrap_or(&Vec::new()) {
@@ -103,6 +103,14 @@ pub enum NodeGraphMessage {
node_id: NodeId, node_id: NodeId,
hidden: bool, hidden: bool,
}, },
SetName {
node_id: NodeId,
name: String,
},
SetNameImpl {
node_id: NodeId,
name: String,
},
TogglePreview { TogglePreview {
node_id: NodeId, node_id: NodeId,
}, },
@@ -73,9 +73,11 @@ pub struct FrontendGraphOutput {
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]
pub struct FrontendNode { pub struct FrontendNode {
#[serde(rename = "isLayer")]
pub is_layer: bool,
pub id: graph_craft::document::NodeId, pub id: graph_craft::document::NodeId,
#[serde(rename = "displayName")] pub name: String,
pub display_name: String, pub identifier: String,
#[serde(rename = "primaryInput")] #[serde(rename = "primaryInput")]
pub primary_input: Option<FrontendGraphInput>, pub primary_input: Option<FrontendGraphInput>,
#[serde(rename = "exposedInputs")] #[serde(rename = "exposedInputs")]
@@ -116,7 +118,7 @@ impl FrontendNodeType {
} }
} }
#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct NodeGraphMessageHandler { pub struct NodeGraphMessageHandler {
pub layer_path: Option<Vec<LayerId>>, pub layer_path: Option<Vec<LayerId>>,
pub network: Vec<NodeId>, pub network: Vec<NodeId>,
@@ -125,6 +127,21 @@ pub struct NodeGraphMessageHandler {
pub widgets: [LayoutGroup; 2], pub widgets: [LayoutGroup; 2],
} }
impl Default for NodeGraphMessageHandler {
fn default() -> Self {
// TODO: Replace this with an "Add Node" button, also next to an "Add Layer" button
let add_nodes_label = TextLabel::new("Right Click Graph to Add Nodes").italic(true).widget_holder();
let add_nodes_label_row = LayoutGroup::Row { widgets: vec![add_nodes_label] };
Self {
layer_path: None,
network: Vec::new(),
has_selection: false,
widgets: [add_nodes_label_row, LayoutGroup::default()],
}
}
}
impl Into<Message> for document_legacy::document_metadata::SelectionChanged { impl Into<Message> for document_legacy::document_metadata::SelectionChanged {
fn into(self) -> Message { fn into(self) -> Message {
BroadcastMessage::TriggerEvent(BroadcastEvent::SelectionChanged).into() BroadcastMessage::TriggerEvent(BroadcastEvent::SelectionChanged).into()
@@ -140,63 +157,11 @@ impl NodeGraphMessageHandler {
}); });
} }
/// Collect the addresses of the currently viewed nested node e.g. Root -> MyFunFilter -> Exposure
fn collect_nested_addresses(&mut self, document: &Document, document_name: &str, responses: &mut VecDeque<Message>) {
let layer_if_selected = self.layer_path.as_ref().and_then(|path| document.layer(path).ok());
// Build path list for the layer, or otherwise the root document
let path_root = match layer_if_selected {
Some(layer) => layer.name.as_deref().unwrap_or("Untitled Layer"),
None => document_name,
};
let mut path = vec![path_root.to_string()];
let (icon, tooltip) = match layer_if_selected {
Some(_) => ("Layer", "Layer"),
None => ("File", "Document"),
};
let mut network = Some(&document.document_network);
for node_id in &self.network {
let node = network.and_then(|network| network.nodes.get(node_id));
if let Some(DocumentNode { name, .. }) = node {
path.push(name.clone());
}
network = node.and_then(|node| node.implementation.get_network());
}
let nesting = path.len();
// Update UI
self.widgets[0] = LayoutGroup::Row {
widgets: vec![
IconLabel::new(icon).tooltip(tooltip).widget_holder(),
Separator::new(SeparatorType::Unrelated).widget_holder(),
BreadcrumbTrailButtons::new(path.clone())
.on_update(move |input: &u64| {
NodeGraphMessage::ExitNestedNetwork {
depth_of_nesting: nesting - (*input as usize) - 1,
}
.into()
})
.widget_holder(),
],
};
self.send_node_bar_layout(responses);
}
/// Updates the buttons for disable and preview /// Updates the buttons for disable and preview
fn update_selection_action_buttons(&mut self, document: &Document, responses: &mut VecDeque<Message>) { fn update_selection_action_buttons(&mut self, document: &Document, responses: &mut VecDeque<Message>) {
if let Some(network) = document.document_network.nested_network(&self.network) { if let Some(network) = document.document_network.nested_network(&self.network) {
let mut widgets = Vec::new(); let mut widgets = Vec::new();
// TODO: Replace this with an add node button
let add_nodes_label = TextLabel::new("Right Click Graph to Add Nodes").italic(true).widget_holder();
widgets.push(add_nodes_label);
// Don't allow disabling input or output nodes // Don't allow disabling input or output nodes
let mut selected_nodes = document.metadata.selected_nodes().filter(|&&id| !network.inputs.contains(&id) && !network.original_outputs_contain(id)); let mut selected_nodes = document.metadata.selected_nodes().filter(|&&id| !network.inputs.contains(&id) && !network.original_outputs_contain(id));
@@ -330,8 +295,10 @@ impl NodeGraphMessageHandler {
let _graph_identifier = GraphIdentifier::new(layer_id); let _graph_identifier = GraphIdentifier::new(layer_id);
nodes.push(FrontendNode { nodes.push(FrontendNode {
is_layer: node.is_layer(),
id: *id, id: *id,
display_name: node.name.clone(), name: node.alias.clone(),
identifier: node.name.clone(),
primary_input, primary_input,
exposed_inputs, exposed_inputs,
primary_output, primary_output,
@@ -354,7 +321,7 @@ impl NodeGraphMessageHandler {
fn remove_references_from_network(network: &mut NodeNetwork, deleting_node_id: NodeId, reconnect: bool) -> bool { fn remove_references_from_network(network: &mut NodeNetwork, deleting_node_id: NodeId, reconnect: bool) -> bool {
if network.inputs.contains(&deleting_node_id) { if network.inputs.contains(&deleting_node_id) {
warn!("Deleting input node"); warn!("Deleting input node!");
return false; return false;
} }
if network.outputs_contain(deleting_node_id) { if network.outputs_contain(deleting_node_id) {
@@ -368,7 +335,7 @@ impl NodeGraphMessageHandler {
// Check whether the being-deleted node's first (primary) input is a node // Check whether the being-deleted node's first (primary) input is a node
if let Some(node) = network.nodes.get(&deleting_node_id) { if let Some(node) = network.nodes.get(&deleting_node_id) {
// Reconnect to the node below when deleting a layer node. // Reconnect to the node below when deleting a layer node.
let reconnect_from_input_index = if node.name == "Layer" { 7 } else { 0 }; let reconnect_from_input_index = if node.is_layer() { 1 } else { 0 };
if matches!(&node.inputs.get(reconnect_from_input_index), Some(NodeInput::Node { .. })) { if matches!(&node.inputs.get(reconnect_from_input_index), Some(NodeInput::Node { .. })) {
reconnect_to_input = Some(node.inputs[reconnect_from_input_index].clone()); reconnect_to_input = Some(node.inputs[reconnect_from_input_index].clone());
} }
@@ -614,7 +581,6 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
if let Some(network) = document.document_network.nested_network(&self.network) { if let Some(network) = document.document_network.nested_network(&self.network) {
Self::send_graph(network, &self.layer_path, graph_view_overlay_open, responses); Self::send_graph(network, &self.layer_path, graph_view_overlay_open, responses);
} }
self.collect_nested_addresses(document, data.document_name, responses);
self.update_selected(document, responses); self.update_selected(document, responses);
} }
NodeGraphMessage::DuplicateSelectedNodes => { NodeGraphMessage::DuplicateSelectedNodes => {
@@ -651,7 +617,6 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
if let Some(network) = document.document_network.nested_network(&self.network) { if let Some(network) = document.document_network.nested_network(&self.network) {
Self::send_graph(network, &self.layer_path, graph_view_overlay_open, responses); Self::send_graph(network, &self.layer_path, graph_view_overlay_open, responses);
} }
self.collect_nested_addresses(document, data.document_name, responses);
self.update_selected(document, responses); self.update_selected(document, responses);
} }
NodeGraphMessage::ExposeInput { node_id, input_index, new_exposed } => { NodeGraphMessage::ExposeInput { node_id, input_index, new_exposed } => {
@@ -713,7 +678,6 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
let node_types = document_node_types::collect_node_types(); let node_types = document_node_types::collect_node_types();
responses.add(FrontendMessage::UpdateNodeTypes { node_types }); responses.add(FrontendMessage::UpdateNodeTypes { node_types });
} }
self.collect_nested_addresses(document, data.document_name, responses);
self.update_selected(document, responses); self.update_selected(document, responses);
} }
NodeGraphMessage::PasteNodes { serialized_nodes } => { NodeGraphMessage::PasteNodes { serialized_nodes } => {
@@ -925,6 +889,18 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
} }
self.update_selection_action_buttons(document, responses); self.update_selection_action_buttons(document, responses);
} }
NodeGraphMessage::SetName { node_id, name } => {
responses.add(DocumentMessage::StartTransaction);
responses.add(NodeGraphMessage::SetNameImpl { node_id, name });
}
NodeGraphMessage::SetNameImpl { node_id, name } => {
if let Some(network) = document.document_network.nested_network_mut(&self.network) {
if let Some(node) = network.nodes.get_mut(&node_id) {
node.alias = name;
responses.add(NodeGraphMessage::SendGraph { should_rerender: false });
}
}
}
NodeGraphMessage::TogglePreview { node_id } => { NodeGraphMessage::TogglePreview { node_id } => {
responses.add(DocumentMessage::StartTransaction); responses.add(DocumentMessage::StartTransaction);
responses.add(NodeGraphMessage::TogglePreviewImpl { node_id }); responses.add(NodeGraphMessage::TogglePreviewImpl { node_id });
@@ -958,7 +934,6 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
let node_types = document_node_types::collect_node_types(); let node_types = document_node_types::collect_node_types();
responses.add(FrontendMessage::UpdateNodeTypes { node_types }); responses.add(FrontendMessage::UpdateNodeTypes { node_types });
} }
self.collect_nested_addresses(document, data.document_name, responses);
self.update_selected(document, responses); self.update_selected(document, responses);
} }
} }
@@ -96,8 +96,10 @@ impl NodeImplementation {
/// Acts as a description for a [DocumentNode] before it gets instantiated as one. /// Acts as a description for a [DocumentNode] before it gets instantiated as one.
#[derive(Clone)] #[derive(Clone)]
pub struct DocumentNodeBlueprint { pub struct DocumentNodeBlueprint {
// TODO: Rename to `identifier` (also rename the TODOs in the `DocumentNode` struct)
pub name: &'static str, pub name: &'static str,
pub category: &'static str, pub category: &'static str,
// TODO: Rename to `implementation` (also rename the TODOs in the `DocumentNode` struct)
pub identifier: NodeImplementation, pub identifier: NodeImplementation,
pub inputs: Vec<DocumentInputType>, pub inputs: Vec<DocumentInputType>,
pub outputs: Vec<DocumentOutputType>, pub outputs: Vec<DocumentOutputType>,
@@ -115,7 +117,7 @@ impl Default for DocumentNodeBlueprint {
inputs: Default::default(), inputs: Default::default(),
outputs: Default::default(), outputs: Default::default(),
has_primary_output: true, has_primary_output: true,
properties: node_properties::no_properties, properties: node_properties::node_no_properties,
manual_composition: Default::default(), manual_composition: Default::default(),
} }
} }
@@ -198,7 +200,7 @@ fn static_nodes() -> Vec<DocumentNodeBlueprint> {
name: "Layer", name: "Layer",
category: "General", category: "General",
identifier: NodeImplementation::DocumentNode(NodeNetwork { identifier: NodeImplementation::DocumentNode(NodeNetwork {
inputs: vec![0, 2, 2, 2, 2, 2, 2, 2], inputs: vec![0, 2],
outputs: vec![NodeOutput::new(2, 0)], outputs: vec![NodeOutput::new(2, 0)],
nodes: [ nodes: [
( (
@@ -225,15 +227,9 @@ fn static_nodes() -> Vec<DocumentNodeBlueprint> {
manual_composition: Some(concrete!(Footprint)), manual_composition: Some(concrete!(Footprint)),
inputs: vec![ inputs: vec![
NodeInput::node(1, 0), NodeInput::node(1, 0),
NodeInput::Network(concrete!(String)),
NodeInput::Network(concrete!(BlendMode)),
NodeInput::Network(concrete!(f32)),
NodeInput::Network(concrete!(bool)),
NodeInput::Network(concrete!(bool)),
NodeInput::Network(concrete!(bool)),
NodeInput::Network(graphene_core::Type::Fn(Box::new(concrete!(Footprint)), Box::new(concrete!(graphene_core::GraphicGroup)))), NodeInput::Network(graphene_core::Type::Fn(Box::new(concrete!(Footprint)), Box::new(concrete!(graphene_core::GraphicGroup)))),
], ],
implementation: DocumentNodeImplementation::proto("graphene_core::ConstructLayerNode<_, _, _, _, _, _, _, _>"), implementation: DocumentNodeImplementation::proto("graphene_core::ConstructLayerNode<_, _>"),
..Default::default() ..Default::default()
}, },
), ),
@@ -242,17 +238,11 @@ fn static_nodes() -> Vec<DocumentNodeBlueprint> {
..Default::default() ..Default::default()
}), }),
inputs: vec![ inputs: vec![
DocumentInputType::value("Vector Data", TaggedValue::GraphicGroup(GraphicGroup::EMPTY), true), DocumentInputType::value("Graphical Data", TaggedValue::GraphicGroup(GraphicGroup::EMPTY), true),
DocumentInputType::value("Name", TaggedValue::String(String::new()), false),
DocumentInputType::value("Blend Mode", TaggedValue::BlendMode(BlendMode::Normal), false),
DocumentInputType::value("Opacity", TaggedValue::F32(100.), false),
DocumentInputType::value("Visible", TaggedValue::Bool(true), false),
DocumentInputType::value("Locked", TaggedValue::Bool(false), false),
DocumentInputType::value("Collapsed", TaggedValue::Bool(false), false),
DocumentInputType::value("Stack", TaggedValue::GraphicGroup(GraphicGroup::EMPTY), true), DocumentInputType::value("Stack", TaggedValue::GraphicGroup(GraphicGroup::EMPTY), true),
], ],
outputs: vec![DocumentOutputType::new("Out", FrontendGraphDataType::GraphicGroup)], outputs: vec![DocumentOutputType::new("Out", FrontendGraphDataType::GraphicGroup)],
properties: node_properties::layer_properties, properties: node_properties::layer_no_properties,
..Default::default() ..Default::default()
}, },
DocumentNodeBlueprint { DocumentNodeBlueprint {
@@ -748,12 +738,12 @@ fn static_nodes() -> Vec<DocumentNodeBlueprint> {
..Default::default() ..Default::default()
}, },
DocumentNodeBlueprint { DocumentNodeBlueprint {
name: "Blend Mode", name: "Blend Mode Value",
category: "Image Adjustments", category: "Inputs",
identifier: NodeImplementation::proto("graphene_core::ops::IdNode"), identifier: NodeImplementation::proto("graphene_core::ops::IdNode"),
inputs: vec![DocumentInputType::value("Mode", TaggedValue::BlendMode(BlendMode::Normal), false)], inputs: vec![DocumentInputType::value("Blend Mode", TaggedValue::BlendMode(BlendMode::Normal), false)],
outputs: vec![DocumentOutputType::new("Out", FrontendGraphDataType::General)], outputs: vec![DocumentOutputType::new("Out", FrontendGraphDataType::General)],
properties: node_properties::blend_mode_properties, properties: node_properties::blend_mode_value_properties,
..Default::default() ..Default::default()
}, },
DocumentNodeBlueprint { DocumentNodeBlueprint {
@@ -1442,7 +1432,7 @@ fn static_nodes() -> Vec<DocumentNodeBlueprint> {
DocumentInputType::value("Image", TaggedValue::ImageFrame(ImageFrame::empty()), true), DocumentInputType::value("Image", TaggedValue::ImageFrame(ImageFrame::empty()), true),
DocumentInputType::value("Second", TaggedValue::ImageFrame(ImageFrame::empty()), true), DocumentInputType::value("Second", TaggedValue::ImageFrame(ImageFrame::empty()), true),
DocumentInputType::value("Blend Mode", TaggedValue::BlendMode(BlendMode::Normal), false), DocumentInputType::value("Blend Mode", TaggedValue::BlendMode(BlendMode::Normal), false),
DocumentInputType::value("Opacity", TaggedValue::F32(100.0), false), DocumentInputType::value("Opacity", TaggedValue::F32(100.), false),
], ],
outputs: vec![DocumentOutputType::new("Image", FrontendGraphDataType::Raster)], outputs: vec![DocumentOutputType::new("Image", FrontendGraphDataType::Raster)],
properties: node_properties::blend_properties, properties: node_properties::blend_properties,
@@ -1708,7 +1698,19 @@ fn static_nodes() -> Vec<DocumentNodeBlueprint> {
DocumentInputType::value("Factor", TaggedValue::F32(100.), false), DocumentInputType::value("Factor", TaggedValue::F32(100.), false),
], ],
outputs: vec![DocumentOutputType::new("Image", FrontendGraphDataType::Raster)], outputs: vec![DocumentOutputType::new("Image", FrontendGraphDataType::Raster)],
properties: node_properties::multiply_opacity, properties: node_properties::opacity_properties,
..Default::default()
},
DocumentNodeBlueprint {
name: "Blend Mode",
category: "Image Adjustments",
identifier: NodeImplementation::proto("graphene_core::raster::BlendModeNode<_>"),
inputs: vec![
DocumentInputType::value("Image", TaggedValue::ImageFrame(ImageFrame::empty()), true),
DocumentInputType::value("Blend Mode", TaggedValue::BlendMode(BlendMode::Normal), false),
],
outputs: vec![DocumentOutputType::new("Image", FrontendGraphDataType::Raster)],
properties: node_properties::blend_mode_properties,
..Default::default() ..Default::default()
}, },
DocumentNodeBlueprint { DocumentNodeBlueprint {
@@ -1803,7 +1805,7 @@ fn static_nodes() -> Vec<DocumentNodeBlueprint> {
identifier: NodeImplementation::proto("graphene_core::ops::FloorNode"), identifier: NodeImplementation::proto("graphene_core::ops::FloorNode"),
inputs: vec![DocumentInputType::value("Primary", TaggedValue::F32(0.), true)], inputs: vec![DocumentInputType::value("Primary", TaggedValue::F32(0.), true)],
outputs: vec![DocumentOutputType::new("Output", FrontendGraphDataType::Number)], outputs: vec![DocumentOutputType::new("Output", FrontendGraphDataType::Number)],
properties: node_properties::no_properties, properties: node_properties::node_no_properties,
..Default::default() ..Default::default()
}, },
DocumentNodeBlueprint { DocumentNodeBlueprint {
@@ -1812,7 +1814,7 @@ fn static_nodes() -> Vec<DocumentNodeBlueprint> {
identifier: NodeImplementation::proto("graphene_core::ops::CeilNode"), identifier: NodeImplementation::proto("graphene_core::ops::CeilNode"),
inputs: vec![DocumentInputType::value("Primary", TaggedValue::F32(0.), true)], inputs: vec![DocumentInputType::value("Primary", TaggedValue::F32(0.), true)],
outputs: vec![DocumentOutputType::new("Output", FrontendGraphDataType::Number)], outputs: vec![DocumentOutputType::new("Output", FrontendGraphDataType::Number)],
properties: node_properties::no_properties, properties: node_properties::node_no_properties,
..Default::default() ..Default::default()
}, },
DocumentNodeBlueprint { DocumentNodeBlueprint {
@@ -1821,7 +1823,7 @@ fn static_nodes() -> Vec<DocumentNodeBlueprint> {
identifier: NodeImplementation::proto("graphene_core::ops::RoundNode"), identifier: NodeImplementation::proto("graphene_core::ops::RoundNode"),
inputs: vec![DocumentInputType::value("Primary", TaggedValue::F32(0.), true)], inputs: vec![DocumentInputType::value("Primary", TaggedValue::F32(0.), true)],
outputs: vec![DocumentOutputType::new("Output", FrontendGraphDataType::Number)], outputs: vec![DocumentOutputType::new("Output", FrontendGraphDataType::Number)],
properties: node_properties::no_properties, properties: node_properties::node_no_properties,
..Default::default() ..Default::default()
}, },
DocumentNodeBlueprint { DocumentNodeBlueprint {
@@ -1830,7 +1832,7 @@ fn static_nodes() -> Vec<DocumentNodeBlueprint> {
identifier: NodeImplementation::proto("graphene_core::ops::AbsoluteNode"), identifier: NodeImplementation::proto("graphene_core::ops::AbsoluteNode"),
inputs: vec![DocumentInputType::value("Primary", TaggedValue::F32(0.), true)], inputs: vec![DocumentInputType::value("Primary", TaggedValue::F32(0.), true)],
outputs: vec![DocumentOutputType::new("Output", FrontendGraphDataType::Number)], outputs: vec![DocumentOutputType::new("Output", FrontendGraphDataType::Number)],
properties: node_properties::no_properties, properties: node_properties::node_no_properties,
..Default::default() ..Default::default()
}, },
DocumentNodeBlueprint { DocumentNodeBlueprint {
@@ -1851,7 +1853,7 @@ fn static_nodes() -> Vec<DocumentNodeBlueprint> {
identifier: NodeImplementation::proto("graphene_core::ops::NaturalLogNode"), identifier: NodeImplementation::proto("graphene_core::ops::NaturalLogNode"),
inputs: vec![DocumentInputType::value("Primary", TaggedValue::F32(0.), true)], inputs: vec![DocumentInputType::value("Primary", TaggedValue::F32(0.), true)],
outputs: vec![DocumentOutputType::new("Output", FrontendGraphDataType::Number)], outputs: vec![DocumentOutputType::new("Output", FrontendGraphDataType::Number)],
properties: node_properties::no_properties, properties: node_properties::node_no_properties,
..Default::default() ..Default::default()
}, },
DocumentNodeBlueprint { DocumentNodeBlueprint {
@@ -1860,7 +1862,7 @@ fn static_nodes() -> Vec<DocumentNodeBlueprint> {
identifier: NodeImplementation::proto("graphene_core::ops::SineNode"), identifier: NodeImplementation::proto("graphene_core::ops::SineNode"),
inputs: vec![DocumentInputType::value("Primary", TaggedValue::F32(0.), true)], inputs: vec![DocumentInputType::value("Primary", TaggedValue::F32(0.), true)],
outputs: vec![DocumentOutputType::new("Output", FrontendGraphDataType::Number)], outputs: vec![DocumentOutputType::new("Output", FrontendGraphDataType::Number)],
properties: node_properties::no_properties, properties: node_properties::node_no_properties,
..Default::default() ..Default::default()
}, },
DocumentNodeBlueprint { DocumentNodeBlueprint {
@@ -1869,7 +1871,7 @@ fn static_nodes() -> Vec<DocumentNodeBlueprint> {
identifier: NodeImplementation::proto("graphene_core::ops::CosineNode"), identifier: NodeImplementation::proto("graphene_core::ops::CosineNode"),
inputs: vec![DocumentInputType::value("Primary", TaggedValue::F32(0.), true)], inputs: vec![DocumentInputType::value("Primary", TaggedValue::F32(0.), true)],
outputs: vec![DocumentOutputType::new("Output", FrontendGraphDataType::Number)], outputs: vec![DocumentOutputType::new("Output", FrontendGraphDataType::Number)],
properties: node_properties::no_properties, properties: node_properties::node_no_properties,
..Default::default() ..Default::default()
}, },
DocumentNodeBlueprint { DocumentNodeBlueprint {
@@ -1878,7 +1880,7 @@ fn static_nodes() -> Vec<DocumentNodeBlueprint> {
identifier: NodeImplementation::proto("graphene_core::ops::TangentNode"), identifier: NodeImplementation::proto("graphene_core::ops::TangentNode"),
inputs: vec![DocumentInputType::value("Primary", TaggedValue::F32(0.), true)], inputs: vec![DocumentInputType::value("Primary", TaggedValue::F32(0.), true)],
outputs: vec![DocumentOutputType::new("Output", FrontendGraphDataType::Number)], outputs: vec![DocumentOutputType::new("Output", FrontendGraphDataType::Number)],
properties: node_properties::no_properties, properties: node_properties::node_no_properties,
..Default::default() ..Default::default()
}, },
DocumentNodeBlueprint { DocumentNodeBlueprint {
@@ -1935,7 +1937,7 @@ fn static_nodes() -> Vec<DocumentNodeBlueprint> {
identifier: NodeImplementation::proto("graphene_core::logic::LogToConsoleNode"), identifier: NodeImplementation::proto("graphene_core::logic::LogToConsoleNode"),
inputs: vec![DocumentInputType::value("Input", TaggedValue::String("Not Connected to a value yet".into()), true)], inputs: vec![DocumentInputType::value("Input", TaggedValue::String("Not Connected to a value yet".into()), true)],
outputs: vec![DocumentOutputType::new("Output", FrontendGraphDataType::General)], outputs: vec![DocumentOutputType::new("Output", FrontendGraphDataType::General)],
properties: node_properties::no_properties, properties: node_properties::node_no_properties,
..Default::default() ..Default::default()
}, },
DocumentNodeBlueprint { DocumentNodeBlueprint {
@@ -1980,7 +1982,7 @@ fn static_nodes() -> Vec<DocumentNodeBlueprint> {
identifier: NodeImplementation::proto("graphene_core::logic::LogicNotNode"), identifier: NodeImplementation::proto("graphene_core::logic::LogicNotNode"),
inputs: vec![DocumentInputType::value("Input", TaggedValue::Bool(false), true)], inputs: vec![DocumentInputType::value("Input", TaggedValue::Bool(false), true)],
outputs: vec![DocumentOutputType::new("Output", FrontendGraphDataType::Boolean)], outputs: vec![DocumentOutputType::new("Output", FrontendGraphDataType::Boolean)],
properties: node_properties::no_properties, properties: node_properties::node_no_properties,
..Default::default() ..Default::default()
}, },
(*IMAGINATE_NODE).clone(), (*IMAGINATE_NODE).clone(),
@@ -2077,6 +2079,7 @@ fn static_nodes() -> Vec<DocumentNodeBlueprint> {
identifier: NodeImplementation::proto("graphene_core::vector::generator_nodes::PathGenerator<_>"), identifier: NodeImplementation::proto("graphene_core::vector::generator_nodes::PathGenerator<_>"),
inputs: vec![ inputs: vec![
DocumentInputType::value("Path Data", TaggedValue::Subpaths(vec![]), false), DocumentInputType::value("Path Data", TaggedValue::Subpaths(vec![]), false),
// TODO: Keavon asks: what is this for? Is it dead code? It seems to only be set, never read.
DocumentInputType::value("Mirror", TaggedValue::ManipulatorGroupIds(vec![]), false), DocumentInputType::value("Mirror", TaggedValue::ManipulatorGroupIds(vec![]), false),
], ],
outputs: vec![DocumentOutputType::new("Vector", FrontendGraphDataType::Subpath)], outputs: vec![DocumentOutputType::new("Vector", FrontendGraphDataType::Subpath)],
@@ -2234,7 +2237,7 @@ fn static_nodes() -> Vec<DocumentNodeBlueprint> {
identifier: NodeImplementation::proto("graphene_core::vector::BoundingBoxNode"), identifier: NodeImplementation::proto("graphene_core::vector::BoundingBoxNode"),
inputs: vec![DocumentInputType::value("Vector Data", TaggedValue::VectorData(graphene_core::vector::VectorData::empty()), true)], inputs: vec![DocumentInputType::value("Vector Data", TaggedValue::VectorData(graphene_core::vector::VectorData::empty()), true)],
outputs: vec![DocumentOutputType::new("Vector", FrontendGraphDataType::Subpath)], outputs: vec![DocumentOutputType::new("Vector", FrontendGraphDataType::Subpath)],
properties: node_properties::no_properties, properties: node_properties::node_no_properties,
..Default::default() ..Default::default()
}, },
DocumentNodeBlueprint { DocumentNodeBlueprint {
@@ -2269,7 +2272,7 @@ fn static_nodes() -> Vec<DocumentNodeBlueprint> {
identifier: NodeImplementation::proto("graphene_core::vector::SplineFromPointsNode"), identifier: NodeImplementation::proto("graphene_core::vector::SplineFromPointsNode"),
inputs: vec![DocumentInputType::value("Vector Data", TaggedValue::VectorData(graphene_core::vector::VectorData::empty()), true)], inputs: vec![DocumentInputType::value("Vector Data", TaggedValue::VectorData(graphene_core::vector::VectorData::empty()), true)],
outputs: vec![DocumentOutputType::new("Vector", FrontendGraphDataType::Subpath)], outputs: vec![DocumentOutputType::new("Vector", FrontendGraphDataType::Subpath)],
properties: node_properties::no_properties, properties: node_properties::node_no_properties,
..Default::default() ..Default::default()
}, },
DocumentNodeBlueprint { DocumentNodeBlueprint {
@@ -2471,7 +2474,7 @@ impl DocumentNodeBlueprint {
self.to_document_node(inputs, metadata) self.to_document_node(inputs, metadata)
} }
/// Converts the [DocumentNodeBlueprint] type to a [DocumentNode], completly default /// Converts the [DocumentNodeBlueprint] type to a [DocumentNode], completely default
pub fn default_document_node(&self) -> DocumentNode { pub fn default_document_node(&self) -> DocumentNode {
self.to_document_node(self.inputs.iter().map(|input| input.default.clone()), DocumentNodeMetadata::default()) self.to_document_node(self.inputs.iter().map(|input| input.default.clone()), DocumentNodeMetadata::default())
} }
@@ -777,10 +777,6 @@ pub fn mask_properties(document_node: &DocumentNode, node_id: NodeId, _context:
vec![mask] vec![mask]
} }
pub fn blend_mode_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
vec![blend_mode(document_node, node_id, 0, "Blend Mode", true)]
}
pub fn color_channel_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<LayoutGroup> { pub fn color_channel_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
vec![color_channel(document_node, node_id, 0, "Channel", true)] vec![color_channel(document_node, node_id, 0, "Channel", true)]
} }
@@ -803,7 +799,7 @@ pub fn extract_channel_properties(document_node: &DocumentNode, node_id: NodeId,
vec![color_channel] vec![color_channel]
} }
// Noise Type is commented out for now as ther is only one type of noise (White Noise). // Noise Type is commented out for now as there is only one type of noise (White Noise).
// As soon as there are more types of noise, this should be uncommented. // As soon as there are more types of noise, this should be uncommented.
pub fn pixel_noise_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<LayoutGroup> { pub fn pixel_noise_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
let width = number_widget(document_node, node_id, 0, "Width", NumberInput::default().unit("px").min(1.), true); let width = number_widget(document_node, node_id, 0, "Width", NumberInput::default().unit("px").min(1.), true);
@@ -1025,12 +1021,20 @@ pub fn _gpu_map_properties(document_node: &DocumentNode, node_id: NodeId, _conte
vec![LayoutGroup::Row { widgets: map }] vec![LayoutGroup::Row { widgets: map }]
} }
pub fn multiply_opacity(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<LayoutGroup> { pub fn opacity_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
let gamma = number_widget(document_node, node_id, 1, "Factor", NumberInput::default().min(0.).max(100.).unit("%"), true); let gamma = number_widget(document_node, node_id, 1, "Factor", NumberInput::default().mode_range().min(0.).max(100.).unit("%"), true);
vec![LayoutGroup::Row { widgets: gamma }] vec![LayoutGroup::Row { widgets: gamma }]
} }
pub fn blend_mode_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
vec![blend_mode(document_node, node_id, 1, "Blend Mode", true)]
}
pub fn blend_mode_value_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
vec![blend_mode(document_node, node_id, 0, "Blend Mode", true)]
}
pub fn posterize_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<LayoutGroup> { pub fn posterize_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
let value = number_widget(document_node, node_id, 1, "Levels", NumberInput::default().min(2.).max(255.).int(), true); let value = number_widget(document_node, node_id, 1, "Levels", NumberInput::default().min(2.).max(255.).int(), true);
@@ -1769,10 +1773,14 @@ fn unknown_node_properties(document_node: &DocumentNode) -> Vec<LayoutGroup> {
string_properties(format!("Node '{}' cannot be found in library", document_node.name)) string_properties(format!("Node '{}' cannot be found in library", document_node.name))
} }
pub fn no_properties(_document_node: &DocumentNode, _node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<LayoutGroup> { pub fn node_no_properties(_document_node: &DocumentNode, _node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
string_properties("Node has no properties") string_properties("Node has no properties")
} }
pub fn layer_no_properties(_document_node: &DocumentNode, _node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
string_properties("Layer has no properties")
}
pub fn index_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<LayoutGroup> { pub fn index_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
let index = number_widget(document_node, node_id, 1, "Index", NumberInput::default().min(0.), true); let index = number_widget(document_node, node_id, 1, "Index", NumberInput::default().min(0.), true);
@@ -1875,23 +1883,6 @@ pub fn fill_properties(document_node: &DocumentNode, node_id: NodeId, _context:
widgets widgets
} }
pub fn layer_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
let name = text_widget(document_node, node_id, 1, "Name", true);
let blend_mode = blend_mode(document_node, node_id, 2, "Blend Mode", true);
let opacity = number_widget(document_node, node_id, 3, "Opacity", NumberInput::default().percentage(), true);
let visible = bool_widget(document_node, node_id, 4, "Visible", true);
let locked = bool_widget(document_node, node_id, 5, "Locked", true);
let collapsed = bool_widget(document_node, node_id, 6, "Collapsed", true);
vec![
LayoutGroup::Row { widgets: name },
blend_mode,
LayoutGroup::Row { widgets: opacity },
LayoutGroup::Row { widgets: visible },
LayoutGroup::Row { widgets: locked },
LayoutGroup::Row { widgets: collapsed },
]
}
pub fn artboard_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<LayoutGroup> { pub fn artboard_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
let location = vec2_widget(document_node, node_id, 1, "Location", "X", "Y", " px", add_blank_assist); let location = vec2_widget(document_node, node_id, 1, "Location", "X", "Y", " px", add_blank_assist);
let dimensions = vec2_widget(document_node, node_id, 2, "Dimensions", "W", "H", " px", add_blank_assist); let dimensions = vec2_widget(document_node, node_id, 2, "Dimensions", "W", "H", " px", add_blank_assist);
@@ -18,7 +18,6 @@ pub enum PropertiesPanelMessage {
Deactivate, Deactivate,
Init, Init,
ModifyFill { fill: Fill }, ModifyFill { fill: Fill },
ModifyName { name: String },
ModifyPreserveAspect { preserve_aspect: bool }, ModifyPreserveAspect { preserve_aspect: bool },
ModifyStroke { stroke: Stroke }, ModifyStroke { stroke: Stroke },
ModifyTransform { value: f64, transform_op: TransformOp }, ModifyTransform { value: f64, transform_op: TransformOp },
@@ -13,7 +13,7 @@ use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, Default)] #[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct PropertiesPanelMessageHandler { pub struct PropertiesPanelMessageHandler {
active_selection: Option<Vec<LayerId>>, active_selection: Option<Vec<LayerId>>, // TODO: Delete this if it's indeed dead code?
} }
impl<'a> MessageHandler<PropertiesPanelMessage, (&PersistentData, PropertiesPanelMessageHandlerData<'a>)> for PropertiesPanelMessageHandler { impl<'a> MessageHandler<PropertiesPanelMessage, (&PersistentData, PropertiesPanelMessageHandlerData<'a>)> for PropertiesPanelMessageHandler {
@@ -38,7 +38,7 @@ impl<'a> MessageHandler<PropertiesPanelMessage, (&PersistentData, PropertiesPane
responses.add(NodeGraphMessage::CloseNodeGraph); responses.add(NodeGraphMessage::CloseNodeGraph);
} else { } else {
let path = paths.into_iter().next().unwrap(); let path = paths.into_iter().next().unwrap();
if Some(&path) != self.active_selection.as_ref() { if self.active_selection.as_ref() != Some(&path) {
// Update the layer visibility // Update the layer visibility
if artwork_document if artwork_document
.layer(&path) .layer(&path)
@@ -86,10 +86,6 @@ impl<'a> MessageHandler<PropertiesPanelMessage, (&PersistentData, PropertiesPane
self.create_document_operation(Operation::SetLayerTransform { path: path.clone(), transform }, true, responses); self.create_document_operation(Operation::SetLayerTransform { path: path.clone(), transform }, true, responses);
} }
ModifyName { name } => {
let path = self.active_selection.clone().expect("Received update for properties panel with no active layer");
self.create_document_operation(Operation::SetLayerName { path, name }, true, responses);
}
ModifyPreserveAspect { preserve_aspect } => { ModifyPreserveAspect { preserve_aspect } => {
let layer_path = self.active_selection.clone().expect("Received update for properties panel with no active layer"); let layer_path = self.active_selection.clone().expect("Received update for properties panel with no active layer");
self.create_document_operation(Operation::SetLayerPreserveAspect { layer_path, preserve_aspect }, true, responses); self.create_document_operation(Operation::SetLayerPreserveAspect { layer_path, preserve_aspect }, true, responses);
@@ -131,6 +127,7 @@ impl<'a> MessageHandler<PropertiesPanelMessage, (&PersistentData, PropertiesPane
} }
ResendActiveProperties => { ResendActiveProperties => {
if let Some(path) = self.active_selection.clone() { if let Some(path) = self.active_selection.clone() {
// TODO: Remove this conditional now that the document graph is the only form of graph? (Also any other related code.)
let layer = artwork_document.layer(&path).unwrap(); let layer = artwork_document.layer(&path).unwrap();
register_artwork_layer_properties(artwork_document, path, layer, responses, persistent_data, node_graph_message_handler, executor); register_artwork_layer_properties(artwork_document, path, layer, responses, persistent_data, node_graph_message_handler, executor);
} else { } else {
@@ -82,7 +82,7 @@ pub fn register_artwork_layer_properties(
}, },
Separator::new(SeparatorType::Unrelated).widget_holder(), Separator::new(SeparatorType::Unrelated).widget_holder(),
TextInput::new(layer.name.clone().unwrap_or_else(|| "Untitled Layer".to_string())) TextInput::new(layer.name.clone().unwrap_or_else(|| "Untitled Layer".to_string()))
.on_update(|text_input: &TextInput| PropertiesPanelMessage::ModifyName { name: text_input.value.clone() }.into()) .on_update(|_text_input: &TextInput| panic!("This is presumed to be dead code, but if you are seeing this crash, please file a bug report."))
.widget_holder(), .widget_holder(),
Separator::new(SeparatorType::Related).widget_holder(), Separator::new(SeparatorType::Related).widget_holder(),
PopoverButton::new("Additional Options", "Coming soon").widget_holder(), PopoverButton::new("Additional Options", "Coming soon").widget_holder(),
@@ -21,9 +21,6 @@ pub enum EditorError {
#[error("This document was created in an older version of the editor.\n\nBackwards compatibility is, regrettably, not present in the current alpha release.\n\nTechnical details:\n{0:?}")] #[error("This document was created in an older version of the editor.\n\nBackwards compatibility is, regrettably, not present in the current alpha release.\n\nTechnical details:\n{0:?}")]
DocumentDeserialization(String), DocumentDeserialization(String),
#[error("A rollback was initiated but no transaction was in progress")]
NoTransactionInProgress,
#[error("{0}")] #[error("{0}")]
Misc(String), Misc(String),
} }
@@ -58,6 +58,8 @@ pub struct LayerPanelEntry {
} }
impl LayerPanelEntry { impl LayerPanelEntry {
// TODO: Deprecate this because it's using document-legacy layer data which is no longer linked to data from the node graph,
// TODO: so this doesn't feed `name` (that's fed elsewhere) or `visible` (that's broken entirely), etc.
pub fn new(layer_metadata: &LayerMetadata, transform: DAffine2, layer: &Layer, path: Vec<LayerId>, render_data: &RenderData) -> Self { pub fn new(layer_metadata: &LayerMetadata, transform: DAffine2, layer: &Layer, path: Vec<LayerId>, render_data: &RenderData) -> Self {
let name = layer.name.clone().unwrap_or_else(|| String::from("")); let name = layer.name.clone().unwrap_or_else(|| String::from(""));
@@ -228,7 +228,7 @@ impl<'a> NodeGraphLayer<'a> {
error!("Tried to modify root layer"); error!("Tried to modify root layer");
return None; return None;
}; };
while node_graph.nodes.get(&layer_node)?.name != "Layer" { while !node_graph.nodes.get(&layer_node)?.is_layer() {
layer_node = outwards_links.get(&layer_node)?.first().copied()?; layer_node = outwards_links.get(&layer_node)?.first().copied()?;
} }
Some(Self { Some(Self {
+1 -8
View File
@@ -533,14 +533,7 @@ impl NodeGraphExecutor {
let layer = LayerNodeIdentifier::new(node_id, &document.document_network); let layer = LayerNodeIdentifier::new(node_id, &document.document_network);
responses.add(FrontendMessage::UpdateDocumentLayerDetails { responses.add(FrontendMessage::UpdateDocumentLayerDetails {
data: LayerPanelEntry { data: LayerPanelEntry {
name: if document.metadata.is_artboard(layer) { name: document.document_network.nodes.get(&node_id).map(|node| node.alias.clone()).unwrap_or_default(),
"Artboard"
} else if document.metadata.is_folder(layer) {
"Folder"
} else {
"Layer"
}
.to_string(),
tooltip: if cfg!(debug_assertions) { format!("Layer ID: {node_id}") } else { "".into() }, tooltip: if cfg!(debug_assertions) { format!("Layer ID: {node_id}") } else { "".into() },
visible: !document.document_network.disabled.contains(&layer.to_node()), visible: !document.document_network.disabled.contains(&layer.to_node()),
layer_type: if document.metadata.is_artboard(layer) { layer_type: if document.metadata.is_artboard(layer) {
+21 -16
View File
@@ -83,7 +83,8 @@
await tick(); await tick();
const textInput = (list?.div()?.querySelector("[data-text-input]:not([disabled])") || undefined) as HTMLInputElement | undefined; const query = list?.div()?.querySelector("[data-text-input]:not([disabled])");
const textInput = (query instanceof HTMLInputElement && query) || undefined;
textInput?.select(); textInput?.select();
} }
@@ -92,20 +93,23 @@
if (!listing.editingName) return; if (!listing.editingName) return;
draggable = true; draggable = true;
const name = (e.target as HTMLInputElement | undefined)?.value;
listing.editingName = false; listing.editingName = false;
layers = layers; layers = layers;
if (name) editor.instance.setLayerName(listing.entry.path, name);
const name = (e.target instanceof HTMLInputElement && e.target.value) || "";
editor.instance.setLayerName(listing.entry.path, name);
listing.entry.name = name;
} }
async function onEditLayerNameDeselect(listing: LayerListingInfo) { async function onEditLayerNameDeselect(listing: LayerListingInfo) {
draggable = true; draggable = true;
listing.editingName = false; listing.editingName = false;
layers = layers; layers = layers;
await tick(); // Set it back to the original name if the user didn't enter a new name
if (document.activeElement instanceof HTMLInputElement) document.activeElement.value = listing.entry.name;
// Deselect the text so it doesn't appear selected while the input field becomes disabled and styled to look like regular text
window.getSelection()?.removeAllRanges(); window.getSelection()?.removeAllRanges();
} }
@@ -153,7 +157,7 @@
let highlightFolder = false; let highlightFolder = false;
let markerHeight = 0; let markerHeight = 0;
let previousHeight = undefined as undefined | number; let previousHeight: number | undefined = undefined;
if (treeChildren !== undefined && treeOffset !== undefined) { if (treeChildren !== undefined && treeOffset !== undefined) {
Array.from(treeChildren).forEach((treeChild, index) => { Array.from(treeChildren).forEach((treeChild, index) => {
@@ -212,8 +216,9 @@
if (!layer.layerMetadata.selected) selectLayer(false, false, listing); if (!layer.layerMetadata.selected) selectLayer(false, false, listing);
}; };
const target = (event.target || undefined) as HTMLElement | undefined; const target = (event.target instanceof HTMLElement && event.target) || undefined;
const draggingELement = (target?.closest("[data-layer]") || undefined) as HTMLElement | undefined; const closest = target?.closest("[data-layer]") || undefined;
const draggingELement = (closest instanceof HTMLElement && closest) || undefined;
if (draggingELement) beginDraggingElement(draggingELement); if (draggingELement) beginDraggingElement(draggingELement);
// Set style of cursor for drag // Set style of cursor for drag
@@ -332,7 +337,7 @@
data-text-input data-text-input
type="text" type="text"
value={listing.entry.name} value={listing.entry.name}
placeholder={`Untitled ${listing.entry.layerType || "[Unknown Layer Type]"}`} placeholder={listing.entry.layerType}
disabled={!listing.editingName} disabled={!listing.editingName}
on:blur={() => onEditLayerNameDeselect(listing)} on:blur={() => onEditLayerNameDeselect(listing)}
on:keydown={(e) => e.key === "Escape" && onEditLayerNameDeselect(listing)} on:keydown={(e) => e.key === "Escape" && onEditLayerNameDeselect(listing)}
@@ -484,12 +489,6 @@
pointer-events: none; pointer-events: none;
} }
&::placeholder {
opacity: 1;
color: inherit;
font-style: italic;
}
&:focus { &:focus {
background: var(--color-1-nearblack); background: var(--color-1-nearblack);
padding: 0 4px; padding: 0 4px;
@@ -498,6 +497,12 @@
opacity: 0.5; opacity: 0.5;
} }
} }
&::placeholder {
opacity: 1;
color: inherit;
font-style: italic;
}
} }
} }
+26 -19
View File
@@ -5,7 +5,7 @@
import type { IconName } from "@graphite/utility-functions/icons"; import type { IconName } from "@graphite/utility-functions/icons";
import type { Editor } from "@graphite/wasm-communication/editor"; import type { Editor } from "@graphite/wasm-communication/editor";
import { UpdateNodeGraphSelection } from "@graphite/wasm-communication/messages"; import { UpdateNodeGraphSelection } from "@graphite/wasm-communication/messages";
import type { FrontendNodeLink, FrontendNodeType, FrontendNode } from "@graphite/wasm-communication/messages"; import type { FrontendNodeLink, FrontendNodeType, FrontendNode, FrontendGraphDataType } from "@graphite/wasm-communication/messages";
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte"; import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
import TextButton from "@graphite/components/widgets/buttons/TextButton.svelte"; import TextButton from "@graphite/components/widgets/buttons/TextButton.svelte";
@@ -116,8 +116,8 @@
const from = connectorToNodeIndex(linkInProgressFromConnector); const from = connectorToNodeIndex(linkInProgressFromConnector);
const to = linkInProgressToConnector instanceof SVGSVGElement ? connectorToNodeIndex(linkInProgressToConnector) : undefined; const to = linkInProgressToConnector instanceof SVGSVGElement ? connectorToNodeIndex(linkInProgressToConnector) : undefined;
const linkStart = $nodeGraph.nodes.find((node) => node.id === from?.nodeId)?.displayName === "Layer"; const linkStart = $nodeGraph.nodes.find((node) => node.id === from?.nodeId)?.isLayer;
const linkEnd = $nodeGraph.nodes.find((node) => node.id === to?.nodeId)?.displayName === "Layer" && to?.index !== 0; const linkEnd = $nodeGraph.nodes.find((node) => node.id === to?.nodeId)?.isLayer && to?.index !== 0;
return createWirePath(linkInProgressFromConnector, linkInProgressToConnector, linkStart, linkEnd); return createWirePath(linkInProgressFromConnector, linkInProgressToConnector, linkStart, linkEnd);
} }
return undefined; return undefined;
@@ -158,8 +158,8 @@
const { nodeInput, nodeOutput } = resolveLink(link); const { nodeInput, nodeOutput } = resolveLink(link);
if (!nodeInput || !nodeOutput) return []; if (!nodeInput || !nodeOutput) return [];
if (disconnecting?.linkIndex === index) return []; if (disconnecting?.linkIndex === index) return [];
const linkStart = $nodeGraph.nodes.find((node) => node.id === link.linkStart)?.displayName === "Layer"; const linkStart = $nodeGraph.nodes.find((node) => node.id === link.linkStart)?.isLayer;
const linkEnd = $nodeGraph.nodes.find((node) => node.id === link.linkEnd)?.displayName === "Layer" && link.linkEndInputIndex !== 0n; const linkEnd = $nodeGraph.nodes.find((node) => node.id === link.linkEnd)?.isLayer && link.linkEndInputIndex !== 0n;
return [createWirePath(nodeOutput, nodeInput.getBoundingClientRect(), linkStart, linkEnd)]; return [createWirePath(nodeOutput, nodeInput.getBoundingClientRect(), linkStart, linkEnd)];
}); });
@@ -604,6 +604,11 @@
return `M-2,-2 L${nodeWidth + 2},-2 L${nodeWidth + 2},${nodeHeight + 2} L-2,${nodeHeight + 2}z ${rectangles.join(" ")}`; return `M-2,-2 L${nodeWidth + 2},-2 L${nodeWidth + 2},${nodeHeight + 2} L-2,${nodeHeight + 2}z ${rectangles.join(" ")}`;
} }
function dataTypeTooltip(dataType: FrontendGraphDataType): string {
const capitalized = dataType[0].toUpperCase() + dataType.slice(1);
return `${capitalized} Data`;
}
onMount(() => { onMount(() => {
editor.subscriptions.subscribeJsMessage(UpdateNodeGraphSelection, (updateNodeGraphSelection) => { editor.subscriptions.subscribeJsMessage(UpdateNodeGraphSelection, (updateNodeGraphSelection) => {
selected = updateNodeGraphSelection.selected; selected = updateNodeGraphSelection.selected;
@@ -665,7 +670,7 @@
<!-- Layers and nodes --> <!-- Layers and nodes -->
<div class="layers-and-nodes" style:transform={`scale(${transform.scale}) translate(${transform.x}px, ${transform.y}px)`} style:transform-origin={`0 0`} bind:this={nodesContainer}> <div class="layers-and-nodes" style:transform={`scale(${transform.scale}) translate(${transform.x}px, ${transform.y}px)`} style:transform-origin={`0 0`} bind:this={nodesContainer}>
<!-- Layers --> <!-- Layers -->
{#each $nodeGraph.nodes.flatMap((node, nodeIndex) => (node.displayName === "Layer" ? [{ node, nodeIndex }] : [])) as { node, nodeIndex } (nodeIndex)} {#each $nodeGraph.nodes.flatMap((node, nodeIndex) => (node.isLayer ? [{ node, nodeIndex }] : [])) as { node, nodeIndex } (nodeIndex)}
{@const clipPathId = `${Math.random()}`.substring(2)} {@const clipPathId = `${Math.random()}`.substring(2)}
{@const stackDatainput = node.exposedInputs[0]} {@const stackDatainput = node.exposedInputs[0]}
<div <div
@@ -693,7 +698,9 @@
style:--data-color-dim={`var(--color-data-${node.primaryInput?.dataType}-dim)`} style:--data-color-dim={`var(--color-data-${node.primaryInput?.dataType}-dim)`}
bind:this={inputs[nodeIndex][0]} bind:this={inputs[nodeIndex][0]}
> >
<title>{node.primaryInput} data</title> {#if node.primaryInput}
<title>{dataTypeTooltip(node.primaryInput.dataType)}</title>
{/if}
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" /> <path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" />
</svg> </svg>
</div> </div>
@@ -712,7 +719,7 @@
style:--data-color-dim={`var(--color-data-${node.primaryOutput.dataType}-dim)`} style:--data-color-dim={`var(--color-data-${node.primaryOutput.dataType}-dim)`}
bind:this={outputs[nodeIndex][0]} bind:this={outputs[nodeIndex][0]}
> >
<title>{node.primaryOutput.dataType} data</title> <title>{dataTypeTooltip(node.primaryOutput.dataType)}</title>
<path d="M0,2.953,2.521,1.259a2.649,2.649,0,0,1,2.959,0L8,2.953V8H0Z" /> <path d="M0,2.953,2.521,1.259a2.649,2.649,0,0,1,2.959,0L8,2.953V8H0Z" />
</svg> </svg>
{/if} {/if}
@@ -726,12 +733,13 @@
style:--data-color-dim={`var(--color-data-${stackDatainput.dataType}-dim)`} style:--data-color-dim={`var(--color-data-${stackDatainput.dataType}-dim)`}
bind:this={inputs[nodeIndex][1]} bind:this={inputs[nodeIndex][1]}
> >
<title>{stackDatainput.dataType} data</title> <title>{dataTypeTooltip(stackDatainput.dataType)}</title>
<path d="M0,0H8V8L5.479,6.319a2.666,2.666,0,0,0-2.959,0L0,8Z" /> <path d="M0,0H8V8L5.479,6.319a2.666,2.666,0,0,0-2.959,0L0,8Z" />
</svg> </svg>
</div> </div>
<div class="details"> <div class="details">
<TextLabel tooltip={`${node.displayName} node with id: ${node.id}`}>{node.displayName}</TextLabel> <!-- TODO: Allow the user to edit the name, just like in the Layers panel -->
<TextLabel tooltip={editor.instance.inDevelopmentMode() ? `Node ID: ${node.id}` : undefined} italic={!node.name}>{node.name || "Layer"}</TextLabel>
</div> </div>
<svg class="border-mask" width="0" height="0"> <svg class="border-mask" width="0" height="0">
@@ -744,7 +752,7 @@
</div> </div>
{/each} {/each}
<!-- Nodes --> <!-- Nodes -->
{#each $nodeGraph.nodes.flatMap((node, nodeIndex) => (node.displayName !== "Layer" ? [{ node, nodeIndex }] : [])) as { node, nodeIndex } (nodeIndex)} {#each $nodeGraph.nodes.flatMap((node, nodeIndex) => (node.isLayer ? [] : [{ node, nodeIndex }])) as { node, nodeIndex } (nodeIndex)}
{@const exposedInputsOutputs = [...node.exposedInputs, ...node.exposedOutputs]} {@const exposedInputsOutputs = [...node.exposedInputs, ...node.exposedOutputs]}
{@const clipPathId = `${Math.random()}`.substring(2)} {@const clipPathId = `${Math.random()}`.substring(2)}
<div <div
@@ -752,7 +760,6 @@
class:selected={selected.includes(node.id)} class:selected={selected.includes(node.id)}
class:previewed={node.previewed} class:previewed={node.previewed}
class:disabled={node.disabled} class:disabled={node.disabled}
class:is-layer={node.displayName === "Layer"}
style:--offset-left={(node.position?.x || 0) + (selected.includes(node.id) ? draggingNodes?.roundX || 0 : 0)} style:--offset-left={(node.position?.x || 0) + (selected.includes(node.id) ? draggingNodes?.roundX || 0 : 0)}
style:--offset-top={(node.position?.y || 0) + (selected.includes(node.id) ? draggingNodes?.roundY || 0 : 0)} style:--offset-top={(node.position?.y || 0) + (selected.includes(node.id) ? draggingNodes?.roundY || 0 : 0)}
style:--clip-path-id={`url(#${clipPathId})`} style:--clip-path-id={`url(#${clipPathId})`}
@@ -762,8 +769,9 @@
> >
<!-- Primary row --> <!-- Primary row -->
<div class="primary" class:no-parameter-section={exposedInputsOutputs.length === 0}> <div class="primary" class:no-parameter-section={exposedInputsOutputs.length === 0}>
<IconLabel icon={nodeIcon(node.displayName)} /> <IconLabel icon={nodeIcon(node.identifier)} />
<TextLabel tooltip={`${node.displayName} node (ID: ${node.id})`}>{node.displayName}</TextLabel> <!-- TODO: Allow the user to edit the name, just like in the Layers panel -->
<TextLabel tooltip={editor.instance.inDevelopmentMode() ? `Node ID: ${node.id}` : undefined} italic={!node.name}>{node.name || node.identifier}</TextLabel>
</div> </div>
<!-- Parameter rows --> <!-- Parameter rows -->
{#if exposedInputsOutputs.length > 0} {#if exposedInputsOutputs.length > 0}
@@ -789,7 +797,7 @@
style:--data-color-dim={`var(--color-data-${node.primaryInput?.dataType}-dim)`} style:--data-color-dim={`var(--color-data-${node.primaryInput?.dataType}-dim)`}
bind:this={inputs[nodeIndex][0]} bind:this={inputs[nodeIndex][0]}
> >
<title>{node.primaryInput} data</title> <title>{dataTypeTooltip(node.primaryInput.dataType)}</title>
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" /> <path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" />
</svg> </svg>
{/if} {/if}
@@ -805,7 +813,7 @@
style:--data-color-dim={`var(--color-data-${parameter.dataType}-dim)`} style:--data-color-dim={`var(--color-data-${parameter.dataType}-dim)`}
bind:this={inputs[nodeIndex][index + 1]} bind:this={inputs[nodeIndex][index + 1]}
> >
<title>{parameter.dataType} data</title> <title>{dataTypeTooltip(parameter.dataType)}</title>
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" /> <path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" />
</svg> </svg>
{/if} {/if}
@@ -824,7 +832,7 @@
style:--data-color-dim={`var(--color-data-${node.primaryOutput.dataType}-dim)`} style:--data-color-dim={`var(--color-data-${node.primaryOutput.dataType}-dim)`}
bind:this={outputs[nodeIndex][0]} bind:this={outputs[nodeIndex][0]}
> >
<title>{node.primaryOutput.dataType} data</title> <title>{dataTypeTooltip(node.primaryOutput.dataType)}</title>
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" /> <path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" />
</svg> </svg>
{/if} {/if}
@@ -839,7 +847,7 @@
style:--data-color-dim={`var(--color-data-${parameter.dataType}-dim)`} style:--data-color-dim={`var(--color-data-${parameter.dataType}-dim)`}
bind:this={outputs[nodeIndex][outputIndex + 1]} bind:this={outputs[nodeIndex][outputIndex + 1]}
> >
<title>{parameter.dataType} data</title> <title>{dataTypeTooltip(parameter.dataType)}</title>
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" /> <path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" />
</svg> </svg>
{/each} {/each}
@@ -1179,7 +1187,6 @@
width: 100%; width: 100%;
height: 24px; height: 24px;
border-radius: 2px 2px 0 0; border-radius: 2px 2px 0 0;
font-style: italic;
background: rgba(255, 255, 255, 0.05); background: rgba(255, 255, 255, 0.05);
&.no-parameter-section { &.no-parameter-section {
+5 -1
View File
@@ -97,9 +97,13 @@ export class FrontendGraphOutput {
} }
export class FrontendNode { export class FrontendNode {
readonly isLayer!: boolean;
readonly id!: bigint; readonly id!: bigint;
readonly displayName!: string; readonly name!: string;
readonly identifier!: string;
readonly primaryInput!: FrontendGraphInput | undefined; readonly primaryInput!: FrontendGraphInput | undefined;
+2 -1
View File
@@ -556,7 +556,8 @@ impl JsEditorHandle {
/// Set the name for the layer /// Set the name for the layer
#[wasm_bindgen(js_name = setLayerName)] #[wasm_bindgen(js_name = setLayerName)]
pub fn set_layer_name(&self, layer_path: Vec<LayerId>, name: String) { pub fn set_layer_name(&self, layer_path: Vec<LayerId>, name: String) {
let message = DocumentMessage::SetLayerName { layer_path, name }; let node_id = *layer_path.last().unwrap();
let message = NodeGraphMessage::SetName { node_id, name };
self.dispatch(message); self.dispatch(message);
} }
+2 -2
View File
@@ -102,7 +102,7 @@ The `graphene_core::value::CopiedNode` is a node that, when evaluated, copies `1
## Creating a new protonode ## Creating a new protonode
Instead of manually implementing the `Node` trait with complex generics, one can use the `node_fn` macro, which can be applied to a function like `image_opacity` with an attribute of the name of the node: Instead of manually implementing the `Node` trait with complex generics, one can use the `node_fn` macro, which can be applied to a function like `opacity_node` with an attribute of the name of the node:
```rs ```rs
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
@@ -111,7 +111,7 @@ pub struct OpacityNode<O> {
} }
#[node_macro::node_fn(OpacityNode)] #[node_macro::node_fn(OpacityNode)]
fn image_opacity(color: Color, opacity_multiplier: f64) -> Color { fn opacity_node(color: Color, opacity_multiplier: f64) -> Color {
let opacity_multiplier = opacity_multiplier as f32 / 100.; let opacity_multiplier = opacity_multiplier as f32 / 100.;
Color::from_rgbaf32_unchecked(color.r(), color.g(), color.b(), color.a() * opacity_multiplier) Color::from_rgbaf32_unchecked(color.r(), color.g(), color.b(), color.a() * opacity_multiplier)
} }
+7 -44
View File
@@ -19,6 +19,7 @@ pub mod renderer;
pub struct GraphicGroup { pub struct GraphicGroup {
elements: Vec<GraphicElement>, elements: Vec<GraphicElement>,
pub opacity: f32, pub opacity: f32,
pub blend_mode: BlendMode,
pub transform: DAffine2, pub transform: DAffine2,
} }
@@ -41,29 +42,16 @@ pub enum GraphicElementData {
Artboard(Artboard), Artboard(Artboard),
} }
/// A named [`GraphicElementData`] with a blend mode, opacity, as well as visibility, locked, and collapsed states. // TODO: Remove this wrapper and directly use GraphicElementData
#[derive(Clone, Debug, PartialEq, DynAny)] #[derive(Clone, Debug, PartialEq, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct GraphicElement { pub struct GraphicElement {
pub name: String,
pub blend_mode: BlendMode,
/// In range 0..=1
pub opacity: f32,
pub visible: bool,
pub locked: bool,
pub collapsed: bool,
pub graphic_element_data: GraphicElementData, pub graphic_element_data: GraphicElementData,
} }
impl Default for GraphicElement { impl Default for GraphicElement {
fn default() -> Self { fn default() -> Self {
Self { Self {
name: "".to_owned(),
blend_mode: BlendMode::Normal,
opacity: 1.,
visible: true,
locked: false,
collapsed: false,
graphic_element_data: GraphicElementData::VectorShape(Box::new(VectorData::empty())), graphic_element_data: GraphicElementData::VectorShape(Box::new(VectorData::empty())),
} }
} }
@@ -93,14 +81,8 @@ impl Artboard {
} }
} }
pub struct ConstructLayerNode<GraphicElementData, Name, BlendMode, Opacity, Visible, Locked, Collapsed, Stack> { pub struct ConstructLayerNode<GraphicElementData, Stack> {
graphic_element_data: GraphicElementData, graphic_element_data: GraphicElementData,
name: Name,
blend_mode: BlendMode,
opacity: Opacity,
visible: Visible,
locked: Locked,
collapsed: Collapsed,
stack: Stack, stack: Stack,
} }
@@ -108,23 +90,11 @@ pub struct ConstructLayerNode<GraphicElementData, Name, BlendMode, Opacity, Visi
async fn construct_layer<Data: Into<GraphicElementData>, Fut1: Future<Output = Data>, Fut2: Future<Output = GraphicGroup>>( async fn construct_layer<Data: Into<GraphicElementData>, Fut1: Future<Output = Data>, Fut2: Future<Output = GraphicGroup>>(
footprint: crate::transform::Footprint, footprint: crate::transform::Footprint,
graphic_element_data: impl Node<crate::transform::Footprint, Output = Fut1>, graphic_element_data: impl Node<crate::transform::Footprint, Output = Fut1>,
name: String,
blend_mode: BlendMode,
opacity: f32,
visible: bool,
locked: bool,
collapsed: bool,
mut stack: impl Node<crate::transform::Footprint, Output = Fut2>, mut stack: impl Node<crate::transform::Footprint, Output = Fut2>,
) -> GraphicGroup { ) -> GraphicGroup {
let graphic_element_data = self.graphic_element_data.eval(footprint).await; let graphic_element_data = self.graphic_element_data.eval(footprint).await;
let mut stack = self.stack.eval(footprint).await; let mut stack = self.stack.eval(footprint).await;
stack.push(GraphicElement { stack.push(GraphicElement {
name,
blend_mode,
opacity: opacity / 100.,
visible,
locked,
collapsed,
graphic_element_data: graphic_element_data.into(), graphic_element_data: graphic_element_data.into(),
}); });
stack stack
@@ -154,7 +124,7 @@ async fn construct_artboard<Fut: Future<Output = GraphicGroup>>(
background: Color, background: Color,
clip: bool, clip: bool,
) -> Artboard { ) -> Artboard {
footprint.transform = footprint.transform * DAffine2::from_translation(location.as_dvec2()); footprint.transform *= DAffine2::from_translation(location.as_dvec2());
let graphic_group = self.contents.eval(footprint).await; let graphic_group = self.contents.eval(footprint).await;
Artboard { Artboard {
graphic_group, graphic_group,
@@ -212,13 +182,11 @@ where
T: ToGraphicElement, T: ToGraphicElement,
{ {
fn from(value: T) -> Self { fn from(value: T) -> Self {
let element = GraphicElement { let element = GraphicElement { graphic_element_data: value.into() };
graphic_element_data: value.into(),
..Default::default()
};
Self { Self {
elements: (vec![element]), elements: (vec![element]),
opacity: 1., opacity: 1.,
blend_mode: BlendMode::Normal,
transform: DAffine2::IDENTITY, transform: DAffine2::IDENTITY,
} }
} }
@@ -228,6 +196,7 @@ impl GraphicGroup {
pub const EMPTY: Self = Self { pub const EMPTY: Self = Self {
elements: Vec::new(), elements: Vec::new(),
opacity: 1., opacity: 1.,
blend_mode: BlendMode::Normal,
transform: DAffine2::IDENTITY, transform: DAffine2::IDENTITY,
}; };
@@ -337,12 +306,6 @@ impl GraphicElement {
impl core::hash::Hash for GraphicElement { impl core::hash::Hash for GraphicElement {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) { fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.name.hash(state);
self.blend_mode.hash(state);
self.opacity.to_bits().hash(state);
self.visible.hash(state);
self.locked.hash(state);
self.collapsed.hash(state);
self.graphic_element_data.hash(state); self.graphic_element_data.hash(state);
} }
} }
@@ -59,8 +59,6 @@ pub struct SvgRender {
pub svg: SvgSegmentList, pub svg: SvgSegmentList,
pub svg_defs: String, pub svg_defs: String,
pub transform: DAffine2, pub transform: DAffine2,
pub opacity: f32,
pub blend_mode: BlendMode,
pub image_data: Vec<(u64, Image<Color>)>, pub image_data: Vec<(u64, Image<Color>)>,
indent: usize, indent: usize,
} }
@@ -71,8 +69,6 @@ impl SvgRender {
svg: SvgSegmentList::default(), svg: SvgSegmentList::default(),
svg_defs: String::new(), svg_defs: String::new(),
transform: DAffine2::IDENTITY, transform: DAffine2::IDENTITY,
opacity: 1.,
blend_mode: BlendMode::Normal,
image_data: Vec::new(), image_data: Vec::new(),
indent: 0, indent: 0,
} }
@@ -121,6 +117,7 @@ impl SvgRender {
self.indent(); self.indent();
self.svg.push("<"); self.svg.push("<");
self.svg.push(name.clone()); self.svg.push(name.clone());
// Wraps `self` in a newtype (1-tuple) which is then mutated by the `attributes` closure
attributes(&mut SvgRenderAttrs(self)); attributes(&mut SvgRenderAttrs(self));
self.svg.push(">"); self.svg.push(">");
let length = self.svg.len(); let length = self.svg.len();
@@ -183,6 +180,7 @@ pub fn format_transform_matrix(transform: DAffine2) -> String {
result.push(')'); result.push(')');
result result
} }
fn to_transform(transform: DAffine2) -> usvg::Transform { fn to_transform(transform: DAffine2) -> usvg::Transform {
let cols = transform.to_cols_array(); let cols = transform.to_cols_array();
usvg::Transform::from_row(cols[0] as f32, cols[1] as f32, cols[2] as f32, cols[3] as f32, cols[4] as f32, cols[5] as f32) usvg::Transform::from_row(cols[0] as f32, cols[1] as f32, cols[2] as f32, cols[3] as f32, cols[4] as f32, cols[5] as f32)
@@ -204,6 +202,7 @@ pub trait GraphicElementRendered {
let tree = usvg::Tree::from_str(&svg, &opt).expect("Failed to parse SVG"); let tree = usvg::Tree::from_str(&svg, &opt).expect("Failed to parse SVG");
tree.root.clone() tree.root.clone()
} }
fn to_usvg_tree(&self, resolution: glam::UVec2, viewbox: [DVec2; 2]) -> usvg::Tree { fn to_usvg_tree(&self, resolution: glam::UVec2, viewbox: [DVec2; 2]) -> usvg::Tree {
let root_node = self.to_usvg_node(); let root_node = self.to_usvg_node();
usvg::Tree { usvg::Tree {
@@ -219,26 +218,33 @@ pub trait GraphicElementRendered {
impl GraphicElementRendered for GraphicGroup { impl GraphicElementRendered for GraphicGroup {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) { fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
let old_opacity = render.opacity;
render.opacity *= self.opacity;
render.parent_tag( render.parent_tag(
"g", "g",
|attributes| attributes.push("transform", format_transform_matrix(self.transform)), |attributes| {
attributes.push("transform", format_transform_matrix(self.transform));
if self.opacity < 1. {
attributes.push("opacity", self.opacity.to_string());
}
if self.blend_mode != BlendMode::default() {
attributes.push("style", self.blend_mode.render());
}
},
|render| { |render| {
for element in self.iter() { for element in self.iter() {
render.blend_mode = element.blend_mode;
element.graphic_element_data.render_svg(render, render_params); element.graphic_element_data.render_svg(render, render_params);
} }
}, },
); );
render.opacity = old_opacity;
} }
fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]> { fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]> {
self.iter() self.iter()
.filter_map(|element| element.graphic_element_data.bounding_box(transform * self.transform)) .filter_map(|element| element.graphic_element_data.bounding_box(transform * self.transform))
.reduce(Quad::combine_bounds) .reduce(Quad::combine_bounds)
} }
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 {
@@ -260,24 +266,31 @@ impl GraphicElementRendered for VectorData {
for subpath in &self.subpaths { for subpath in &self.subpaths {
let _ = subpath.subpath_to_svg(&mut path, multiplied_transform); let _ = subpath.subpath_to_svg(&mut path, multiplied_transform);
} }
render.leaf_tag("path", |attributes| { render.leaf_tag("path", |attributes| {
attributes.push("class", "vector-data"); attributes.push("class", "vector-data");
attributes.push("d", path); attributes.push("d", path);
let render = &mut attributes.0;
let style = self.style.render(render_params.view_mode, &mut render.svg_defs, multiplied_transform, layer_bounds, transformed_bounds); let fill_and_stroke = self
attributes.push_val(style); .style
if attributes.0.blend_mode != BlendMode::default() { .render(render_params.view_mode, &mut attributes.0.svg_defs, multiplied_transform, layer_bounds, transformed_bounds);
attributes.push_complex("style", |v| { attributes.push_val(fill_and_stroke);
v.svg.push("mix-blend-mode: ");
v.svg.push(v.blend_mode.to_svg_style_name()); if self.style.opacity < 1. {
v.svg.push(";"); attributes.push("opacity", self.style.opacity.to_string());
}) }
if self.style.blend_mode != BlendMode::default() {
attributes.push("style", self.style.blend_mode.render());
} }
}); });
} }
fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]> { fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]> {
self.bounding_box_with_transform(self.transform * transform) self.bounding_box_with_transform(self.transform * transform)
} }
fn add_click_targets(&self, click_targets: &mut Vec<ClickTarget>) { fn add_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
let stroke_width = self.style.stroke().as_ref().map_or(0., crate::vector::style::Stroke::weight); let stroke_width = self.style.stroke().as_ref().map_or(0., crate::vector::style::Stroke::weight);
let update_closed = |mut subpath: bezier_rs::Subpath<ManipulatorGroupId>| { let update_closed = |mut subpath: bezier_rs::Subpath<ManipulatorGroupId>| {
@@ -345,19 +358,24 @@ impl GraphicElementRendered for Artboard {
attributes.push("font-size", "14px"); attributes.push("font-size", "14px");
}, },
|render| { |render| {
// TODO: Use the artboard's layer name
render.svg.push("Artboard"); render.svg.push("Artboard");
}, },
); );
// Contents group // Contents group (includes the artwork but not the background)
render.parent_tag( render.parent_tag(
// SVG group tag
"g", "g",
// Group tag attributes
|attributes| { |attributes| {
attributes.push("class", "artboard"); attributes.push("class", "artboard");
attributes.push( attributes.push(
"transform", "transform",
format_transform_matrix(DAffine2::from_translation(self.location.as_dvec2()) * self.graphic_group.transform), format_transform_matrix(DAffine2::from_translation(self.location.as_dvec2()) * self.graphic_group.transform),
); );
if self.clip { if self.clip {
let id = format!("artboard-{}", generate_uuid()); let id = format!("artboard-{}", generate_uuid());
let selector = format!("url(#{id})"); let selector = format!("url(#{id})");
@@ -373,19 +391,15 @@ impl GraphicElementRendered for Artboard {
attributes.push("clip-path", selector); attributes.push("clip-path", selector);
} }
}, },
// Artboard contents
|render| { |render| {
let old_opacity = render.opacity;
render.opacity *= self.graphic_group.opacity;
// Contents
for element in self.graphic_group.iter() { for element in self.graphic_group.iter() {
render.blend_mode = element.blend_mode;
element.graphic_element_data.render_svg(render, render_params); element.graphic_element_data.render_svg(render, render_params);
} }
render.opacity = old_opacity;
}, },
); );
} }
fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]> { fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]> {
let artboard_bounds = (transform * Quad::from_box([self.location.as_dvec2(), self.location.as_dvec2() + self.dimensions.as_dvec2()])).bounding_box(); let artboard_bounds = (transform * Quad::from_box([self.location.as_dvec2(), self.location.as_dvec2() + self.dimensions.as_dvec2()])).bounding_box();
if self.clip { if self.clip {
@@ -394,6 +408,7 @@ impl GraphicElementRendered for Artboard {
[self.graphic_group.bounding_box(transform), Some(artboard_bounds)].into_iter().flatten().reduce(Quad::combine_bounds) [self.graphic_group.bounding_box(transform), Some(artboard_bounds)].into_iter().flatten().reduce(Quad::combine_bounds)
} }
} }
fn add_click_targets(&self, click_targets: &mut Vec<ClickTarget>) { fn add_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
let subpath = Subpath::new_rect(DVec2::ZERO, self.dimensions.as_dvec2()); let subpath = Subpath::new_rect(DVec2::ZERO, self.dimensions.as_dvec2());
click_targets.push(ClickTarget { stroke_width: 0., subpath }); click_targets.push(ClickTarget { stroke_width: 0., subpath });
@@ -412,7 +427,10 @@ impl GraphicElementRendered for ImageFrame<Color> {
attributes.push("height", 1.to_string()); attributes.push("height", 1.to_string());
attributes.push("preserveAspectRatio", "none"); attributes.push("preserveAspectRatio", "none");
attributes.push("transform", transform); attributes.push("transform", transform);
attributes.push("href", SvgSegment::BlobUrl(uuid)) attributes.push("href", SvgSegment::BlobUrl(uuid));
if self.blend_mode != BlendMode::default() {
attributes.push("style", self.blend_mode.render());
}
}); });
render.image_data.push((uuid, self.image.clone())) render.image_data.push((uuid, self.image.clone()))
} }
@@ -429,11 +447,13 @@ impl GraphicElementRendered for ImageFrame<Color> {
render.leaf_tag("image", |attributes| { render.leaf_tag("image", |attributes| {
attributes.push("width", 1.to_string()); attributes.push("width", 1.to_string());
attributes.push("height", 1.to_string()); attributes.push("height", 1.to_string());
attributes.push("preserveAspectRatio", "none"); attributes.push("preserveAspectRatio", "none");
attributes.push("transform", transform); attributes.push("transform", transform);
attributes.push("href", base64_string) attributes.push("href", base64_string);
if self.blend_mode != BlendMode::default() {
attributes.push("style", self.blend_mode.render());
}
}); });
} }
ImageRenderMode::Canvas => { ImageRenderMode::Canvas => {
@@ -441,10 +461,12 @@ impl GraphicElementRendered for ImageFrame<Color> {
} }
} }
} }
fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]> { fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]> {
let transform = self.transform * transform; let transform = self.transform * transform;
(transform.matrix2 != glam::DMat2::ZERO).then(|| (transform * Quad::from_box([DVec2::ZERO, DVec2::ONE])).bounding_box()) (transform.matrix2 != glam::DMat2::ZERO).then(|| (transform * Quad::from_box([DVec2::ZERO, DVec2::ONE])).bounding_box())
} }
fn add_click_targets(&self, click_targets: &mut Vec<ClickTarget>) { fn add_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
let subpath = Subpath::new_rect(DVec2::ZERO, DVec2::ONE); let subpath = Subpath::new_rect(DVec2::ZERO, DVec2::ONE);
click_targets.push(ClickTarget { subpath, stroke_width: 0. }); click_targets.push(ClickTarget { subpath, stroke_width: 0. });
@@ -563,6 +585,7 @@ impl GraphicElementRendered for Option<Color> {
render.parent_tag("text", |_| {}, |render| render.leaf_node("Empty color")); render.parent_tag("text", |_| {}, |render| render.leaf_node("Empty color"));
return; return;
}; };
let color_info = format!("{:?} #{} {:?}", color, color.rgba_hex(), color.to_rgba8_srgb());
render.leaf_tag("rect", |attributes| { render.leaf_tag("rect", |attributes| {
attributes.push("width", "100"); attributes.push("width", "100");
@@ -570,7 +593,6 @@ impl GraphicElementRendered for Option<Color> {
attributes.push("y", "40"); attributes.push("y", "40");
attributes.push("fill", format!("#{}", color.rgba_hex())); attributes.push("fill", format!("#{}", color.rgba_hex()));
}); });
let color_info = format!("{:?} #{} {:?}", color, color.rgba_hex(), color.to_rgba8_srgb());
render.parent_tag("text", text_attributes, |render| render.leaf_node(color_info)) render.parent_tag("text", text_attributes, |render| render.leaf_node(color_info))
} }
+1
View File
@@ -5,6 +5,7 @@ pub struct LogToConsoleNode;
#[node_macro::node_fn(LogToConsoleNode)] #[node_macro::node_fn(LogToConsoleNode)]
fn log_to_console<T: core::fmt::Debug>(value: T) -> T { fn log_to_console<T: core::fmt::Debug>(value: T) -> T {
#[cfg(not(target_arch = "spirv"))] #[cfg(not(target_arch = "spirv"))]
// KEEP THIS `debug!()` - It acts as the output for the debug node itself
debug!("{value:#?}"); debug!("{value:#?}");
value value
} }
+35 -6
View File
@@ -90,15 +90,15 @@ pub enum BlendMode {
// Not supported by SVG, but we should someday support: Dissolve // Not supported by SVG, but we should someday support: Dissolve
// Darken group // Darken group
Multiply,
Darken, Darken,
Multiply,
ColorBurn, ColorBurn,
LinearBurn, LinearBurn,
DarkerColor, DarkerColor,
// Lighten group // Lighten group
Screen,
Lighten, Lighten,
Screen,
ColorDodge, ColorDodge,
LinearDodge, LinearDodge,
LighterColor, LighterColor,
@@ -172,6 +172,7 @@ impl core::fmt::Display for BlendMode {
} }
} }
} }
impl BlendMode { impl BlendMode {
/// Convert the enum to the CSS string for the blend mode. /// Convert the enum to the CSS string for the blend mode.
/// [Read more](https://developer.mozilla.org/en-US/docs/Web/CSS/blend-mode#values) /// [Read more](https://developer.mozilla.org/en-US/docs/Web/CSS/blend-mode#values)
@@ -206,6 +207,11 @@ impl BlendMode {
} }
} }
/// Renders the blend mode CSS style declaration.
pub fn render(&self) -> String {
format!(r#" mix-blend-mode: {};"#, self.to_svg_style_name())
}
/// List of all the blend modes in their conventional ordering and grouping. /// List of all the blend modes in their conventional ordering and grouping.
pub fn list_modes_in_groups() -> [&'static [BlendMode]; 6] { pub fn list_modes_in_groups() -> [&'static [BlendMode]; 6] {
[ [
@@ -898,32 +904,55 @@ pub struct OpacityNode<O> {
} }
#[node_macro::node_fn(OpacityNode)] #[node_macro::node_fn(OpacityNode)]
fn image_opacity(color: Color, opacity_multiplier: f32) -> Color { fn opacity_node(color: Color, opacity_multiplier: f32) -> Color {
let opacity_multiplier = opacity_multiplier / 100.; let opacity_multiplier = opacity_multiplier / 100.;
Color::from_rgbaf32_unchecked(color.r(), color.g(), color.b(), color.a() * opacity_multiplier) Color::from_rgbaf32_unchecked(color.r(), color.g(), color.b(), color.a() * opacity_multiplier)
} }
#[node_macro::node_impl(OpacityNode)] #[node_macro::node_impl(OpacityNode)]
fn image_opacity(mut vector_data: VectorData, opacity_multiplier: f32) -> VectorData { fn opacity_node(mut vector_data: VectorData, opacity_multiplier: f32) -> VectorData {
let opacity_multiplier = opacity_multiplier / 100.; let opacity_multiplier = opacity_multiplier / 100.;
vector_data.style.opacity *= opacity_multiplier; vector_data.style.opacity *= opacity_multiplier;
vector_data vector_data
} }
#[node_macro::node_impl(OpacityNode)] #[node_macro::node_impl(OpacityNode)]
fn image_opacity(mut graphic_group: GraphicGroup, opacity_multiplier: f32) -> GraphicGroup { fn opacity_node(mut graphic_group: GraphicGroup, opacity_multiplier: f32) -> GraphicGroup {
let opacity_multiplier = opacity_multiplier / 100.; let opacity_multiplier = opacity_multiplier / 100.;
graphic_group.opacity *= opacity_multiplier; graphic_group.opacity *= opacity_multiplier;
graphic_group graphic_group
} }
#[derive(Debug, Clone, Copy)]
pub struct BlendModeNode<BM> {
blend_mode: BM,
}
#[node_macro::node_fn(BlendModeNode)]
fn blend_mode_node(mut vector_data: VectorData, blend_mode: BlendMode) -> VectorData {
vector_data.style.blend_mode = blend_mode;
vector_data
}
#[node_macro::node_impl(BlendModeNode)]
fn blend_mode_node(mut graphic_group: GraphicGroup, blend_mode: BlendMode) -> GraphicGroup {
graphic_group.blend_mode = blend_mode;
graphic_group
}
#[node_macro::node_impl(BlendModeNode)]
fn blend_mode_node(mut image_frame: ImageFrame<Color>, blend_mode: BlendMode) -> ImageFrame<Color> {
image_frame.blend_mode = blend_mode;
image_frame
}
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub struct PosterizeNode<P> { pub struct PosterizeNode<P> {
posterize_value: P, posterize_value: P,
} }
// Based on http://www.axiomx.com/posterize.htm // Based on http://www.axiomx.com/posterize.htm
// This algorithm is perfectly accurate. // This algorithm produces fully accurate output in relation to the industry standard.
#[node_macro::node_fn(PosterizeNode)] #[node_macro::node_fn(PosterizeNode)]
fn posterize(color: Color, posterize_value: f32) -> Color { fn posterize(color: Color, posterize_value: f32) -> Color {
let color = color.to_gamma_srgb(); let color = color.to_gamma_srgb();
+5 -1
View File
@@ -250,7 +250,6 @@ fn map_node<P: Pixel>(input: (u32, u32), data: Vec<P>) -> Image<P> {
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ImageFrame<P: Pixel> { pub struct ImageFrame<P: Pixel> {
pub image: Image<P>, pub image: Image<P>,
// The transform that maps image space to layer space. // The transform that maps image space to layer space.
// //
// Image space is unitless [0, 1] for both axes, with x axis positive // Image space is unitless [0, 1] for both axes, with x axis positive
@@ -261,6 +260,7 @@ pub struct ImageFrame<P: Pixel> {
// positive going right and y axis positive going down, with the origin // positive going right and y axis positive going down, with the origin
// being an unspecified quantity. // being an unspecified quantity.
pub transform: DAffine2, pub transform: DAffine2,
pub blend_mode: BlendMode,
} }
impl<P: Debug + Copy + Pixel> Sample for ImageFrame<P> { impl<P: Debug + Copy + Pixel> Sample for ImageFrame<P> {
@@ -312,6 +312,7 @@ impl<P: Copy + Pixel> ImageFrame<P> {
Self { Self {
image: Image::empty(), image: Image::empty(),
transform: DAffine2::ZERO, transform: DAffine2::ZERO,
blend_mode: BlendMode::Normal,
} }
} }
@@ -319,6 +320,7 @@ impl<P: Copy + Pixel> ImageFrame<P> {
Self { Self {
image: Image::empty(), image: Image::empty(),
transform: DAffine2::IDENTITY, transform: DAffine2::IDENTITY,
blend_mode: BlendMode::Normal,
} }
} }
@@ -379,6 +381,7 @@ impl From<ImageFrame<Color>> for ImageFrame<SRGBA8> {
height: image.image.height, height: image.image.height,
}, },
transform: image.transform, transform: image.transform,
blend_mode: BlendMode::Normal,
} }
} }
} }
@@ -393,6 +396,7 @@ impl From<ImageFrame<SRGBA8>> for ImageFrame<Color> {
height: image.image.height, height: image.image.height,
}, },
transform: image.transform, transform: image.transform,
blend_mode: BlendMode::Normal,
} }
} }
} }
+1 -1
View File
@@ -106,8 +106,8 @@ impl<'i, T: Clone + 'i> Node<'i, ()> for DebugClonedNode<T> {
type Output = T; type Output = T;
#[inline(always)] #[inline(always)]
fn eval(&'i self, _input: ()) -> Self::Output { fn eval(&'i self, _input: ()) -> Self::Output {
// KEEP THIS `debug!()` - It acts as the output for the debug node itself
#[cfg(not(target_arch = "spirv"))] #[cfg(not(target_arch = "spirv"))]
// KEEP THIS `debug!()` - It acts as the output for the debug node itself
log::debug!("DebugClonedNode::eval"); log::debug!("DebugClonedNode::eval");
self.0.clone() self.0.clone()
@@ -99,6 +99,7 @@ fn spline_generator(_input: (), positions: Vec<DVec2>) -> VectorData {
// TODO(TrueDoctor): I removed the Arc requirement we should think about when it makes sense to use it vs making a generic value node // TODO(TrueDoctor): I removed the Arc requirement we should think about when it makes sense to use it vs making a generic value node
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct PathGenerator<Mirror> { pub struct PathGenerator<Mirror> {
// TODO: Keavon asks: what is this for? Is it dead code? It seems to only be set, never read.
mirror: Mirror, mirror: Mirror,
} }
+33 -23
View File
@@ -1,6 +1,7 @@
//! Contains stylistic options for SVG elements. //! Contains stylistic options for SVG elements.
use crate::consts::{LAYER_OUTLINE_STROKE_COLOR, LAYER_OUTLINE_STROKE_WEIGHT}; use crate::consts::{LAYER_OUTLINE_STROKE_COLOR, LAYER_OUTLINE_STROKE_WEIGHT};
use crate::raster::BlendMode;
use crate::Color; use crate::Color;
use dyn_any::{DynAny, StaticType}; use dyn_any::{DynAny, StaticType};
@@ -12,9 +13,9 @@ use std::fmt::{self, Display, Write};
/// A value of 3 would correspond to a precision of 10^-3. /// A value of 3 would correspond to a precision of 10^-3.
const OPACITY_PRECISION: usize = 3; const OPACITY_PRECISION: usize = 3;
fn format_opacity(name: &str, opacity: f32) -> String { fn format_opacity(attribute: &str, opacity: f32) -> String {
if (opacity - 1.).abs() > 10_f32.powi(-(OPACITY_PRECISION as i32)) { if (opacity - 1.).abs() > 10_f32.powi(-(OPACITY_PRECISION as i32)) {
format!(r#" {name}-opacity="{opacity:.OPACITY_PRECISION$}""#) format!(r#" {attribute}="{opacity:.OPACITY_PRECISION$}""#)
} else { } else {
String::new() String::new()
} }
@@ -64,15 +65,15 @@ impl Gradient {
} }
} }
/// Adds the gradient def, returning the gradient id /// Adds the gradient def through mutating the first argument, returning the gradient ID.
fn render_defs(&self, svg_defs: &mut String, multiplied_transform: DAffine2, bounds: [DVec2; 2], transformed_bounds: [DVec2; 2], opacity: f32) -> u64 { fn render_defs(&self, svg_defs: &mut String, multiplied_transform: DAffine2, bounds: [DVec2; 2], transformed_bounds: [DVec2; 2]) -> u64 {
let bound_transform = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]); let bound_transform = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]);
let transformed_bound_transform = DAffine2::from_scale_angle_translation(transformed_bounds[1] - transformed_bounds[0], 0., transformed_bounds[0]); let transformed_bound_transform = DAffine2::from_scale_angle_translation(transformed_bounds[1] - transformed_bounds[0], 0., transformed_bounds[0]);
let updated_transform = multiplied_transform * bound_transform; let updated_transform = multiplied_transform * bound_transform;
let mut positions = String::new(); let mut positions = String::new();
for (position, color) in self.positions.iter().filter_map(|(pos, color)| color.map(|color| (pos, color))) { for (position, color) in self.positions.iter().filter_map(|(pos, color)| color.map(|color| (pos, color))) {
let _ = write!(positions, r##"<stop offset="{}" stop-color="#{}" />"##, position, color.with_alpha(color.a() * opacity).rgba_hex()); let _ = write!(positions, r##"<stop offset="{}" stop-color="#{}" />"##, position, color.with_alpha(color.a()).rgba_hex());
} }
let mod_gradient = transformed_bound_transform.inverse(); let mod_gradient = transformed_bound_transform.inverse();
@@ -178,13 +179,13 @@ impl Fill {
} }
} }
/// Renders the fill, adding necessary defs. /// Renders the fill, adding necessary defs through mutating the first argument.
pub fn render(&self, svg_defs: &mut String, multiplied_transform: DAffine2, bounds: [DVec2; 2], transformed_bounds: [DVec2; 2], opacity: f32) -> String { pub fn render(&self, svg_defs: &mut String, multiplied_transform: DAffine2, bounds: [DVec2; 2], transformed_bounds: [DVec2; 2]) -> String {
match self { match self {
Self::None => r#" fill="none""#.to_string(), Self::None => r#" fill="none""#.to_string(),
Self::Solid(color) => format!(r##" fill="#{}"{}"##, color.rgb_hex(), format_opacity("fill", color.a() * opacity)), Self::Solid(color) => format!(r##" fill="#{}"{}"##, color.rgb_hex(), format_opacity("fill-opacity", color.a())),
Self::Gradient(gradient) => { Self::Gradient(gradient) => {
let gradient_id = gradient.render_defs(svg_defs, multiplied_transform, bounds, transformed_bounds, opacity); let gradient_id = gradient.render_defs(svg_defs, multiplied_transform, bounds, transformed_bounds);
format!(r##" fill="url('#{gradient_id}')""##) format!(r##" fill="url('#{gradient_id}')""##)
} }
} }
@@ -326,12 +327,12 @@ impl Stroke {
} }
/// Provide the SVG attributes for the stroke. /// Provide the SVG attributes for the stroke.
pub fn render(&self, opacity: f32) -> String { pub fn render(&self) -> String {
if let Some(color) = self.color { if let Some(color) = self.color {
format!( format!(
r##" stroke="#{}"{} stroke-width="{}" stroke-dasharray="{}" stroke-dashoffset="{}" stroke-linecap="{}" stroke-linejoin="{}" stroke-miterlimit="{}" "##, r##" stroke="#{}"{} stroke-width="{}" stroke-dasharray="{}" stroke-dashoffset="{}" stroke-linecap="{}" stroke-linejoin="{}" stroke-miterlimit="{}" "##,
color.rgb_hex(), color.rgb_hex(),
format_opacity("stroke", opacity * color.a()), format_opacity("stroke-opacity", color.a()),
self.weight, self.weight,
self.dash_lengths(), self.dash_lengths(),
self.dash_offset, self.dash_offset,
@@ -410,6 +411,7 @@ pub struct PathStyle {
stroke: Option<Stroke>, stroke: Option<Stroke>,
fill: Fill, fill: Fill,
pub opacity: f32, pub opacity: f32,
pub blend_mode: BlendMode,
} }
impl core::hash::Hash for PathStyle { impl core::hash::Hash for PathStyle {
@@ -417,12 +419,18 @@ impl core::hash::Hash for PathStyle {
self.stroke.hash(state); self.stroke.hash(state);
self.fill.hash(state); self.fill.hash(state);
self.opacity.to_bits().hash(state); self.opacity.to_bits().hash(state);
self.blend_mode.hash(state);
} }
} }
impl PathStyle { impl PathStyle {
pub const fn new(stroke: Option<Stroke>, fill: Fill) -> Self { pub const fn new(stroke: Option<Stroke>, fill: Fill) -> Self {
Self { stroke, fill, opacity: 1. } Self {
stroke,
fill,
opacity: 1.,
blend_mode: BlendMode::Normal,
}
} }
/// Get the current path's [Fill]. /// Get the current path's [Fill].
@@ -529,18 +537,20 @@ impl PathStyle {
self.stroke = None; self.stroke = None;
} }
/// Renders the shape's fill and stroke attributes as a string with them concatenated together.
pub fn render(&self, view_mode: ViewMode, svg_defs: &mut String, multiplied_transform: DAffine2, bounds: [DVec2; 2], transformed_bounds: [DVec2; 2]) -> String { pub fn render(&self, view_mode: ViewMode, svg_defs: &mut String, multiplied_transform: DAffine2, bounds: [DVec2; 2], transformed_bounds: [DVec2; 2]) -> String {
let fill_attribute = match (view_mode, &self.fill) { match view_mode {
(ViewMode::Outline, _) => Fill::None.render(svg_defs, multiplied_transform, bounds, transformed_bounds, self.opacity), ViewMode::Outline => {
(_, fill) => fill.render(svg_defs, multiplied_transform, bounds, transformed_bounds, self.opacity), let fill_attribute = Fill::None.render(svg_defs, multiplied_transform, bounds, transformed_bounds);
}; let stroke_attribute = Stroke::new(Some(LAYER_OUTLINE_STROKE_COLOR), LAYER_OUTLINE_STROKE_WEIGHT).render();
let stroke_attribute = match (view_mode, &self.stroke) { format!("{fill_attribute}{stroke_attribute}")
(ViewMode::Outline, _) => Stroke::new(Some(LAYER_OUTLINE_STROKE_COLOR), LAYER_OUTLINE_STROKE_WEIGHT).render(self.opacity), }
(_, Some(stroke)) => stroke.render(self.opacity), _ => {
(_, None) => String::new(), let fill_attribute = self.fill.render(svg_defs, multiplied_transform, bounds, transformed_bounds);
}; let stroke_attribute = self.stroke.as_ref().map(|stroke| stroke.render()).unwrap_or_default();
format!("{fill_attribute}{stroke_attribute}")
format!("{fill_attribute}{stroke_attribute}") }
}
} }
} }
+3 -2
View File
@@ -15,6 +15,7 @@ pub struct VectorData {
pub subpaths: Vec<bezier_rs::Subpath<ManipulatorGroupId>>, pub subpaths: Vec<bezier_rs::Subpath<ManipulatorGroupId>>,
pub transform: DAffine2, pub transform: DAffine2,
pub style: PathStyle, pub style: PathStyle,
// TODO: Keavon asks: what is this for? Is it dead code? It seems to only be set, never read.
pub mirror_angle: Vec<ManipulatorGroupId>, pub mirror_angle: Vec<ManipulatorGroupId>,
} }
@@ -47,12 +48,12 @@ impl VectorData {
self.subpaths.iter().find_map(|subpath| subpath.manipulator_from_id(id)) self.subpaths.iter().find_map(|subpath| subpath.manipulator_from_id(id))
} }
/// Construct some new vector data from a single subpath with an identy transform and black fill. /// Construct some new vector data from a single subpath with an identity transform and black fill.
pub fn from_subpath(subpath: bezier_rs::Subpath<ManipulatorGroupId>) -> Self { pub fn from_subpath(subpath: bezier_rs::Subpath<ManipulatorGroupId>) -> Self {
Self::from_subpaths(vec![subpath]) Self::from_subpaths(vec![subpath])
} }
/// Construct some new vector data from subpaths with an identy transform and black fill. /// Construct some new vector data from subpaths with an identity transform and black fill.
pub fn from_subpaths(subpaths: Vec<bezier_rs::Subpath<ManipulatorGroupId>>) -> Self { pub fn from_subpaths(subpaths: Vec<bezier_rs::Subpath<ManipulatorGroupId>>) -> Self {
super::VectorData { subpaths, ..Self::empty() } super::VectorData { subpaths, ..Self::empty() }
} }
+19 -5
View File
@@ -43,6 +43,11 @@ fn return_true() -> bool {
#[derive(Clone, Debug, PartialEq, Hash, DynAny)] #[derive(Clone, Debug, PartialEq, Hash, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct DocumentNode { pub struct DocumentNode {
// TODO: Rename to "name" (also rename the TODOs in the `DocumentNodeBlueprint` struct)
/// A name chosen by the user for this node. Empty indicates no given name, in which case the node's identifier is displayed to the user in italics.
#[serde(default)]
pub alias: String,
// TODO: Rename to "identifier" (also rename the TODOs in the `DocumentNodeBlueprint` struct)
/// An identifier used to display in the UI and to display the appropriate properties. /// An identifier used to display in the UI and to display the appropriate properties.
pub name: String, pub name: String,
/// The inputs to a node, which are either: /// The inputs to a node, which are either:
@@ -157,6 +162,7 @@ pub struct DocumentNode {
impl Default for DocumentNode { impl Default for DocumentNode {
fn default() -> Self { fn default() -> Self {
Self { Self {
alias: Default::default(),
name: Default::default(), name: Default::default(),
inputs: Default::default(), inputs: Default::default(),
manual_composition: Default::default(), manual_composition: Default::default(),
@@ -262,6 +268,12 @@ impl DocumentNode {
} }
self self
} }
pub fn is_layer(&self) -> bool {
// TODO: Use something more robust than checking against a string.
// TODO: Or, more fundamentally separate the concept of a layer from a node.
self.name == "Layer"
}
} }
/// Represents the possible inputs to a node. /// Represents the possible inputs to a node.
@@ -405,9 +417,9 @@ pub struct NodeNetwork {
pub inputs: Vec<NodeId>, pub inputs: Vec<NodeId>,
pub outputs: Vec<NodeOutput>, pub outputs: Vec<NodeOutput>,
pub nodes: HashMap<NodeId, DocumentNode>, pub nodes: HashMap<NodeId, DocumentNode>,
/// These nodes are replaced with identity nodes when flattening /// These nodes are replaced with identity nodes during the graph flattening step
pub disabled: Vec<NodeId>, pub disabled: Vec<NodeId>,
/// In the case where a new node is chosen as output - what was the original /// In the case when a new node is chosen as a temporary output, this stores what it used to be so it can be restored later
pub previous_outputs: Option<Vec<NodeOutput>>, pub previous_outputs: Option<Vec<NodeOutput>>,
} }
@@ -811,9 +823,9 @@ impl NodeNetwork {
if node.implementation != DocumentNodeImplementation::Unresolved("graphene_core::ops::IdNode".into()) && self.disabled.contains(&id) { if node.implementation != DocumentNodeImplementation::Unresolved("graphene_core::ops::IdNode".into()) && self.disabled.contains(&id) {
node.implementation = DocumentNodeImplementation::Unresolved("graphene_core::ops::IdNode".into()); node.implementation = DocumentNodeImplementation::Unresolved("graphene_core::ops::IdNode".into());
if node.name == "Layer" { if node.is_layer() {
// Connect layer node to the graphic group below // Connect layer node to the graphic group below
node.inputs.drain(..7); node.inputs.drain(..1);
} else { } else {
node.inputs.drain(1..); node.inputs.drain(1..);
} }
@@ -873,8 +885,10 @@ impl NodeNetwork {
assert_eq!( assert_eq!(
node.inputs.len(), node.inputs.len(),
inner_network.inputs.len(), inner_network.inputs.len(),
"The number of inputs to the node and the inner network must be the same for {}. The node has {:?} inputs, the network has {:?} inputs.", "\n\nThe number of inputs to the node and the inner network must be the same for \"{}\". The node has {} inputs, the network has {} inputs.\n\nNode inputs:\n\n{:?}\n\nNetwork inputs:\n\n{:?}\n",
node.name, node.name,
node.inputs.len(),
inner_network.inputs.len(),
node.inputs, node.inputs,
inner_network.inputs inner_network.inputs
); );
+2 -2
View File
@@ -621,7 +621,7 @@ impl TypingContext {
let impls = self let impls = self
.lookup .lookup
.get(&node.identifier) .get(&node.identifier)
.ok_or(format!("No implementations found for {:?}. Other implementations found {:?}", node.identifier, self.lookup))?; .ok_or(format!("No implementations found for:\n\n{:?}\n\nOther implementations found:\n\n{:?}", node.identifier, self.lookup))?;
if matches!(input, Type::Generic(_)) { if matches!(input, Type::Generic(_)) {
return Err(format!("Generic types are not supported as inputs yet {:?} occurred in {:?}", input, node.identifier)); return Err(format!("Generic types are not supported as inputs yet {:?} occurred in {:?}", input, node.identifier));
@@ -673,7 +673,7 @@ impl TypingContext {
[] => { [] => {
dbg!(&self.inferred); dbg!(&self.inferred);
Err(format!( Err(format!(
"No implementations found for {identifier} with \ninput: {input:?} and \nparameters: {parameters:?}.\nOther Implementations found: {:?}", "No implementations found for:\n\n{identifier}\n\nwith input:\n\n{input:?}\n\nand parameters:\n\n{parameters:?}\n\nOther Implementations found:\n\n{:?}",
impls.keys().collect::<Vec<_>>(), impls.keys().collect::<Vec<_>>(),
)) ))
} }
+6 -1
View File
@@ -356,6 +356,7 @@ async fn brush(image: ImageFrame<Color>, bounds: ImageFrame<Color>, strokes: Vec
let opaque_image = ImageFrame { let opaque_image = ImageFrame {
image: Image::new(bbox.size().x as u32, bbox.size().y as u32, Color::WHITE), image: Image::new(bbox.size().x as u32, bbox.size().y as u32, Color::WHITE),
transform: background_bounds, transform: background_bounds,
blend_mode: BlendMode::Normal,
}; };
let mut erase_restore_mask = opaque_image; let mut erase_restore_mask = opaque_image;
@@ -409,7 +410,11 @@ mod test {
#[test] #[test]
fn test_translate_node() { fn test_translate_node() {
let image = Image::new(10, 10, Color::TRANSPARENT); let image = Image::new(10, 10, Color::TRANSPARENT);
let mut image = ImageFrame { image, transform: DAffine2::IDENTITY }; let mut image = ImageFrame {
image,
transform: DAffine2::IDENTITY,
blend_mode: BlendMode::Normal,
};
image.translate(DVec2::new(1., 2.)); image.translate(DVec2::new(1., 2.));
let translate_node = TranslateNode::new(ClonedNode::new(image)); let translate_node = TranslateNode::new(ClonedNode::new(image));
let image = translate_node.eval(DVec2::new(1., 2.)); let image = translate_node.eval(DVec2::new(1., 2.));
+3
View File
@@ -90,6 +90,7 @@ async fn map_gpu<'a: 'input>(image: ImageFrame<Color>, node: DocumentNode, edito
height: image.image.height, height: image.image.height,
}, },
transform: image.transform, transform: image.transform,
blend_mode: image.blend_mode,
}; };
// TODO: The cache should be based on the network topology not the node name // TODO: The cache should be based on the network topology not the node name
@@ -141,6 +142,7 @@ async fn map_gpu<'a: 'input>(image: ImageFrame<Color>, node: DocumentNode, edito
height: image.image.height, height: image.image.height,
}, },
transform: image.transform, transform: image.transform,
blend_mode: image.blend_mode,
} }
} }
@@ -586,5 +588,6 @@ async fn blend_gpu_image(foreground: ImageFrame<Color>, background: ImageFrame<C
height: background.image.height, height: background.image.height,
}, },
transform: background.transform, transform: background.transform,
blend_mode: background.blend_mode,
} }
} }
-1
View File
@@ -1,5 +1,4 @@
use autoquant::packing::ErrorFunction; use autoquant::packing::ErrorFunction;
use dyn_any::{DynAny, StaticType};
use graphene_core::quantization::*; use graphene_core::quantization::*;
use graphene_core::raster::{Color, ImageFrame}; use graphene_core::raster::{Color, ImageFrame};
use graphene_core::Node; use graphene_core::Node;
+26 -12
View File
@@ -89,15 +89,15 @@ fn sample(footprint: Footprint, image_frame: ImageFrame<Color>) -> ImageFrame<Co
let viewport_resolution_x = footprint.transform.transform_vector2(DVec2::X * size.x).length(); let viewport_resolution_x = footprint.transform.transform_vector2(DVec2::X * size.x).length();
let viewport_resolution_y = footprint.transform.transform_vector2(DVec2::Y * size.y).length(); let viewport_resolution_y = footprint.transform.transform_vector2(DVec2::Y * size.y).length();
let mut nwidth = size_px.x; let mut new_width = size_px.x;
let mut nheight = size_px.y; let mut new_height = size_px.y;
// Only downscale the image for now // Only downscale the image for now
let resized = if nwidth < image.width || nheight < image.height { let resized = if new_width < image.width || new_height < image.height {
nwidth = viewport_resolution_x as u32; new_width = viewport_resolution_x as u32;
nheight = viewport_resolution_y as u32; new_height = viewport_resolution_y as u32;
// TODO: choose filter based on quality reqirements // TODO: choose filter based on quality requirements
cropped.resize_exact(nwidth, nheight, image::imageops::Triangle) cropped.resize_exact(new_width, new_height, image::imageops::Triangle)
} else { } else {
cropped cropped
}; };
@@ -105,14 +105,18 @@ fn sample(footprint: Footprint, image_frame: ImageFrame<Color>) -> ImageFrame<Co
let buffer = buffer.into_raw(); let buffer = buffer.into_raw();
let vec = bytemuck::cast_vec(buffer); let vec = bytemuck::cast_vec(buffer);
let image = Image { let image = Image {
width: nwidth, width: new_width,
height: nheight, height: new_height,
data: vec, data: vec,
}; };
// we need to adjust the offset if we truncate the offset calculation // we need to adjust the offset if we truncate the offset calculation
let new_transform = image_frame.transform * DAffine2::from_translation(offset) * DAffine2::from_scale(size); let new_transform = image_frame.transform * DAffine2::from_translation(offset) * DAffine2::from_scale(size);
ImageFrame { image, transform: new_transform } ImageFrame {
image,
transform: new_transform,
blend_mode: image_frame.blend_mode,
}
} }
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
@@ -305,6 +309,7 @@ where
let mut new_background = ImageFrame { let mut new_background = ImageFrame {
image: new_background, image: new_background,
transform: transfrom, transform: transfrom,
blend_mode: background.blend_mode,
}; };
new_background = blend_image(background, new_background, map_fn); new_background = blend_image(background, new_background, map_fn);
@@ -417,6 +422,7 @@ fn extend_image_to_bounds_node(image: ImageFrame<Color>, bounds: DAffine2) -> Im
ImageFrame { ImageFrame {
image: new_img, image: new_img,
transform: new_texture_to_layer_space, transform: new_texture_to_layer_space,
blend_mode: image.blend_mode,
} }
} }
@@ -450,7 +456,9 @@ fn empty_image<_P: Pixel>(transform: DAffine2, color: _P) -> ImageFrame<_P> {
let height = transform.transform_vector2(DVec2::new(0., 1.)).length() as u32; let height = transform.transform_vector2(DVec2::new(0., 1.)).length() as u32;
let image = Image::new(width, height, color); let image = Image::new(width, height, color);
ImageFrame { image, transform }
let blend_mode = BlendMode::Normal;
ImageFrame { image, transform, blend_mode }
} }
macro_rules! generate_imaginate_node { macro_rules! generate_imaginate_node {
@@ -538,7 +546,11 @@ pub struct ImageFrameNode<P, Transform> {
} }
#[node_macro::node_fn(ImageFrameNode<_P>)] #[node_macro::node_fn(ImageFrameNode<_P>)]
fn image_frame<_P: Pixel>(image: Image<_P>, transform: DAffine2) -> graphene_core::raster::ImageFrame<_P> { fn image_frame<_P: Pixel>(image: Image<_P>, transform: DAffine2) -> graphene_core::raster::ImageFrame<_P> {
graphene_core::raster::ImageFrame { image, transform } graphene_core::raster::ImageFrame {
image,
transform,
blend_mode: BlendMode::Normal,
}
} }
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
@@ -564,6 +576,7 @@ fn pixel_noise(width: u32, height: u32, seed: u32, noise_type: NoiseType) -> gra
ImageFrame::<Color> { ImageFrame::<Color> {
image, image,
transform: DAffine2::from_scale(DVec2::new(width as f64, height as f64)), transform: DAffine2::from_scale(DVec2::new(width as f64, height as f64)),
blend_mode: BlendMode::Normal,
} }
} }
@@ -608,6 +621,7 @@ fn mandelbrot_node(footprint: Footprint) -> ImageFrame<Color> {
ImageFrame { ImageFrame {
image: Image { width, height, data }, image: Image { width, height, data },
transform: DAffine2::from_translation(offset) * DAffine2::from_scale(size), transform: DAffine2::from_translation(offset) * DAffine2::from_scale(size),
blend_mode: BlendMode::Normal,
} }
} }
@@ -280,6 +280,7 @@ fn decode_image_node<'a: 'input>(data: Arc<[u8]>) -> ImageFrame<Color> {
height: image.height(), height: image.height(),
}, },
transform: glam::DAffine2::IDENTITY, transform: glam::DAffine2::IDENTITY,
blend_mode: graphene_core::raster::BlendMode::Normal,
}; };
image image
} }
@@ -79,7 +79,7 @@ macro_rules! register_node {
} }
macro_rules! async_node { macro_rules! async_node {
// TODO: we currently need to annotate the type here because the compiler would otherwise (correctly) // TODO: we currently need to annotate the type here because the compiler would otherwise (correctly)
// assign a Pin<Box<dyn Fututure<Output=T>>> type to the node, which is not what we want for now. // assign a Pin<Box<dyn Future<Output=T>>> type to the node, which is not what we want for now.
($path:ty, input: $input:ty, output: $output:ty, params: [ $($type:ty),*]) => { ($path:ty, input: $input:ty, output: $output:ty, params: [ $($type:ty),*]) => {
async_node!($path, input: $input, output: $output, fn_params: [ $(() => $type),*]) async_node!($path, input: $input, output: $output, fn_params: [ $(() => $type),*])
}; };
@@ -312,6 +312,7 @@ fn node_registry() -> HashMap<NodeIdentifier, HashMap<NodeIOTypes, NodeConstruct
let empty_image = ImageFrame { let empty_image = ImageFrame {
image: Image::new(bounds.x, bounds.y, Color::BLACK), image: Image::new(bounds.x, bounds.y, Color::BLACK),
transform, transform,
blend_mode: BlendMode::Normal,
}; };
let final_image = ClonedNode::new(empty_image).then(complete_node); let final_image = ClonedNode::new(empty_image).then(complete_node);
let final_image = FutureWrapperNode::new(final_image); let final_image = FutureWrapperNode::new(final_image);
@@ -545,6 +546,9 @@ fn node_registry() -> HashMap<NodeIdentifier, HashMap<NodeIOTypes, NodeConstruct
raster_node!(graphene_core::raster::OpacityNode<_>, params: [f32]), raster_node!(graphene_core::raster::OpacityNode<_>, params: [f32]),
register_node!(graphene_core::raster::OpacityNode<_>, input: VectorData, params: [f32]), register_node!(graphene_core::raster::OpacityNode<_>, input: VectorData, params: [f32]),
register_node!(graphene_core::raster::OpacityNode<_>, input: GraphicGroup, params: [f32]), register_node!(graphene_core::raster::OpacityNode<_>, input: GraphicGroup, params: [f32]),
register_node!(graphene_core::raster::BlendModeNode<_>, input: VectorData, params: [BlendMode]),
register_node!(graphene_core::raster::BlendModeNode<_>, input: GraphicGroup, params: [BlendMode]),
register_node!(graphene_core::raster::BlendModeNode<_>, input: ImageFrame<Color>, params: [BlendMode]),
raster_node!(graphene_core::raster::PosterizeNode<_>, params: [f32]), raster_node!(graphene_core::raster::PosterizeNode<_>, params: [f32]),
raster_node!(graphene_core::raster::ExposureNode<_, _, _>, params: [f32, f32, f32]), raster_node!(graphene_core::raster::ExposureNode<_, _, _>, params: [f32, f32, f32]),
register_node!(graphene_core::memo::LetNode<_>, input: Option<ImageFrame<Color>>, params: []), register_node!(graphene_core::memo::LetNode<_>, input: Option<ImageFrame<Color>>, params: []),
@@ -597,10 +601,10 @@ fn node_registry() -> HashMap<NodeIdentifier, HashMap<NodeIOTypes, NodeConstruct
|args: Vec<graph_craft::proto::SharedNodeContainer>| { |args: Vec<graph_craft::proto::SharedNodeContainer>| {
Box::pin(async move { Box::pin(async move {
use graphene_std::raster::ImaginateNode; use graphene_std::raster::ImaginateNode;
macro_rules! instanciate_imaginate_node { macro_rules! instantiate_imaginate_node {
($($i:expr,)*) => { ImaginateNode::new($(graphene_std::any::input_node(args[$i].clone()),)* ) }; ($($i:expr,)*) => { ImaginateNode::new($(graphene_std::any::input_node(args[$i].clone()),)* ) };
} }
let node: ImaginateNode<Color, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _> = instanciate_imaginate_node!(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,); let node: ImaginateNode<Color, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _> = instantiate_imaginate_node!(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,);
let any = graphene_std::any::DynAnyNode::new(node); let any = graphene_std::any::DynAnyNode::new(node);
any.into_type_erased() any.into_type_erased()
}) })
@@ -839,7 +843,7 @@ fn node_registry() -> HashMap<NodeIdentifier, HashMap<NodeIOTypes, NodeConstruct
register_node!(graphene_core::text::TextGenerator<_, _, _>, input: WasmEditorApi, params: [String, graphene_core::text::Font, f64]), register_node!(graphene_core::text::TextGenerator<_, _, _>, input: WasmEditorApi, params: [String, graphene_core::text::Font, f64]),
register_node!(graphene_std::brush::VectorPointsNode, input: VectorData, params: []), register_node!(graphene_std::brush::VectorPointsNode, input: VectorData, params: []),
register_node!(graphene_core::ExtractImageFrame, input: WasmEditorApi, params: []), register_node!(graphene_core::ExtractImageFrame, input: WasmEditorApi, params: []),
async_node!(graphene_core::ConstructLayerNode<_, _, _, _, _, _, _, _>, input: Footprint, output: GraphicGroup, fn_params: [Footprint => graphene_core::GraphicElementData, () => String, () => BlendMode, () => f32, () => bool, () => bool, () => bool, Footprint => GraphicGroup]), async_node!(graphene_core::ConstructLayerNode<_, _>, input: Footprint, output: GraphicGroup, fn_params: [Footprint => graphene_core::GraphicElementData, Footprint => GraphicGroup]),
register_node!(graphene_core::ToGraphicElementData, input: graphene_core::vector::VectorData, params: []), register_node!(graphene_core::ToGraphicElementData, input: graphene_core::vector::VectorData, params: []),
register_node!(graphene_core::ToGraphicElementData, input: ImageFrame<Color>, params: []), register_node!(graphene_core::ToGraphicElementData, input: ImageFrame<Color>, params: []),
register_node!(graphene_core::ToGraphicElementData, input: GraphicGroup, params: []), register_node!(graphene_core::ToGraphicElementData, input: GraphicGroup, params: []),