Artboard nodes (#1328)

* Create artboard nodes

* Update node when resizing artboard

* Render clipped artboards

* More stable feature

* Do not render old artboards

* Fix some issues with transforms

* Fix crash when drawing rectangle

* Format

* Allow renaming document from Properties panel

* Adjust artboard label styling

* Fix document graph refresh so artboards show up

* Make "Clear Artboards" coming soon

* Fix displaying an infinite canvas

* Show document name in node graph options bar

* info!() to debug!()

* Fix Properties panel not being cleared when all docs closed

* Remove dead code

* Remove debug logs added in this branch

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
0HyperCube 2023-07-27 07:35:17 +01:00 committed by Keavon Chambers
parent ad0dc3276e
commit 08f9be6aaf
26 changed files with 636 additions and 182 deletions

58
Cargo.lock generated
View File

@ -397,7 +397,7 @@ dependencies = [
"http",
"http-body",
"hyper",
"itoa 1.0.8",
"itoa 1.0.9",
"matchit",
"memchr",
"mime",
@ -1191,9 +1191,9 @@ checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650"
[[package]]
name = "dtoa"
version = "1.0.8"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "519b83cd10f5f6e969625a409f735182bea5558cd8b64c655806ceaae36f1999"
checksum = "dcbb2bf8e87535c23f7a8a321e364ce21462d0ff10cb6407820e8e96dfff6653"
[[package]]
name = "dtoa-short"
@ -1231,9 +1231,9 @@ dependencies = [
[[package]]
name = "dyn-clone"
version = "1.0.11"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68b0cf012f1230e43cd00ebb729c6bb58707ecfa8ad08b52ef3a4ccd2697fc30"
checksum = "304e6508efa593091e97a9abbc10f90aa7ca635b6d2784feff3c89d41dd12272"
[[package]]
name = "either"
@ -2320,7 +2320,7 @@ checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482"
dependencies = [
"bytes",
"fnv",
"itoa 1.0.8",
"itoa 1.0.9",
]
[[package]]
@ -2379,7 +2379,7 @@ dependencies = [
"http-body",
"httparse",
"httpdate",
"itoa 1.0.8",
"itoa 1.0.9",
"pin-project-lite",
"socket2",
"tokio",
@ -2598,9 +2598,9 @@ checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
[[package]]
name = "itoa"
version = "1.0.8"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62b02a5381cc465bd3041d84623d0fa3b66738b52b8e2fc3bab8ad63ab032f4a"
checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38"
[[package]]
name = "javascriptcore-rs"
@ -3597,9 +3597,9 @@ dependencies = [
[[package]]
name = "paste"
version = "1.0.13"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4b27ab7be369122c218afc2079489cdcb4b517c0a3fc386ff11e1fedfcc2b35"
checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"
[[package]]
name = "pathdiff"
@ -3856,9 +3856,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068"
[[package]]
name = "proc-macro2"
version = "1.0.64"
version = "1.0.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78803b62cbf1f46fde80d7c0e803111524b9877184cfe7c3033659490ac7a7da"
checksum = "92de25114670a878b1261c79c9f8f729fb97e95bac93f6312f583c60dd6a1dfe"
dependencies = [
"unicode-ident",
]
@ -3889,9 +3889,9 @@ dependencies = [
[[package]]
name = "quote"
version = "1.0.29"
version = "1.0.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "573015e8ab27661678357f27dc26460738fd2b6c86e46f386fde94cb5d913105"
checksum = "5907a1b7c277254a8b15170f6e7c97cfa60ee7872a3217663bb81151e48184bb"
dependencies = [
"proc-macro2",
]
@ -4281,9 +4281,9 @@ dependencies = [
[[package]]
name = "rustversion"
version = "1.0.13"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc31bd9b61a32c31f9650d18add92aa83a49ba979c143eefd27fe7177b05bd5f"
checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4"
[[package]]
name = "rustybuzz"
@ -4303,9 +4303,9 @@ dependencies = [
[[package]]
name = "ryu"
version = "1.0.14"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe232bdf6be8c8de797b22184ee71118d63780ea42ac85b61d1baa6d3b782ae9"
checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741"
[[package]]
name = "safe_arch"
@ -4420,9 +4420,9 @@ dependencies = [
[[package]]
name = "semver"
version = "1.0.17"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed"
checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918"
dependencies = [
"serde",
]
@ -4471,22 +4471,22 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.102"
version = "1.0.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5062a995d481b2308b6064e9af76011f2921c35f97b0468811ed9f6cd91dfed"
checksum = "d03b412469450d4404fe8499a268edd7f8b79fecb074b0d812ad64ca21f4031b"
dependencies = [
"itoa 1.0.8",
"itoa 1.0.9",
"ryu",
"serde",
]
[[package]]
name = "serde_path_to_error"
version = "0.1.13"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8acc4422959dd87a76cb117c191dcbffc20467f06c9100b76721dab370f24d3a"
checksum = "4beec8bce849d58d06238cb50db2e1c417cfeafa4c63f692b15c82b7c80f8335"
dependencies = [
"itoa 1.0.8",
"itoa 1.0.9",
"serde",
]
@ -4517,7 +4517,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
dependencies = [
"form_urlencoded",
"itoa 1.0.8",
"itoa 1.0.9",
"ryu",
"serde",
]
@ -5405,7 +5405,7 @@ version = "0.3.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59e399c068f43a5d116fedaf73b203fa4f9c519f17e2b34f63221d3792f81446"
dependencies = [
"itoa 1.0.8",
"itoa 1.0.9",
"libc",
"num_threads",
"serde",

View File

@ -5,7 +5,9 @@ use crate::messages::layout::utility_types::widgets::input_widgets::{CheckboxInp
use crate::messages::layout::utility_types::widgets::label_widgets::{Separator, SeparatorDirection, SeparatorType, TextLabel};
use crate::messages::prelude::*;
use glam::UVec2;
use graphene_core::uuid::generate_uuid;
use glam::{IVec2, UVec2};
/// A dialog to allow users to set some initial options about a new document.
#[derive(Debug, Clone, Default)]
@ -27,13 +29,20 @@ impl MessageHandler<NewDocumentDialogMessage, ()> for NewDocumentDialogMessageHa
responses.add(PortfolioMessage::NewDocumentWithName { name: self.name.clone() });
if !self.infinite && self.dimensions.x > 0 && self.dimensions.y > 0 {
let id = generate_uuid();
responses.add(ArtboardMessage::AddArtboard {
id: None,
id: Some(id),
position: (0., 0.),
size: (self.dimensions.x as f64, self.dimensions.y as f64),
});
responses.add(GraphOperationMessage::NewArtboard {
id,
artboard: graphene_core::Artboard::new(IVec2::ZERO, self.dimensions.as_ivec2()),
});
responses.add(DocumentMessage::ZoomCanvasToFitAll);
}
responses.add(NodeGraphMessage::RunDocumentGraph);
responses.add(NodeGraphMessage::UpdateNewNodeGraph);
}
}

View File

@ -150,6 +150,9 @@ pub enum FrontendMessage {
layout_target: LayoutTarget,
diff: Vec<WidgetDiff>,
},
UpdateDocumentNodeRender {
svg: String,
},
UpdateDocumentOverlays {
svg: String,
},
@ -163,6 +166,9 @@ pub enum FrontendMessage {
size: (f64, f64),
multiplier: (f64, f64),
},
UpdateDocumentTransform {
transform: String,
},
UpdateEyedropperSamplingState {
#[serde(rename = "mousePosition")]
mouse_position: Option<(f64, f64)>,

View File

@ -32,17 +32,7 @@ impl MessageHandler<InputPreprocessorMessage, KeyboardPlatformLayout> for InputP
// TODO: Extend this to multiple viewports instead of setting it to the value of this last loop iteration
self.viewport_bounds = bounds;
responses.add(Operation::TransformLayer {
path: vec![],
transform: glam::DAffine2::from_translation(translation).to_cols_array(),
});
responses.add(DocumentMessage::Artboard(
Operation::TransformLayer {
path: vec![],
transform: glam::DAffine2::from_translation(translation).to_cols_array(),
}
.into(),
));
responses.add(NavigationMessage::TranslateCanvas { delta: DVec2::ZERO });
responses.add(FrontendMessage::TriggerViewportResize);
}
}

View File

@ -61,9 +61,11 @@ impl MessageHandler<ArtboardMessage, &PersistentData> for ArtboardMessageHandler
responses.add(DocumentMessage::RenderDocument);
}
ClearArtboards => {
for &artboard in self.artboard_ids.iter() {
responses.add_front(ArtboardMessage::DeleteArtboard { artboard });
}
// TODO: Make this remove the artboard layers from the graph (and cleanly reconnect the artwork)
responses.add(DialogMessage::RequestComingSoonDialog { issue: None });
// for &artboard in self.artboard_ids.iter() {
// responses.add_front(ArtboardMessage::DeleteArtboard { artboard });
// }
}
DeleteArtboard { artboard } => {
self.artboard_ids.retain(|&id| id != artboard);
@ -78,11 +80,13 @@ impl MessageHandler<ArtboardMessage, &PersistentData> for ArtboardMessageHandler
responses.add(FrontendMessage::UpdateDocumentArtboards {
svg: r##"<rect width="100%" height="100%" fill="#ffffff" />"##.to_string(),
})
} else {
let render_data = RenderData::new(&persistent_data.font_cache, ViewMode::Normal, None);
responses.add(FrontendMessage::UpdateDocumentArtboards {
svg: self.artboards_document.render_root(&render_data),
});
// TODO: Delete this whole legacy code path when cleaning up/removing the old (non-node based) artboard implementation
// TODO: The below code was used to draw the non-node based artboards, but we still need the above code to draw the infinite canvas until the refactor is complete and all this code can be removed
// } else {
// let render_data = RenderData::new(&persistent_data.font_cache, ViewMode::Normal, None);
// responses.add(FrontendMessage::UpdateDocumentArtboards {
// svg: self.artboards_document.render_root(&render_data),
// });
}
}
ResizeArtboard { artboard, position, mut size } => {

View File

@ -124,6 +124,9 @@ pub enum DocumentMessage {
mouse: Option<(f64, f64)>,
},
Redo,
RenameDocument {
new_name: String,
},
RenameLayer {
layer_path: Vec<LayerId>,
new_name: String,

View File

@ -197,6 +197,7 @@ impl MessageHandler<DocumentMessage, (u64, &InputPreprocessorMessageHandler, &Pe
#[remain::unsorted]
PropertiesPanel(message) => {
let properties_panel_message_handler_data = PropertiesPanelMessageHandlerData {
document_name: &self.name.as_str(),
artwork_document: &self.document_legacy,
artboard_document: &self.artboard_message_handler.artboards_document,
selected_layers: &mut self.layer_metadata.iter().filter_map(|(path, data)| data.selected.then_some(path.as_slice())),
@ -208,7 +209,8 @@ impl MessageHandler<DocumentMessage, (u64, &InputPreprocessorMessageHandler, &Pe
}
#[remain::unsorted]
NodeGraph(message) => {
self.node_graph_handler.process_message(message, responses, (&mut self.document_legacy, executor, document_id));
self.node_graph_handler
.process_message(message, responses, (&mut self.document_legacy, executor, document_id, self.name.as_str()));
}
#[remain::unsorted]
GraphOperation(message) => GraphOperationMessageHandler.process_message(message, responses, (&mut self.document_legacy, &mut self.node_graph_handler)),
@ -659,6 +661,11 @@ impl MessageHandler<DocumentMessage, (u64, &InputPreprocessorMessageHandler, &Pe
responses.add(RenderDocument);
responses.add(FolderChanged { affected_folder_path: vec![] });
}
RenameDocument { new_name } => {
self.name = new_name;
responses.add(PortfolioMessage::UpdateOpenDocumentsList);
responses.add(NodeGraphMessage::UpdateNewNodeGraph);
}
RenameLayer { layer_path, new_name } => responses.add(DocumentOperation::RenameLayer { layer_path, new_name }),
RenderDocument => {
responses.add(FrontendMessage::UpdateDocumentArtwork {
@ -1096,6 +1103,7 @@ impl DocumentMessageHandler {
let starting_root_transform = document.navigation_handler.calculate_offset_transform(ipp.viewport_bounds.size() / 2.);
document.document_legacy.root.transform = starting_root_transform;
document.artboard_message_handler.artboards_document.root.transform = starting_root_transform;
document
}

View File

@ -10,6 +10,7 @@ use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo};
use document_legacy::document::Document;
use document_legacy::Operation as DocumentOperation;
use graphene_core::renderer::format_transform_matrix;
use glam::{DAffine2, DVec2};
use serde::{Deserialize, Serialize};
@ -352,6 +353,9 @@ impl NavigationMessageHandler {
}
.into(),
));
let transform = format_transform_matrix(self.calculate_offset_transform(scaled_half_viewport));
responses.add(FrontendMessage::UpdateDocumentTransform { transform });
// TODO: Artboard pos
}
pub fn center_zoom(&self, viewport_bounds: DVec2, zoom_factor: f64, mouse: DVec2) -> Message {

View File

@ -1,11 +1,13 @@
use crate::messages::prelude::*;
use graph_craft::document::NodeId;
use graphene_core::uuid::ManipulatorGroupId;
use graphene_core::vector::brush_stroke::BrushStroke;
use graphene_core::vector::style::{Fill, Stroke};
use graphene_core::vector::ManipulatorPointId;
use graphene_core::Artboard;
use glam::{DAffine2, DVec2};
use glam::{DAffine2, DVec2, IVec2};
pub type LayerIdentifier = Vec<document_legacy::LayerId>;
@ -51,6 +53,20 @@ pub enum GraphOperationMessage {
layer: LayerIdentifier,
strokes: Vec<BrushStroke>,
},
NewArtboard {
id: NodeId,
artboard: Artboard,
},
ResizeArtboard {
id: NodeId,
location: IVec2,
dimensions: IVec2,
},
DeleteArtboard {
id: NodeId,
},
ClearArtboards,
}
#[derive(PartialEq, Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]

View File

@ -4,12 +4,13 @@ use crate::messages::prelude::*;
use document_legacy::document::Document;
use document_legacy::{LayerId, Operation};
use graph_craft::document::value::TaggedValue;
use graph_craft::document::{generate_uuid, NodeId, NodeInput, NodeNetwork};
use graph_craft::document::{generate_uuid, DocumentNode, DocumentNodeMetadata, NodeId, NodeInput, NodeNetwork, NodeOutput};
use graphene_core::vector::brush_stroke::BrushStroke;
use graphene_core::vector::style::{Fill, FillType, Stroke};
use graphene_core::Artboard;
use transform_utils::LayerBounds;
use glam::{DAffine2, DVec2};
use glam::{DAffine2, DVec2, IVec2};
pub mod transform_utils;
@ -21,27 +22,152 @@ struct ModifyInputsContext<'a> {
node_graph: &'a mut NodeGraphMessageHandler,
responses: &'a mut VecDeque<Message>,
layer: &'a [LayerId],
outwards_links: HashMap<NodeId, Vec<NodeId>>,
layer_node: Option<NodeId>,
}
impl<'a> ModifyInputsContext<'a> {
/// Get the node network from the document
fn new(layer: &'a [LayerId], document: &'a mut Document, node_graph: &'a mut NodeGraphMessageHandler, responses: &'a mut VecDeque<Message>) -> Option<Self> {
document.layer_mut(layer).ok().and_then(|layer| layer.as_layer_network_mut().ok()).map(|network| Self {
outwards_links: network.collect_outwards_links(),
network,
node_graph,
responses,
layer,
layer_node: None,
})
}
/// Get the node network from the document
fn new_doc(document: &'a mut Document, node_graph: &'a mut NodeGraphMessageHandler, responses: &'a mut VecDeque<Message>) -> Self {
Self {
outwards_links: document.document_network.collect_outwards_links(),
network: &mut document.document_network,
node_graph,
responses,
layer: &[],
layer_node: None,
}
}
fn locate_layer(&mut self, mut id: NodeId) -> Option<NodeId> {
while self.network.nodes.get(&id)?.name != "Layer" {
id = self.outwards_links.get(&id)?.first().copied()?;
}
self.layer_node = Some(id);
Some(id)
}
/// Updates the input of an existing node
fn modify_existing_node_inputs(&mut self, node_id: NodeId, update_input: impl FnOnce(&mut Vec<NodeInput>)) {
let document_node = self.network.nodes.get_mut(&node_id).unwrap();
update_input(&mut document_node.inputs);
}
pub fn insert_between(&mut self, pre: NodeOutput, post: NodeOutput, mut node: DocumentNode, input: usize, output: usize) -> Option<NodeId> {
let id = generate_uuid();
let pre_node = self.network.nodes.get_mut(&pre.node_id)?;
node.metadata.position = pre_node.metadata.position;
let post_node = self.network.nodes.get_mut(&post.node_id)?;
node.inputs[input] = NodeInput::node(pre.node_id, pre.node_output_index);
post_node.inputs[post.node_output_index] = NodeInput::node(id, output);
self.network.nodes.insert(id, node);
self.shift_upstream(id, IVec2::new(-8, 0));
Some(id)
}
pub fn insert_layer_below(&mut self, node_id: NodeId, input_index: usize) -> Option<NodeId> {
let layer_node = resolve_document_node_type("Layer").expect("Layer node");
let new_id = generate_uuid();
let post_node = self.network.nodes.get_mut(&node_id)?;
post_node.inputs[input_index] = NodeInput::node(new_id, 0);
let document_node = layer_node.to_document_node_default_inputs([], DocumentNodeMetadata::position(post_node.metadata.position + IVec2::new(0, 2)));
self.network.nodes.insert(new_id, document_node);
Some(new_id)
}
pub fn insert_node_before(&mut self, new_id: NodeId, node_id: NodeId, input_index: usize, mut document_node: DocumentNode, offset: IVec2) -> Option<NodeId> {
let post_node = self.network.nodes.get_mut(&node_id)?;
post_node.inputs[input_index] = NodeInput::node(new_id, 0);
document_node.metadata.position = post_node.metadata.position + offset;
self.network.nodes.insert(new_id, document_node);
Some(new_id)
}
pub fn create_layer(&mut self, new_id: NodeId, output_node_id: NodeId) -> Option<NodeId> {
let mut current_node = output_node_id;
let mut input_index = 0;
let mut current_input = &self.network.nodes.get(&current_node)?.inputs[input_index];
while let NodeInput::Node { node_id, output_index, .. } = current_input {
let mut sibling_node = &self.network.nodes.get(node_id)?;
if sibling_node.name == "Layer" {
current_node = *node_id;
input_index = 7;
current_input = &self.network.nodes.get(&current_node)?.inputs[input_index];
} else {
// Insert a layer node between the output and the new
let layer_node = resolve_document_node_type("Layer").expect("Layer node");
let node = layer_node.to_document_node_default_inputs([], DocumentNodeMetadata::default());
let node_id = self.insert_between(NodeOutput::new(*node_id, *output_index), NodeOutput::new(current_node, input_index), node, 0, 0)?;
current_node = node_id;
input_index = 7;
current_input = &self.network.nodes.get(&current_node)?.inputs[input_index];
}
}
let layer_node = resolve_document_node_type("Layer").expect("Node").to_document_node_default_inputs([], Default::default());
let layer_node = self.insert_node_before(new_id, current_node, input_index, layer_node, IVec2::new(0, 3))?;
Some(layer_node)
}
fn insert_artboard(&mut self, artboard: Artboard, layer: NodeId) -> Option<NodeId> {
let artboard_node = resolve_document_node_type("Artboard").expect("Node").to_document_node_default_inputs(
[
None,
Some(NodeInput::value(TaggedValue::IVec2(artboard.location), false)),
Some(NodeInput::value(TaggedValue::IVec2(artboard.dimensions), false)),
Some(NodeInput::value(TaggedValue::Color(artboard.background), false)),
Some(NodeInput::value(TaggedValue::Bool(artboard.clip), false)),
],
Default::default(),
);
self.insert_node_before(generate_uuid(), layer, 0, artboard_node, IVec2::new(-8, 0))
}
fn shift_upstream(&mut self, node_id: NodeId, shift: IVec2) {
let mut shift_nodes = HashSet::new();
let mut stack = vec![node_id];
while let Some(node) = stack.pop() {
let Some(node) = self.network.nodes.get(&node_id) else { continue };
for input in &node.inputs {
let NodeInput::Node { node_id, .. } = input else { continue };
if shift_nodes.insert(*node_id) {
stack.push(*node_id);
}
}
}
for node_id in shift_nodes {
if let Some(node) = self.network.nodes.get_mut(&node_id) {
node.metadata.position += shift;
}
}
}
/// Inserts a new node and modifies the inputs
fn modify_new_node(&mut self, name: &'static str, update_input: impl FnOnce(&mut Vec<NodeInput>)) {
let output_node_id = self.network.outputs[0].node_id;
let output_node_id = self.layer_node.unwrap_or(self.network.outputs[0].node_id);
let Some(output_node) = self.network.nodes.get_mut(&output_node_id) else {
warn!("Output node doesn't exist");
return;
@ -65,7 +191,7 @@ impl<'a> ModifyInputsContext<'a> {
/// Changes the inputs of a specific node
fn modify_inputs(&mut self, name: &'static str, skip_rerender: bool, update_input: impl FnOnce(&mut Vec<NodeInput>)) {
let existing_node_id = self.network.primary_flow().find(|(node, _)| node.name == name).map(|(_, id)| id);
let existing_node_id = self.network.primary_flow_from_opt(self.layer_node).find(|(node, _)| node.name == name).map(|(_, id)| id);
if let Some(node_id) = existing_node_id {
self.modify_existing_node_inputs(node_id, update_input);
} else {
@ -229,6 +355,36 @@ impl<'a> ModifyInputsContext<'a> {
inputs[2] = NodeInput::value(TaggedValue::BrushStrokes(strokes), false);
});
}
fn resize_artboard(&mut self, location: IVec2, dimensions: IVec2) {
self.modify_inputs("Artboard", false, |inputs| {
inputs[1] = NodeInput::value(TaggedValue::IVec2(location), false);
inputs[2] = NodeInput::value(TaggedValue::IVec2(dimensions), false);
});
}
fn delete_layer(&mut self, id: NodeId) {
let mut new_input = None;
let post_node = self.outwards_links.get(&id).and_then(|links| links.first().copied());
let mut delete_nodes = vec![id];
for (node, id) in self.network.primary_flow_from_opt(Some(id)) {
delete_nodes.push(id);
if node.name == "Artboard" {
new_input = Some(node.inputs[0].clone());
break;
}
}
for node_id in delete_nodes {
self.network.nodes.remove(&node_id);
}
if let (Some(new_input), Some(post_node)) = (new_input, post_node) {
if let Some(node) = self.network.nodes.get_mut(&post_node) {
node.inputs[0] = new_input;
}
}
}
}
impl MessageHandler<GraphOperationMessage, (&mut Document, &mut NodeGraphMessageHandler)> for GraphOperationMessageHandler {
@ -316,6 +472,31 @@ impl MessageHandler<GraphOperationMessage, (&mut Document, &mut NodeGraphMessage
modify_inputs.brush_modify(strokes);
}
}
GraphOperationMessage::NewArtboard { id, artboard } => {
let mut modify_inputs = ModifyInputsContext::new_doc(document, node_graph, responses);
if let Some(layer) = modify_inputs.create_layer(id, modify_inputs.network.outputs[0].node_id) {
modify_inputs.insert_artboard(artboard, layer);
}
//modify_inputs.brush_modify(strokes);
}
GraphOperationMessage::ResizeArtboard { id, location, dimensions } => {
let mut modify_inputs = ModifyInputsContext::new_doc(document, node_graph, responses);
if let Some(layer) = modify_inputs.locate_layer(id) {
modify_inputs.resize_artboard(location, dimensions);
}
}
GraphOperationMessage::DeleteArtboard { id } => {
let mut modify_inputs = ModifyInputsContext::new_doc(document, node_graph, responses);
modify_inputs.delete_layer(id);
}
GraphOperationMessage::ClearArtboards => {
let mut modify_inputs = ModifyInputsContext::new_doc(document, node_graph, responses);
let artboard_nodes = modify_inputs.network.nodes.iter().filter(|(_, node)| node.name == "Artboard").map(|(id, _)| *id).collect::<Vec<_>>();
for id in artboard_nodes {
modify_inputs.delete_layer(id);
}
}
}
}

View File

@ -156,7 +156,7 @@ impl NodeGraphMessageHandler {
self.get_root_network_mut(document).nested_network_mut(&self.nested_path)
}
/// Send the cached layout for the bar at the top of the node panel to the frontend
/// Send the cached layout to the frontend for the options bar at the top of the node panel
fn send_node_bar_layout(&self, responses: &mut VecDeque<Message>) {
responses.add(LayoutMessage::SendLayout {
layout: Layout::WidgetLayout(WidgetLayout::new(self.widgets.to_vec())),
@ -165,34 +165,48 @@ impl NodeGraphMessageHandler {
}
/// Collect the addresses of the currently viewed nested node e.g. Root -> MyFunFilter -> Exposure
fn collect_nested_addresses(&mut self, document: &Document, responses: &mut VecDeque<Message>) {
// // Build path list
let mut path = vec![];
if let Some(layer) = self.layer_path.as_ref().and_then(|path| document.layer(path).ok()) {
path.push(format!("Layer: {}", layer.name.as_deref().unwrap_or("Untitled Layer")));
} else {
path.push("Document Network".to_string());
}
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(self.get_root_network(document));
for node_id in &self.nested_path {
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![BreadcrumbTrailButtons::new(path.clone())
.on_update(move |input: &u64| {
NodeGraphMessage::ExitNestedNetwork {
depth_of_nesting: nesting - (*input as usize) - 1,
}
.into()
})
.widget_holder()],
widgets: vec![
IconLabel::new(icon).tooltip(tooltip).widget_holder(),
WidgetHolder::unrelated_separator(),
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);
@ -425,9 +439,9 @@ impl NodeGraphMessageHandler {
}
}
impl MessageHandler<NodeGraphMessage, (&mut Document, &NodeGraphExecutor, u64)> for NodeGraphMessageHandler {
impl MessageHandler<NodeGraphMessage, (&mut Document, &NodeGraphExecutor, u64, &str)> for NodeGraphMessageHandler {
#[remain::check]
fn process_message(&mut self, message: NodeGraphMessage, responses: &mut VecDeque<Message>, (document, executor, document_id): (&mut Document, &NodeGraphExecutor, u64)) {
fn process_message(&mut self, message: NodeGraphMessage, responses: &mut VecDeque<Message>, (document, executor, document_id, document_name): (&mut Document, &NodeGraphExecutor, u64, &str)) {
#[remain::sorted]
match message {
NodeGraphMessage::CloseNodeGraph => {
@ -564,7 +578,7 @@ impl MessageHandler<NodeGraphMessage, (&mut Document, &NodeGraphExecutor, u64)>
if let Some(network) = self.get_active_network(document) {
Self::send_graph(network, executor, &self.layer_path, responses);
}
self.collect_nested_addresses(document, responses);
self.collect_nested_addresses(document, document_name, responses);
self.update_selected(document, responses);
}
NodeGraphMessage::DuplicateSelectedNodes => {
@ -600,7 +614,7 @@ impl MessageHandler<NodeGraphMessage, (&mut Document, &NodeGraphExecutor, u64)>
if let Some(network) = self.get_active_network(document) {
Self::send_graph(network, executor, &self.layer_path, responses);
}
self.collect_nested_addresses(document, responses);
self.collect_nested_addresses(document, document_name, responses);
self.update_selected(document, responses);
}
NodeGraphMessage::ExposeInput { node_id, input_index, new_exposed } => {
@ -662,7 +676,7 @@ impl MessageHandler<NodeGraphMessage, (&mut Document, &NodeGraphExecutor, u64)>
let node_types = document_node_types::collect_node_types();
responses.add(FrontendMessage::UpdateNodeTypes { node_types });
}
self.collect_nested_addresses(document, responses);
self.collect_nested_addresses(document, document_name, responses);
self.update_selected(document, responses);
}
NodeGraphMessage::PasteNodes { serialized_nodes } => {
@ -895,7 +909,7 @@ impl MessageHandler<NodeGraphMessage, (&mut Document, &NodeGraphExecutor, u64)>
let node_types = document_node_types::collect_node_types();
responses.add(FrontendMessage::UpdateNodeTypes { node_types });
}
self.collect_nested_addresses(document, responses);
self.collect_nested_addresses(document, document_name, responses);
self.update_selected(document, responses);
}
}

View File

@ -204,12 +204,13 @@ fn static_nodes() -> Vec<DocumentNodeType> {
DocumentNodeType {
name: "Artboard",
category: "General",
identifier: NodeImplementation::proto("graphene_core::ConstructArtboardNode<_, _, _>"),
identifier: NodeImplementation::proto("graphene_core::ConstructArtboardNode<_, _, _, _>"),
inputs: vec![
DocumentInputType::value("Graphic Group", TaggedValue::GraphicGroup(GraphicGroup::EMPTY), true),
DocumentInputType::value("Location", TaggedValue::IVec2(glam::IVec2::ZERO), false),
DocumentInputType::value("Dimensions", TaggedValue::IVec2(glam::IVec2::new(1920, 1080)), false),
DocumentInputType::value("Background", TaggedValue::Color(Color::WHITE), false),
DocumentInputType::value("Clip", TaggedValue::Bool(false), false),
],
outputs: vec![DocumentOutputType::new("Out", FrontendGraphDataType::Artboard)],
properties: node_properties::artboard_properties,

View File

@ -1623,5 +1623,8 @@ pub fn artboard_properties(document_node: &DocumentNode, node_id: NodeId, _conte
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 background = color_widget(document_node, node_id, 3, "Background", ColorInput::default().allow_none(false), true);
vec![location, dimensions, background]
let clip = LayoutGroup::Row {
widgets: bool_widget(document_node, node_id, 4, "Clip", true),
};
vec![location, dimensions, background, clip]
}

View File

@ -24,6 +24,7 @@ impl<'a> MessageHandler<PropertiesPanelMessage, (&PersistentData, PropertiesPane
use PropertiesPanelMessage::*;
let PropertiesPanelMessageHandlerData {
document_name,
artwork_document,
artboard_document,
selected_layers,
@ -63,6 +64,9 @@ impl<'a> MessageHandler<PropertiesPanelMessage, (&PersistentData, PropertiesPane
}
}
ClearSelection => {
// This causes the Properties panel to change, so this needs to happen before the following lines clear the Properties panel
responses.add(NodeGraphMessage::CloseNodeGraph);
responses.add(LayoutMessage::SendLayout {
layout: Layout::WidgetLayout(WidgetLayout::new(vec![])),
layout_target: LayoutTarget::PropertiesOptions,
@ -71,7 +75,6 @@ impl<'a> MessageHandler<PropertiesPanelMessage, (&PersistentData, PropertiesPane
layout: Layout::WidgetLayout(WidgetLayout::new(vec![])),
layout_target: LayoutTarget::PropertiesSections,
});
responses.add(NodeGraphMessage::CloseNodeGraph);
self.active_selection = None;
}
Deactivate => responses.add(BroadcastMessage::UnsubscribeEvent {
@ -151,7 +154,7 @@ impl<'a> MessageHandler<PropertiesPanelMessage, (&PersistentData, PropertiesPane
executor,
network: &artwork_document.document_network,
};
register_document_graph_properties(context, node_graph_message_handler);
register_document_graph_properties(context, node_graph_message_handler, document_name);
}
}
UpdateSelectedDocumentProperties => responses.add(PropertiesPanelMessage::SetActiveLayers {

View File

@ -78,19 +78,14 @@ pub fn register_artboard_layer_properties(layer: &Layer, responses: &mut VecDequ
..Default::default()
})),
WidgetHolder::unrelated_separator(),
WidgetHolder::new(Widget::TextLabel(TextLabel {
value: "Artboard".into(),
..TextLabel::default()
})),
WidgetHolder::unrelated_separator(),
WidgetHolder::new(Widget::TextInput(TextInput {
value: layer.name.clone().unwrap_or_else(|| "Untitled".to_string()),
value: layer.name.clone().unwrap_or_else(|| "Untitled Artboard".to_string()),
on_update: WidgetCallback::new(|text_input: &TextInput| PropertiesPanelMessage::ModifyName { name: text_input.value.clone() }.into()),
..Default::default()
})),
WidgetHolder::related_separator(),
WidgetHolder::new(Widget::PopoverButton(PopoverButton {
header: "Options Bar".into(),
header: "Additional Options".into(),
text: "Coming soon".into(),
..Default::default()
})),
@ -261,22 +256,14 @@ pub fn register_artwork_layer_properties(
})),
},
WidgetHolder::unrelated_separator(),
WidgetHolder::new(Widget::TextLabel(TextLabel {
value: match &layer.data {
LayerDataType::Layer(_) => "Layer".into(),
other => LayerDataTypeDiscriminant::from(other).to_string(),
},
..TextLabel::default()
})),
WidgetHolder::unrelated_separator(),
WidgetHolder::new(Widget::TextInput(TextInput {
value: layer.name.clone().unwrap_or_else(|| "Untitled".to_string()),
value: layer.name.clone().unwrap_or_else(|| "Untitled Layer".to_string()),
on_update: WidgetCallback::new(|text_input: &TextInput| PropertiesPanelMessage::ModifyName { name: text_input.value.clone() }.into()),
..Default::default()
})),
WidgetHolder::related_separator(),
WidgetHolder::new(Widget::PopoverButton(PopoverButton {
header: "Options Bar".into(),
header: "Additional Options".into(),
text: "Coming soon".into(),
..Default::default()
})),
@ -326,18 +313,18 @@ pub fn register_artwork_layer_properties(
});
}
pub fn register_document_graph_properties(mut context: NodePropertiesContext, node_graph_message_handler: &NodeGraphMessageHandler) {
pub fn register_document_graph_properties(mut context: NodePropertiesContext, node_graph_message_handler: &NodeGraphMessageHandler, document_name: &str) {
let mut properties_sections = Vec::new();
node_graph_message_handler.collate_properties(&mut context, &mut properties_sections);
let options_bar = vec![LayoutGroup::Row {
widgets: vec![
IconLabel::new("File").widget_holder(),
IconLabel::new("File").tooltip("Document").widget_holder(),
WidgetHolder::unrelated_separator(),
TextLabel::new("Document graph").widget_holder(),
WidgetHolder::unrelated_separator(),
TextInput::new("No layer selected").disabled(true).widget_holder(),
TextInput::new(document_name)
.on_update(|text_input| DocumentMessage::RenameDocument { new_name: text_input.value.clone() }.into())
.widget_holder(),
WidgetHolder::related_separator(),
PopoverButton::new("Options Bar", "Coming soon").widget_holder(),
PopoverButton::new("Additional Options", "Coming soon").widget_holder(),
],
}];

View File

@ -6,6 +6,7 @@ use serde::{Deserialize, Serialize};
use crate::{messages::prelude::NodeGraphMessageHandler, node_graph_executor::NodeGraphExecutor};
pub struct PropertiesPanelMessageHandlerData<'a> {
pub document_name: &'a str,
pub artwork_document: &'a DocumentLegacy,
pub artboard_document: &'a DocumentLegacy,
pub selected_layers: &'a mut dyn Iterator<Item = &'a [LayerId]>,

View File

@ -1,5 +1,3 @@
use std::sync::Arc;
use super::utility_types::PersistentData;
use crate::application::generate_uuid;
use crate::consts::{DEFAULT_DOCUMENT_NAME, GRAPHITE_DOCUMENT_VERSION};
@ -19,6 +17,9 @@ use graph_craft::document::value::TaggedValue;
use graph_craft::document::{NodeId, NodeInput};
use graphene_core::text::Font;
use glam::DAffine2;
use std::sync::Arc;
#[derive(Debug, Default)]
pub struct PortfolioMessageHandler {
menu_bar_message_handler: MenuBarMessageHandler,
@ -114,12 +115,13 @@ impl MessageHandler<PortfolioMessage, (&InputPreprocessorMessageHandler, &Prefer
// Actually delete the document (delay to delete document is required to let the document and properties panel messages above get processed)
responses.add(PortfolioMessage::DeleteDocument { document_id });
responses.add(FrontendMessage::TriggerIndexedDbRemoveDocument { document_id });
// Send the new list of document tab names
responses.add(PortfolioMessage::UpdateOpenDocumentsList);
responses.add(FrontendMessage::TriggerIndexedDbRemoveDocument { document_id });
responses.add(DocumentMessage::RenderDocument);
responses.add(DocumentMessage::DocumentStructureChanged);
if let Some(document) = self.active_document() {
for layer in document.layer_metadata.keys() {
responses.add(DocumentMessage::LayerChanged { affected_layer_path: layer.clone() });
@ -246,7 +248,7 @@ impl MessageHandler<PortfolioMessage, (&InputPreprocessorMessageHandler, &Prefer
}
PortfolioMessage::ImaginatePreferences => self.executor.update_imaginate_preferences(preferences.get_imaginate_preferences()),
PortfolioMessage::ImaginateServerHostname => {
info!("setting imaginate persistent data");
debug!("setting imaginate persistent data");
self.persistent_data.imaginate.set_host_name(&preferences.imaginate_server_hostname);
}
PortfolioMessage::Import => {
@ -466,6 +468,7 @@ impl MessageHandler<PortfolioMessage, (&InputPreprocessorMessageHandler, &Prefer
responses.add(BroadcastEvent::DocumentIsDirty);
responses.add(PortfolioMessage::UpdateDocumentWidgets);
responses.add(NavigationMessage::TranslateCanvas { delta: (0., 0.).into() });
responses.add(NodeGraphMessage::RunDocumentGraph);
}
PortfolioMessage::SetActiveDocument { document_id } => {
self.active_document_id = Some(document_id);
@ -664,7 +667,8 @@ impl PortfolioMessageHandler {
}
pub fn poll_node_graph_evaluation(&mut self, responses: &mut VecDeque<Message>) {
self.executor.poll_node_graph_evaluation(responses).unwrap_or_else(|e| {
let transform = self.active_document().map(|document| document.document_legacy.root.transform).unwrap_or(DAffine2::IDENTITY);
self.executor.poll_node_graph_evaluation(transform, responses).unwrap_or_else(|e| {
log::error!("Error while evaluating node graph: {}", e);
});
}

View File

@ -13,7 +13,7 @@ use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo};
use document_legacy::intersection::Quad;
use document_legacy::LayerId;
use glam::{DVec2, Vec2Swizzles};
use glam::{DVec2, IVec2, Vec2Swizzles};
use serde::{Deserialize, Serialize};
#[derive(Default)]
@ -133,10 +133,6 @@ impl Fsm for ArtboardToolFsmState {
tool_data.bounding_box_overlays = Some(bounding_box_overlays);
responses.add(OverlaysMessage::Rerender);
responses.add(PropertiesPanelMessage::SetActiveLayers {
paths: vec![vec![tool_data.selected_artboard.unwrap()]],
document: TargetDocument::Artboard,
});
}
_ => {}
};
@ -196,11 +192,6 @@ impl Fsm for ArtboardToolFsmState {
.start_snap(document, input, document.bounding_boxes(None, Some(intersection[0]), render_data), true, true);
tool_data.snap_manager.add_all_document_handles(document, input, &[], &[], &[]);
responses.add(PropertiesPanelMessage::SetActiveLayers {
paths: vec![intersection.clone()],
document: TargetDocument::Artboard,
});
ArtboardToolFsmState::Dragging
} else {
tool_data.selected_artboard = None;
@ -226,6 +217,11 @@ impl Fsm for ArtboardToolFsmState {
position: position.round().into(),
size: size.round().into(),
});
responses.add(GraphOperationMessage::ResizeArtboard {
id: tool_data.selected_artboard.unwrap(),
location: position.round().as_ivec2(),
dimensions: size.round().as_ivec2(),
});
responses.add(BroadcastEvent::DocumentIsDirty);
}
@ -251,6 +247,11 @@ impl Fsm for ArtboardToolFsmState {
position: position.round().into(),
size: size.round().into(),
});
responses.add(GraphOperationMessage::ResizeArtboard {
id: tool_data.selected_artboard.unwrap(),
location: position.round().as_ivec2(),
dimensions: size.round().as_ivec2(),
});
responses.add(BroadcastEvent::DocumentIsDirty);
@ -285,6 +286,11 @@ impl Fsm for ArtboardToolFsmState {
position: start.round().into(),
size: size.round().into(),
});
responses.add(GraphOperationMessage::ResizeArtboard {
id: tool_data.selected_artboard.unwrap(),
location: start.round().as_ivec2(),
dimensions: size.round().as_ivec2(),
});
} else {
let id = generate_uuid();
tool_data.selected_artboard = Some(id);
@ -297,14 +303,20 @@ impl Fsm for ArtboardToolFsmState {
position: start.round().into(),
size: (1., 1.),
});
responses.add(GraphOperationMessage::NewArtboard {
id,
artboard: graphene_core::Artboard {
graphic_group: graphene_core::GraphicGroup::EMPTY,
location: start.round().as_ivec2(),
dimensions: IVec2::splat(1),
background: graphene_core::Color::WHITE,
clip: false,
},
})
}
// Have to put message here instead of when Artboard is created
// This might result in a few more calls but it is not reliant on the order of messages
responses.add(PropertiesPanelMessage::SetActiveLayers {
paths: vec![vec![tool_data.selected_artboard.unwrap()]],
document: TargetDocument::Artboard,
});
responses.add(BroadcastEvent::DocumentIsDirty);
@ -352,6 +364,8 @@ impl Fsm for ArtboardToolFsmState {
(_, ArtboardToolMessage::DeleteSelected) => {
if let Some(artboard) = tool_data.selected_artboard.take() {
responses.add(ArtboardMessage::DeleteArtboard { artboard });
responses.add(GraphOperationMessage::DeleteArtboard { id: artboard });
responses.add(BroadcastEvent::DocumentIsDirty);
}
ArtboardToolFsmState::Ready
@ -363,6 +377,11 @@ impl Fsm for ArtboardToolFsmState {
position: (bounds.bounds[0].x + delta_x, bounds.bounds[0].y + delta_y),
size: (bounds.bounds[1] - bounds.bounds[0]).round().into(),
});
responses.add(GraphOperationMessage::ResizeArtboard {
id: tool_data.selected_artboard.unwrap(),
location: DVec2::new(bounds.bounds[0].x + delta_x, bounds.bounds[0].y + delta_y).round().as_ivec2(),
dimensions: (bounds.bounds[1] - bounds.bounds[0]).round().as_ivec2(),
});
}
ArtboardToolFsmState::Ready

View File

@ -214,7 +214,7 @@ impl NodeRuntime {
graphic_group.render_svg(&mut render, &render_params);
let [min, max] = bounds.unwrap_or_default();
render.format_svg(min, max);
info!("SVG {}", render.svg);
debug!("SVG {}", render.svg);
if let (Some(layer_id), Some(node_id)) = (layer_path.last().copied(), node_path.get(node_path.len() - 2).copied()) {
let old_thumbnail = self.thumbnails.entry(layer_id).or_default().entry(node_id).or_default();
@ -283,16 +283,16 @@ struct ExecutionContext {
impl Default for NodeGraphExecutor {
fn default() -> Self {
let (request_sender, request_reciever) = std::sync::mpsc::channel();
let (response_sender, response_reciever) = std::sync::mpsc::channel();
let (request_sender, request_receiver) = std::sync::mpsc::channel();
let (response_sender, response_receiver) = std::sync::mpsc::channel();
NODE_RUNTIME.with(|runtime| {
runtime.borrow_mut().replace(NodeRuntime::new(request_reciever, response_sender));
runtime.borrow_mut().replace(NodeRuntime::new(request_receiver, response_sender));
});
Self {
futures: Default::default(),
sender: request_sender,
receiver: response_reciever,
receiver: response_receiver,
last_output_type: Default::default(),
thumbnails: Default::default(),
}
@ -423,7 +423,7 @@ impl NodeGraphExecutor {
Ok(())
}
pub fn poll_node_graph_evaluation(&mut self, responses: &mut VecDeque<Message>) -> Result<(), String> {
pub fn poll_node_graph_evaluation(&mut self, transform: DAffine2, responses: &mut VecDeque<Message>) -> Result<(), String> {
let results = self.receiver.try_iter().collect::<Vec<_>>();
for response in results {
match response {
@ -437,7 +437,7 @@ impl NodeGraphExecutor {
let node_graph_output = result.map_err(|e| format!("Node graph evaluation failed: {:?}", e))?;
let execution_context = self.futures.remove(&generation_id).ok_or_else(|| "Invalid generation ID".to_string())?;
responses.extend(updates);
self.process_node_graph_output(node_graph_output, execution_context.layer_path.clone(), responses, execution_context.document_id)?;
self.process_node_graph_output(node_graph_output, execution_context.layer_path.clone(), transform, responses, execution_context.document_id)?;
responses.add(DocumentMessage::LayerChanged {
affected_layer_path: execution_context.layer_path,
});
@ -456,7 +456,7 @@ impl NodeGraphExecutor {
Ok(())
}
fn process_node_graph_output(&mut self, node_graph_output: TaggedValue, layer_path: Vec<LayerId>, responses: &mut VecDeque<Message>, document_id: u64) -> Result<(), String> {
fn process_node_graph_output(&mut self, node_graph_output: TaggedValue, layer_path: Vec<LayerId>, transform: DAffine2, responses: &mut VecDeque<Message>, document_id: u64) -> Result<(), String> {
self.last_output_type.insert(layer_path.clone(), Some(node_graph_output.ty()));
match node_graph_output {
TaggedValue::VectorData(vector_data) => {
@ -489,11 +489,30 @@ impl NodeGraphExecutor {
}
}
TaggedValue::Artboard(artboard) => {
info!("{artboard:#?}");
debug!("{artboard:#?}");
return Err("Artboard (see console)".to_string());
}
TaggedValue::GraphicGroup(graphic_group) => {
info!("{graphic_group:#?}");
debug!("{graphic_group:#?}");
use graphene_core::renderer::{format_transform_matrix, GraphicElementRendered, RenderParams, SvgRender};
// Setup rendering
let mut render = SvgRender::new();
let render_params = RenderParams::new(ViewMode::Normal, None, false);
// Render svg
graphic_group.render_svg(&mut render, &render_params);
// Conctenate the defs and the svg into one string
let mut svg = "<defs>".to_string();
svg.push_str(&render.svg_defs);
svg.push_str("</defs>");
use std::fmt::Write;
write!(svg, "{}", render.svg).unwrap();
// Send to frontend
responses.add(FrontendMessage::UpdateDocumentNodeRender { svg });
return Err("Graphic group (see console)".to_string());
}
_ => {

View File

@ -18,6 +18,8 @@
UpdateDocumentScrollbars,
UpdateEyedropperSamplingState,
UpdateMouseCursor,
UpdateDocumentNodeRender,
UpdateDocumentTransform,
} from "@graphite/wasm-communication/messages";
import EyedropperPreview, { ZOOM_WINDOW_DIMENSIONS } from "@graphite/components/floating-menus/EyedropperPreview.svelte";
@ -58,8 +60,10 @@
// Rendered SVG viewport data
let artworkSvg = "";
let nodeRenderSvg = "";
let artboardSvg = "";
let overlaysSvg = "";
let artworkTransform = "";
// Rasterized SVG viewport data, or none if it's not up-to-date
let rasterizedCanvas: HTMLCanvasElement | undefined = undefined;
@ -140,12 +144,21 @@
export function updateDocumentOverlays(svg: string) {
overlaysSvg = svg;
}
export function updateDocumentArtboards(svg: string) {
artboardSvg = svg;
rasterizedCanvas = undefined;
}
export function updateDocumentNodeRender(svg: string) {
nodeRenderSvg = svg;
rasterizedCanvas = undefined;
}
export function updateDocumentTransform(transform: string) {
artworkTransform = transform;
}
export async function updateEyedropperSamplingState(mousePosition: XY | undefined, colorPrimary: string, colorSecondary: string): Promise<[number, number, number] | undefined> {
if (mousePosition === undefined) {
cursorEyedropper = false;
@ -335,6 +348,16 @@
updateDocumentArtboards(data.svg);
});
editor.subscriptions.subscribeJsMessage(UpdateDocumentNodeRender, async (data) => {
await tick();
updateDocumentNodeRender(data.svg);
});
editor.subscriptions.subscribeJsMessage(UpdateDocumentTransform, async (data) => {
await tick();
updateDocumentTransform(data.transform);
});
editor.subscriptions.subscribeJsMessage(UpdateEyedropperSamplingState, async (data) => {
await tick();
@ -445,6 +468,11 @@
<svg class="artboards" style:width={canvasWidthCSS} style:height={canvasHeightCSS}>
{@html artboardSvg}
</svg>
<svg class="artboards" style:width={canvasWidthCSS} style:height={canvasHeightCSS}>
<g id="transform-group" transform={artworkTransform}>
{@html nodeRenderSvg}
</g>
</svg>
<svg class="artwork" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style:width={canvasWidthCSS} style:height={canvasHeightCSS}>
{@html artworkSvg}
</svg>

View File

@ -445,6 +445,14 @@ export class UpdateDocumentArtboards extends JsMessage {
readonly svg!: string;
}
export class UpdateDocumentNodeRender extends JsMessage {
readonly svg!: string;
}
export class UpdateDocumentTransform extends JsMessage {
readonly transform!: string;
}
export class UpdateDocumentScrollbars extends JsMessage {
@TupleToVec2
readonly position!: XY;
@ -1351,6 +1359,7 @@ export const messageMakers: Record<string, MessageMaker> = {
UpdateActiveDocument,
UpdateDialogDetails,
UpdateDocumentArtboards,
UpdateDocumentNodeRender,
UpdateDocumentArtwork,
UpdateDocumentBarLayout,
UpdateDocumentLayerDetails,
@ -1359,6 +1368,7 @@ export const messageMakers: Record<string, MessageMaker> = {
UpdateDocumentOverlays,
UpdateDocumentRulers,
UpdateDocumentScrollbars,
UpdateDocumentTransform,
UpdateEyedropperSamplingState,
UpdateImageData,
UpdateInputHints,

View File

@ -49,6 +49,19 @@ pub struct Artboard {
pub location: IVec2,
pub dimensions: IVec2,
pub background: Color,
pub clip: bool,
}
impl Artboard {
pub fn new(location: IVec2, dimensions: IVec2) -> Self {
Self {
graphic_group: GraphicGroup::EMPTY,
location: location.min(location + dimensions),
dimensions: dimensions.abs(),
background: Color::WHITE,
clip: false,
}
}
}
pub struct ConstructLayerNode<Name, BlendMode, Opacity, Visible, Locked, Collapsed, Stack> {
@ -84,19 +97,21 @@ fn construct_layer<Data: Into<GraphicElementData>>(
stack
}
pub struct ConstructArtboardNode<Location, Dimensions, Background> {
pub struct ConstructArtboardNode<Location, Dimensions, Background, Clip> {
location: Location,
dimensions: Dimensions,
background: Background,
clip: Clip,
}
#[node_fn(ConstructArtboardNode)]
fn construct_artboard(graphic_group: GraphicGroup, location: IVec2, dimensions: IVec2, background: Color) -> Artboard {
fn construct_artboard(graphic_group: GraphicGroup, location: IVec2, dimensions: IVec2, background: Color, clip: bool) -> Artboard {
Artboard {
graphic_group,
location,
dimensions,
location: location.min(location + dimensions),
dimensions: dimensions.abs(),
background,
clip,
}
}

View File

@ -12,6 +12,7 @@ pub struct SvgRender {
pub svg_defs: String,
pub transform: DAffine2,
pub image_data: Vec<(u64, Image<Color>)>,
indent: usize,
}
impl SvgRender {
@ -21,9 +22,15 @@ impl SvgRender {
svg_defs: String::new(),
transform: DAffine2::IDENTITY,
image_data: Vec::new(),
indent: 0,
}
}
pub fn indent(&mut self) {
self.svg.push("\n");
self.svg.push("\t".repeat(self.indent));
}
/// Add an outer `<svg />` tag with a `viewBox` and the `<defs />`
pub fn format_svg(&mut self, bounds_min: DVec2, bounds_max: DVec2) {
let (x, y) = bounds_min.into();
@ -31,7 +38,38 @@ impl SvgRender {
let defs = &self.svg_defs;
let svg_header = format!(r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="{x} {y} {size_x} {size_y}"><defs>{defs}</defs>"#,);
self.svg.insert(0, svg_header.into());
self.svg.push("</svg>".into());
self.svg.push("</svg>");
}
pub fn leaf_tag(&mut self, name: impl Into<SvgSegment>, attributes: impl FnOnce(&mut SvgRenderAttrs)) {
self.indent();
self.svg.push("<");
self.svg.push(name);
attributes(&mut SvgRenderAttrs(self));
self.svg.push("/>");
}
pub fn parent_tag(&mut self, name: impl Into<SvgSegment>, attributes: impl FnOnce(&mut SvgRenderAttrs), inner: impl FnOnce(&mut Self)) {
let name = name.into();
self.indent();
self.svg.push("<");
self.svg.push(name.clone());
attributes(&mut SvgRenderAttrs(self));
self.svg.push(">");
let length = self.svg.len();
self.indent += 1;
inner(self);
self.indent -= 1;
if self.svg.len() != length {
self.indent();
self.svg.push("</");
self.svg.push(name);
self.svg.push(">");
} else {
self.svg.pop();
self.svg.push("/>");
}
}
}
@ -54,8 +92,18 @@ impl RenderParams {
}
}
fn format_transform_matrix(transform: DAffine2) -> String {
transform.to_cols_array().iter().map(ToString::to_string).collect::<Vec<_>>().join(",")
pub fn format_transform_matrix(transform: DAffine2) -> String {
use std::fmt::Write;
let mut result = "matrix(".to_string();
let cols = transform.to_cols_array();
for (index, item) in cols.iter().enumerate() {
write!(result, "{}", item).unwrap();
if index != cols.len() - 1 {
result.push_str(", ");
}
}
result.push(')');
result
}
pub trait GraphicElementRendered {
@ -77,17 +125,17 @@ impl GraphicElementRendered for VectorData {
let layer_bounds = self.bounding_box().unwrap_or_default();
let transformed_bounds = self.bounding_box_with_transform(render.transform).unwrap_or_default();
render.svg.push("<path d=\"".into());
let mut path = String::new();
for subpath in &self.subpaths {
let _ = subpath.subpath_to_svg(&mut path, self.transform * render.transform);
}
render.svg.push(path.into());
render.svg.push("\"".into());
let style = self.style.render(render_params.view_mode, &mut render.svg_defs, render.transform, layer_bounds, transformed_bounds);
render.svg.push(style.into());
render.svg.push("/>".into());
render.leaf_tag("path", |attributes| {
attributes.push("class", "vector-data");
attributes.push("d", path);
let render = &mut attributes.0;
let style = self.style.render(render_params.view_mode, &mut render.svg_defs, render.transform, layer_bounds, transformed_bounds);
attributes.push_val(style);
});
}
fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]> {
self.bounding_box_with_transform(self.transform * transform)
@ -96,7 +144,54 @@ impl GraphicElementRendered for VectorData {
impl GraphicElementRendered for Artboard {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
self.graphic_group.render_svg(render, render_params)
// Background
render.leaf_tag("rect", |attributes| {
attributes.push("class", "artboard-bg");
attributes.push("fill", format!("#{}", self.background.rgba_hex()));
attributes.push("x", self.location.x.min(self.location.x + self.dimensions.x).to_string());
attributes.push("y", self.location.y.min(self.location.y + self.dimensions.y).to_string());
attributes.push("width", self.dimensions.x.abs().to_string());
attributes.push("height", self.dimensions.y.abs().to_string());
});
// Label
render.parent_tag(
"text",
|attributes| {
attributes.push("class", "artboard-label");
attributes.push("fill", "white");
attributes.push("x", (self.location.x.min(self.location.x + self.dimensions.x)).to_string());
attributes.push("y", (self.location.y.min(self.location.y + self.dimensions.y) - 4).to_string());
attributes.push("font-size", "14px");
},
|render| {
render.svg.push("Artboard");
},
);
// Contents group
render.parent_tag(
"g",
|attributes| {
attributes.push("class", "artboard");
if self.clip {
let id = format!("artboard-{}", generate_uuid());
let selector = format!("url(#{id})");
use std::fmt::Write;
write!(
&mut attributes.0.svg_defs,
r##"<clipPath id="{id}"><rect x="{}" y="{}" width="{}" height="{}"/></clipPath>"##,
self.location.x, self.location.y, self.dimensions.x, self.dimensions.y
)
.unwrap();
attributes.push("clip-path", selector);
}
},
|render| {
// Contents
self.graphic_group.render_svg(render, render_params);
},
);
}
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();
@ -107,12 +202,14 @@ impl GraphicElementRendered for Artboard {
impl GraphicElementRendered for ImageFrame<Color> {
fn render_svg(&self, render: &mut SvgRender, _render_params: &RenderParams) {
let transform: String = format_transform_matrix(self.transform * render.transform);
render
.svg
.push(format!(r#"<image width="1" height="1" preserveAspectRatio="none" transform="matrix({transform})" href=""#).into());
let uuid = generate_uuid();
render.svg.push(SvgSegment::BlobUrl(uuid));
render.svg.push("\" />".into());
render.leaf_tag("image", |attributes| {
attributes.push("width", 1.to_string());
attributes.push("height", 1.to_string());
attributes.push("preserveAspectRatio", "none");
attributes.push("transform", transform);
attributes.push("href", SvgSegment::BlobUrl(uuid))
});
render.image_data.push((uuid, self.image.clone()))
}
fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]> {
@ -193,3 +290,27 @@ impl core::fmt::Display for SvgSegmentList {
Ok(())
}
}
pub struct SvgRenderAttrs<'a>(&'a mut SvgRender);
impl<'a> SvgRenderAttrs<'a> {
pub fn push_complex(&mut self, name: impl Into<SvgSegment>, value: impl FnOnce(&mut SvgRender)) {
self.0.svg.push(" ");
self.0.svg.push(name);
self.0.svg.push("=\"");
value(self.0);
self.0.svg.push("\"");
}
pub fn push(&mut self, name: impl Into<SvgSegment>, value: impl Into<SvgSegment>) {
self.push_complex(name, move |renderer| renderer.svg.push(value));
}
pub fn push_val(&mut self, value: impl Into<SvgSegment>) {
self.0.svg.push(value);
}
}
impl SvgSegmentList {
pub fn push(&mut self, value: impl Into<SvgSegment>) {
self.0.push(value.into());
}
}

View File

@ -496,34 +496,19 @@ impl NodeNetwork {
///
/// Used for the properties panel and tools.
pub fn primary_flow(&self) -> impl Iterator<Item = (&DocumentNode, u64)> {
struct FlowIter<'a> {
stack: Vec<NodeId>,
network: &'a NodeNetwork,
}
impl<'a> Iterator for FlowIter<'a> {
type Item = (&'a DocumentNode, NodeId);
fn next(&mut self) -> Option<Self::Item> {
loop {
let node_id = self.stack.pop()?;
if let Some(document_node) = self.network.nodes.get(&node_id) {
self.stack.extend(
document_node
.inputs
.iter()
.take(1) // Only show the primary input
.filter_map(|input| if let NodeInput::Node { node_id: ref_id, .. } = input { Some(*ref_id) } else { None }),
);
return Some((document_node, node_id));
};
}
}
}
FlowIter {
stack: self.outputs.iter().map(|output| output.node_id).collect(),
network: self,
}
}
pub fn primary_flow_from_opt(&self, id: Option<NodeId>) -> impl Iterator<Item = (&DocumentNode, u64)> {
FlowIter {
stack: id.map_or_else(|| self.outputs.iter().map(|output| output.node_id).collect(), |id| vec![id]),
network: self,
}
}
pub fn is_acyclic(&self) -> bool {
let mut dependencies: HashMap<u64, Vec<u64>> = HashMap::new();
for (node_id, node) in &self.nodes {
@ -549,6 +534,29 @@ impl NodeNetwork {
}
}
struct FlowIter<'a> {
stack: Vec<NodeId>,
network: &'a NodeNetwork,
}
impl<'a> Iterator for FlowIter<'a> {
type Item = (&'a DocumentNode, NodeId);
fn next(&mut self) -> Option<Self::Item> {
loop {
let node_id = self.stack.pop()?;
if let Some(document_node) = self.network.nodes.get(&node_id) {
self.stack.extend(
document_node
.inputs
.iter()
.take(1) // Only show the primary input
.filter_map(|input| if let NodeInput::Node { node_id: ref_id, .. } = input { Some(*ref_id) } else { None }),
);
return Some((document_node, node_id));
};
}
}
}
/// Functions for compiling the network
impl NodeNetwork {
pub fn map_ids(&mut self, f: impl Fn(NodeId) -> NodeId + Copy) {
@ -835,7 +843,7 @@ impl NodeNetwork {
self.nodes.retain(|_, node| !matches!(node.implementation, DocumentNodeImplementation::Extract));
for (_, node) in &mut extraction_nodes {
log::info!("extraction network: {:#?}", &self);
log::debug!("extraction network: {:#?}", &self);
if let DocumentNodeImplementation::Extract = node.implementation {
assert_eq!(node.inputs.len(), 1);
log::debug!("Resolving extract node {:?}", node);

View File

@ -169,7 +169,7 @@ impl ApplicationIo for WasmApplicationIo {
fn load_resource(&self, url: impl AsRef<str>) -> Result<ResourceFuture, ApplicationError> {
let url = url::Url::parse(url.as_ref()).map_err(|_| ApplicationError::InvalidUrl)?;
log::info!("Loading resource: {:?}", url);
log::trace!("Loading resource: {:?}", url);
match url.scheme() {
#[cfg(feature = "tokio")]
"file" => {
@ -196,7 +196,7 @@ impl ApplicationIo for WasmApplicationIo {
"graphite" => {
let path = url.path();
let path = path.to_owned();
log::info!("Loading resource: {}", path);
log::trace!("Loading local resource: {}", path);
let data = self.resources.get(&path).ok_or(ApplicationError::NotFound)?.clone();
Ok(Box::pin(async move { Ok(data.clone()) }) as Pin<Box<dyn Future<Output = Result<Arc<[u8]>, _>>>>)
}

View File

@ -539,7 +539,7 @@ fn node_registry() -> HashMap<NodeIdentifier, HashMap<NodeIOTypes, NodeConstruct
register_node!(graphene_core::ConstructLayerNode<_, _, _, _, _, _, _>, input: ImageFrame<Color>, params: [String, BlendMode, f32, bool, bool, bool, graphene_core::GraphicGroup]),
register_node!(graphene_core::ConstructLayerNode<_, _, _, _, _, _, _>, input: graphene_core::GraphicGroup, params: [String, BlendMode, f32, bool, bool, bool, graphene_core::GraphicGroup]),
register_node!(graphene_core::ConstructLayerNode<_, _, _, _, _, _, _>, input: graphene_core::Artboard, params: [String, BlendMode, f32, bool, bool, bool, graphene_core::GraphicGroup]),
register_node!(graphene_core::ConstructArtboardNode<_, _, _>, input: graphene_core::GraphicGroup, params: [glam::IVec2, glam::IVec2, Color]),
register_node!(graphene_core::ConstructArtboardNode<_, _, _, _>, input: graphene_core::GraphicGroup, params: [glam::IVec2, glam::IVec2, Color, bool]),
];
let mut map: HashMap<NodeIdentifier, HashMap<NodeIOTypes, NodeConstructor>> = HashMap::new();
for (id, c, types) in node_types.into_iter().flatten() {