Fix click targets (in, e.g., the boolean node) by resolving footprints from render output (#1946)

* add NodeId (u64) and Footprint to Graphic Group

* Render Output footprints

* Small bug fixes

* Commented out render output click targets/footprints

* Run graph when deleting

* Switch to node path

* Add upstream clicktargets for boolean operation

* Fix boolean operations

* Fix grouped layers

* Add click targets to vello render

* Add cache to artwork

* Fix demo artwork

* Improve recursion

* Code review

---------

Co-authored-by: Dennis Kobert <dennis@kobert.dev>
Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
adamgerhant 2024-09-15 18:26:59 -07:00 committed by GitHub
parent ef007736f5
commit ca0d102296
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 1003 additions and 670 deletions

899
Cargo.lock generated

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -11,6 +11,9 @@ use graphene_core::raster::BlendMode;
use graphene_core::raster::Image;
use graphene_core::vector::style::ViewMode;
use graphene_core::Color;
use graphene_std::renderer::ClickTarget;
use graphene_std::transform::Footprint;
use graphene_std::vector::VectorData;
use glam::DAffine2;
@ -155,6 +158,15 @@ pub enum DocumentMessage {
ToggleGridVisibility,
ToggleOverlaysVisibility,
ToggleSnapping,
UpdateUpstreamTransforms {
upstream_transforms: HashMap<NodeId, (Footprint, DAffine2)>,
},
UpdateClickTargets {
click_targets: HashMap<NodeId, Vec<ClickTarget>>,
},
UpdateVectorModify {
vector_modify: HashMap<NodeId, VectorData>,
},
Undo,
UngroupSelectedLayers,
UngroupLayer {

View File

@ -1022,8 +1022,9 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
if self.network_interface.transaction_status() == TransactionStatus::Finished {
return;
}
self.document_undo_history.pop_back();
self.network_interface.finish_transaction();
self.undo(ipp, responses);
responses.add(OverlaysMessage::Draw);
}
DocumentMessage::AddTransaction => {
@ -1054,6 +1055,25 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
self.snapping_state.snapping_enabled = !self.snapping_state.snapping_enabled;
responses.add(PortfolioMessage::UpdateDocumentWidgets);
}
DocumentMessage::UpdateUpstreamTransforms { upstream_transforms } => {
self.network_interface.update_transforms(upstream_transforms);
}
DocumentMessage::UpdateClickTargets { click_targets } => {
// TODO: Allow non layer nodes to have click targets
let layer_click_targets = click_targets
.into_iter()
.filter_map(|(node_id, click_targets)| {
self.network_interface.is_layer(&node_id, &[]).then(|| {
let layer = LayerNodeIdentifier::new(node_id, &self.network_interface, &[]);
(layer, click_targets)
})
})
.collect();
self.network_interface.update_click_targets(layer_click_targets);
}
DocumentMessage::UpdateVectorModify { vector_modify } => {
self.network_interface.update_vector_modify(vector_modify);
}
DocumentMessage::Undo => {
if self.network_interface.transaction_status() != TransactionStatus::Finished {
return;
@ -1107,6 +1127,9 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
node_ids: vec![layer.to_node()],
delete_children: true,
});
responses.add(NodeGraphMessage::RunDocumentGraph);
responses.add(NodeGraphMessage::SelectedNodesUpdated);
responses.add(NodeGraphMessage::SendGraph);
}
DocumentMessage::PTZUpdate => {
if !self.graph_view_overlay_open {
@ -1239,7 +1262,7 @@ impl DocumentMessageHandler {
.filter(|&layer| self.network_interface.selected_nodes(&[]).unwrap().layer_visible(layer, &self.network_interface))
.filter(|&layer| !self.network_interface.selected_nodes(&[]).unwrap().layer_locked(layer, &self.network_interface))
.filter(|&layer| !self.network_interface.is_artboard(&layer.to_node(), &[]))
.filter_map(|layer| self.metadata().click_target(layer).map(|targets| (layer, targets)))
.filter_map(|layer| self.metadata().click_targets(layer).map(|targets| (layer, targets)))
.filter(move |(layer, target)| {
target
.iter()
@ -1256,7 +1279,7 @@ impl DocumentMessageHandler {
.all_layers()
.filter(|&layer| self.network_interface.selected_nodes(&[]).unwrap().layer_visible(layer, &self.network_interface))
.filter(|&layer| !self.network_interface.selected_nodes(&[]).unwrap().layer_locked(layer, &self.network_interface))
.filter_map(|layer| self.metadata().click_target(layer).map(|targets| (layer, targets)))
.filter_map(|layer| self.metadata().click_targets(layer).map(|targets| (layer, targets)))
.filter(move |(layer, target)| target.iter().any(|target| target.intersect_point(point, self.metadata().transform_to_document(*layer))))
.map(|(layer, _)| layer)
}

View File

@ -206,6 +206,9 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageData<'_>> for Gr
});
}
// TODO: Replace deleted artboards with merge nodes
responses.add(NodeGraphMessage::RunDocumentGraph);
responses.add(NodeGraphMessage::SelectedNodesUpdated);
responses.add(NodeGraphMessage::SendGraph);
}
GraphOperationMessage::NewSvg {
id,

View File

@ -253,8 +253,12 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
},
DocumentNode {
manual_composition: Some(concrete!(Footprint)),
inputs: vec![NodeInput::node(NodeId(1), 0), NodeInput::node(NodeId(2), 0)],
implementation: DocumentNodeImplementation::proto("graphene_core::ConstructLayerNode<_, _>"),
inputs: vec![
NodeInput::node(NodeId(1), 0),
NodeInput::node(NodeId(2), 0),
NodeInput::Reflection(graph_craft::document::DocumentNodeMetadata::DocumentNodePath),
],
implementation: DocumentNodeImplementation::proto("graphene_core::ConstructLayerNode<_, _, _>"),
..Default::default()
},
]
@ -359,8 +363,9 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
inputs: vec![
NodeInput::network(graphene_core::Type::Fn(Box::new(concrete!(Footprint)), Box::new(concrete!(ArtboardGroup))), 0),
NodeInput::node(NodeId(1), 0),
NodeInput::Reflection(graph_craft::document::DocumentNodeMetadata::DocumentNodePath),
],
implementation: DocumentNodeImplementation::proto("graphene_core::AddArtboardNode<_, _>"),
implementation: DocumentNodeImplementation::proto("graphene_core::AddArtboardNode<_, _, _>"),
..Default::default()
},
]
@ -3800,14 +3805,62 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
category: "Vector",
node_template: NodeTemplate {
document_node: DocumentNode {
implementation: DocumentNodeImplementation::Network(NodeNetwork {
exports: vec![NodeInput::node(NodeId(1), 0)],
nodes: vec![
DocumentNode {
inputs: vec![NodeInput::network(concrete!(VectorData), 0), NodeInput::network(concrete!(vector::style::Fill), 1)],
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_std::vector::BooleanOperationNode<_>")),
..Default::default()
},
DocumentNode {
inputs: vec![NodeInput::node(NodeId(0), 0)],
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::memo::ImpureMemoNode<_, _, _>")),
manual_composition: Some(concrete!(Footprint)),
..Default::default()
},
]
.into_iter()
.enumerate()
.map(|(id, node)| (NodeId(id as u64), node))
.collect(),
..Default::default()
}),
inputs: vec![
NodeInput::value(TaggedValue::GraphicGroup(GraphicGroup::EMPTY), true),
NodeInput::value(TaggedValue::BooleanOperation(vector::misc::BooleanOperation::Union), false),
],
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_std::vector::BooleanOperationNode<_>")),
..Default::default()
},
persistent_node_metadata: DocumentNodePersistentMetadata {
network_metadata: Some(NodeNetworkMetadata {
persistent_metadata: NodeNetworkPersistentMetadata {
node_metadata: [
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
display_name: "Boolean Operation".to_string(),
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(-7, 0)),
..Default::default()
},
..Default::default()
},
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
display_name: "Cache".to_string(),
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, 0)),
..Default::default()
},
..Default::default()
},
]
.into_iter()
.enumerate()
.map(|(id, node)| (NodeId(id as u64), node))
.collect(),
..Default::default()
},
..Default::default()
}),
input_names: vec!["Group of Paths".to_string(), "Operation".to_string()],
output_names: vec!["Vector".to_string()],
..Default::default()

View File

@ -114,7 +114,8 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
responses.add(NodeGraphMessage::SendSelectedNodes);
responses.add(ArtboardToolMessage::UpdateSelectedArtboard);
responses.add(DocumentMessage::DocumentStructureChanged);
responses.add(NodeGraphMessage::RunDocumentGraph);
responses.add(OverlaysMessage::Draw);
responses.add(NodeGraphMessage::SendGraph);
}
NodeGraphMessage::CreateWire { output_connector, input_connector } => {
// TODO: Add support for flattening NodeInput::Network exports in flatten_with_fns https://github.com/GraphiteEditor/Graphite/issues/1762
@ -203,8 +204,6 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
}
NodeGraphMessage::DeleteNodes { node_ids, delete_children } => {
network_interface.delete_nodes(node_ids, delete_children, selection_network_path);
responses.add(NodeGraphMessage::SelectedNodesUpdated);
responses.add(NodeGraphMessage::SendGraph);
}
// Deletes selected_nodes. If `reconnect` is true, then all children nodes (secondary input) of the selected nodes are deleted and the siblings (primary input/output) are reconnected.
// If `reconnect` is false, then only the selected nodes are deleted and not reconnected.
@ -217,7 +216,10 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
responses.add(NodeGraphMessage::DeleteNodes {
node_ids: selected_nodes.selected_nodes().cloned().collect::<Vec<_>>(),
delete_children,
})
});
responses.add(NodeGraphMessage::RunDocumentGraph);
responses.add(NodeGraphMessage::SelectedNodesUpdated);
responses.add(NodeGraphMessage::SendGraph);
}
NodeGraphMessage::DisconnectInput { input_connector } => {
network_interface.disconnect_input(&input_connector, selection_network_path);

View File

@ -22,7 +22,6 @@ pub struct DocumentMetadata {
pub structure: HashMap<LayerNodeIdentifier, NodeRelations>,
pub click_targets: HashMap<LayerNodeIdentifier, Vec<ClickTarget>>,
pub vector_modify: HashMap<NodeId, VectorData>,
// TODO: Remove and derive from document_ptz in document message handler
/// Transform from document space to viewport space.
pub document_to_viewport: DAffine2,
}
@ -52,7 +51,7 @@ impl DocumentMetadata {
self.structure.contains_key(&layer)
}
pub fn click_target(&self, layer: LayerNodeIdentifier) -> Option<&Vec<ClickTarget>> {
pub fn click_targets(&self, layer: LayerNodeIdentifier) -> Option<&Vec<ClickTarget>> {
self.click_targets.get(&layer)
}
@ -71,29 +70,15 @@ impl DocumentMetadata {
// ============================
impl DocumentMetadata {
/// Update the cached transforms of the layers
pub fn update_transforms(&mut self, new_upstream_transforms: HashMap<NodeId, (Footprint, DAffine2)>) {
self.upstream_transforms = new_upstream_transforms;
}
/// Access the cached transformation to document space from layer space
pub fn transform_to_document(&self, layer: LayerNodeIdentifier) -> DAffine2 {
self.document_to_viewport.inverse() * self.transform_to_viewport(layer)
}
pub fn transform_to_viewport(&self, layer: LayerNodeIdentifier) -> DAffine2 {
layer
.ancestors(self)
.filter_map(|ancestor_layer| {
if ancestor_layer != LayerNodeIdentifier::ROOT_PARENT {
self.upstream_transforms.get(&ancestor_layer.to_node())
} else {
None
}
})
.copied()
.map(|(footprint, transform)| footprint.transform * transform)
.next()
self.upstream_transforms
.get(&layer.to_node())
.map(|(footprint, transform)| footprint.transform * *transform)
.unwrap_or(self.document_to_viewport)
}
@ -119,16 +104,9 @@ impl DocumentMetadata {
// ===============================
impl DocumentMetadata {
/// Update the cached click targets and vector modify values of the layers
pub fn update_from_monitor(&mut self, new_click_targets: HashMap<LayerNodeIdentifier, Vec<ClickTarget>>, new_vector_modify: HashMap<NodeId, VectorData>) {
self.click_targets = new_click_targets;
self.vector_modify = new_vector_modify;
}
/// Get the bounding box of the click target of the specified layer in the specified transform space
pub fn bounding_box_with_transform(&self, layer: LayerNodeIdentifier, transform: DAffine2) -> Option<[DVec2; 2]> {
self.click_targets
.get(&layer)?
self.click_targets(layer)?
.iter()
.filter_map(|click_target| click_target.subpath().bounding_box_with_transform(transform))
.reduce(Quad::combine_bounds)

View File

@ -11,6 +11,7 @@ use bezier_rs::Subpath;
use graph_craft::document::{value::TaggedValue, DocumentNode, DocumentNodeImplementation, NodeId, NodeInput, NodeNetwork, OldDocumentNodeImplementation, OldNodeNetwork};
use graph_craft::{concrete, Type};
use graphene_std::renderer::{ClickTarget, Quad};
use graphene_std::transform::Footprint;
use graphene_std::vector::{PointId, VectorData, VectorModificationType};
use interpreted_executor::{dynamic_executor::ResolvedDocumentNodeTypes, node_registry::NODE_REGISTRY};
@ -2327,7 +2328,7 @@ impl NodeNetworkInterface {
log::error!("Could not get nested node_metadata in position_from_downstream_node");
return None;
};
match &node_metadata.persistent_metadata.node_type_metadata.clone() {
match &node_metadata.persistent_metadata.node_type_metadata {
NodeTypePersistentMetadata::Layer(layer_metadata) => {
match layer_metadata.position {
LayerPosition::Absolute(position) => Some(position),
@ -2550,8 +2551,7 @@ impl NodeNetworkInterface {
}
pub fn set_document_to_viewport_transform(&mut self, transform: DAffine2) {
let document_metadata = self.document_metadata_mut();
document_metadata.document_to_viewport = transform;
self.document_metadata.document_to_viewport = transform;
}
pub fn is_eligible_to_be_layer(&mut self, node_id: &NodeId, network_path: &[NodeId]) -> bool {
@ -2882,7 +2882,7 @@ impl NodeNetworkInterface {
}
for (parent, child) in children {
parent.push_child(self.document_metadata_mut(), child);
parent.push_child(&mut self.document_metadata, child);
}
while let Some((primary_root_node_id, parent_layer_node)) = awaiting_primary_flow.pop() {
@ -2902,7 +2902,7 @@ impl NodeNetworkInterface {
}
}
for child in children {
parent_layer_node.push_child(self.document_metadata_mut(), child);
parent_layer_node.push_child(&mut self.document_metadata, child);
}
}
}
@ -2914,8 +2914,19 @@ impl NodeNetworkInterface {
self.document_metadata.click_targets.retain(|layer, _| self.document_metadata.structure.contains_key(layer));
}
pub fn document_metadata_mut(&mut self) -> &mut DocumentMetadata {
&mut self.document_metadata
/// Update the cached transforms of the layers
pub fn update_transforms(&mut self, new_upstream_transforms: HashMap<NodeId, (Footprint, DAffine2)>) {
self.document_metadata.upstream_transforms = new_upstream_transforms;
}
/// Update the cached click targets of the layers
pub fn update_click_targets(&mut self, new_click_targets: HashMap<LayerNodeIdentifier, Vec<ClickTarget>>) {
self.document_metadata.click_targets = new_click_targets;
}
/// Update the vector modify of the layers
pub fn update_vector_modify(&mut self, new_vector_modify: HashMap<NodeId, VectorData>) {
self.document_metadata.vector_modify = new_vector_modify;
}
}
@ -3083,7 +3094,7 @@ impl NodeNetworkInterface {
}
/// Keep metadata in sync with the new implementation if this is used by anything other than the upgrade scripts
pub fn set_implementation(&mut self, node_id: &NodeId, network_path: &[NodeId], implementation: DocumentNodeImplementation) {
pub fn replace_implementation(&mut self, node_id: &NodeId, network_path: &[NodeId], implementation: DocumentNodeImplementation) {
let Some(network) = self.network_mut(network_path) else {
log::error!("Could not get nested network in set_implementation");
return;
@ -3095,6 +3106,20 @@ impl NodeNetworkInterface {
node.implementation = implementation;
}
// TODO: Eventually remove this (probably starting late 2024)
/// Keep metadata in sync with the new implementation if this is used by anything other than the upgrade scripts
pub fn replace_implementation_metadata(&mut self, node_id: &NodeId, network_path: &[NodeId], metadata: DocumentNodePersistentMetadata) {
let Some(network_metadata) = self.network_metadata_mut(network_path) else {
log::error!("Could not get network metdata in set implementation");
return;
};
let Some(node_metadata) = network_metadata.persistent_metadata.node_metadata.get_mut(node_id) else {
log::error!("Could not get persistent node metadata for node {node_id} in set implementation");
return;
};
node_metadata.persistent_metadata.network_metadata = metadata.network_metadata;
}
/// Keep metadata in sync with the new implementation if this is used by anything other than the upgrade scripts
pub fn replace_inputs(&mut self, node_id: &NodeId, inputs: Vec<NodeInput>, network_path: &[NodeId]) -> Vec<NodeInput> {
let Some(network) = self.network_mut(network_path) else {

View File

@ -425,7 +425,10 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
{
let node_definition = crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type(reference).unwrap();
let default_definition_node = node_definition.default_node_template();
document.network_interface.set_implementation(node_id, &[], default_definition_node.document_node.implementation);
document.network_interface.replace_implementation(node_id, &[], default_definition_node.document_node.implementation);
document
.network_interface
.replace_implementation_metadata(node_id, &[], default_definition_node.persistent_node_metadata);
}
}
}
@ -461,7 +464,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
if reference == "Fill" && node.inputs.len() == 8 {
let node_definition = crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type(reference).unwrap();
let document_node = node_definition.default_node_template().document_node;
document.network_interface.set_implementation(node_id, &[], document_node.implementation.clone());
document.network_interface.replace_implementation(node_id, &[], document_node.implementation.clone());
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), &[]);
@ -515,6 +518,13 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
}
}
// Upgrade construct layer implementation from https://github.com/GraphiteEditor/Graphite/pull/1946
if reference == "Merge" || reference == "Artboard" {
let node_definition = crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type(reference).unwrap();
let new_merge_node = node_definition.default_node_template();
document.network_interface.replace_implementation(node_id, &[], new_merge_node.document_node.implementation)
}
// Upgrade artboard name being passed as hidden value input to "To Artboard"
if reference == "Artboard" {
let label = document.network_interface.display_name(node_id, &[]);

View File

@ -391,6 +391,9 @@ impl SelectToolData {
})
.collect();
responses.add(NodeGraphMessage::SelectedNodesSet { nodes });
responses.add(NodeGraphMessage::RunDocumentGraph);
responses.add(NodeGraphMessage::SelectedNodesUpdated);
responses.add(NodeGraphMessage::SendGraph);
self.layers_dragging = original;
}
}
@ -413,12 +416,13 @@ impl Fsm for SelectToolFsmState {
tool_data.selected_layers_changed = selected_layers_count != tool_data.selected_layers_count;
tool_data.selected_layers_count = selected_layers_count;
// Outline selected layers
// Outline selected layers, but not artboards
for layer in document
.network_interface
.selected_nodes(&[])
.unwrap()
.selected_visible_and_unlocked_layers(&document.network_interface)
.filter(|layer| !document.network_interface.is_artboard(&layer.to_node(), &[]))
{
overlay_context.outline(document.metadata().layer_outline(layer), document.metadata().transform_to_viewport(layer));
}
@ -429,7 +433,7 @@ impl Fsm for SelectToolFsmState {
.selected_nodes(&[])
.unwrap()
.selected_visible_and_unlocked_layers(&document.network_interface)
.next()
.find(|layer| !document.network_interface.is_artboard(&layer.to_node(), &[]))
.map(|layer| document.metadata().transform_to_viewport(layer));
let transform = transform.unwrap_or(DAffine2::IDENTITY);
if transform.matrix2.determinant() == 0. {
@ -440,6 +444,7 @@ impl Fsm for SelectToolFsmState {
.selected_nodes(&[])
.unwrap()
.selected_visible_and_unlocked_layers(&document.network_interface)
.filter(|layer| !document.network_interface.is_artboard(&layer.to_node(), &[]))
.filter_map(|layer| {
document
.metadata()
@ -649,7 +654,6 @@ impl Fsm for SelectToolFsmState {
}
if let Some(intersection) = intersection {
tool_data.layer_selected_on_start = Some(intersection);
selected = intersection_list;
@ -694,7 +698,7 @@ impl Fsm for SelectToolFsmState {
let axis_align = input.keyboard.key(modifier_keys.axis_align);
// Ignore the non duplicated layers if the current layers have not spawned yet.
let layers_exist = tool_data.layers_dragging.iter().all(|&layer| document.metadata().click_target(layer).is_some());
let layers_exist = tool_data.layers_dragging.iter().all(|&layer| document.metadata().click_targets(layer).is_some());
let ignore = tool_data.non_duplicated_layers.as_ref().filter(|_| !layers_exist).unwrap_or(&tool_data.layers_dragging);
let snap_data = SnapData::ignore(document, input, ignore);
@ -1195,9 +1199,12 @@ fn drag_shallowest_manipulation(responses: &mut VecDeque<Message>, selected: Vec
}
fn drag_deepest_manipulation(responses: &mut VecDeque<Message>, selected: Vec<LayerNodeIdentifier>, tool_data: &mut SelectToolData, document: &DocumentMessageHandler) {
tool_data.layers_dragging.append(&mut vec![document
.find_deepest(&selected)
.unwrap_or(LayerNodeIdentifier::ROOT_PARENT.children(document.metadata()).next().expect("Child should exist when dragging deepest"))]);
tool_data.layers_dragging.append(&mut vec![document.find_deepest(&selected).unwrap_or(
LayerNodeIdentifier::ROOT_PARENT
.children(document.metadata())
.next()
.expect("ROOT_PARENT should have a layer child when clicking"),
)]);
responses.add(NodeGraphMessage::SelectedNodesSet {
nodes: tool_data
.layers_dragging

View File

@ -87,6 +87,8 @@ impl<'a> MessageHandler<TransformLayerMessage, TransformData<'a>> for TransformL
*mouse_position = input.mouse.position;
*start_mouse = input.mouse.position;
selected.original_transforms.clear();
selected.responses.add(DocumentMessage::StartTransaction);
};
match message {
@ -97,6 +99,7 @@ impl<'a> MessageHandler<TransformLayerMessage, TransformData<'a>> for TransformL
self.transform_operation = TransformOperation::None;
responses.add(DocumentMessage::EndTransaction);
responses.add(ToolMessage::UpdateHints);
responses.add(NodeGraphMessage::RunDocumentGraph);
}
@ -156,6 +159,7 @@ impl<'a> MessageHandler<TransformLayerMessage, TransformData<'a>> for TransformL
self.transform_operation = TransformOperation::None;
responses.add(DocumentMessage::AbortTransaction);
responses.add(ToolMessage::UpdateHints);
}
TransformLayerMessage::ConstrainX => self.transform_operation.constrain_axis(Axis::X, &mut selected, self.snap),

View File

@ -1,26 +1,22 @@
use crate::consts::FILE_SAVE_SUFFIX;
use crate::messages::frontend::utility_types::{ExportBounds, FileType};
use crate::messages::portfolio::document::node_graph::document_node_definitions::wrap_network_in_scope;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::prelude::*;
use graph_craft::concrete;
use graph_craft::document::value::TaggedValue;
use graph_craft::document::value::{RenderOutput, TaggedValue};
use graph_craft::document::{generate_uuid, DocumentNodeImplementation, NodeId, NodeNetwork};
use graph_craft::graphene_compiler::Compiler;
use graph_craft::proto::GraphErrors;
use graph_craft::wasm_application_io::EditorPreferences;
use graphene_core::application_io::{NodeGraphUpdateMessage, NodeGraphUpdateSender, RenderConfig};
use graphene_core::memo::IORecord;
use graphene_core::raster::ImageFrame;
use graphene_core::renderer::{ClickTarget, GraphicElementRendered, ImageRenderMode, RenderParams, SvgRender};
use graphene_core::renderer::{GraphicElementRendered, ImageRenderMode, RenderParams, SvgRender};
use graphene_core::renderer::{RenderSvgSegmentList, SvgSegment};
use graphene_core::text::FontCache;
use graphene_core::transform::{Footprint, Transform};
use graphene_core::transform::Footprint;
use graphene_core::vector::style::ViewMode;
use graphene_core::vector::VectorData;
use graphene_core::{Color, GraphicElement, SurfaceFrame};
use graphene_std::renderer::format_transform_matrix;
use graphene_std::renderer::{format_transform_matrix, RenderMetadata};
use graphene_std::wasm_application_io::{WasmApplicationIo, WasmEditorApi};
use interpreted_executor::dynamic_executor::{DynamicExecutor, IntrospectError, ResolvedDocumentNodeTypesDelta};
@ -48,12 +44,6 @@ pub struct NodeRuntime {
// TODO: Remove, it doesn't need to be persisted anymore
/// The current renders of the thumbnails for layer nodes.
thumbnail_renders: HashMap<NodeId, Vec<SvgSegment>>,
/// The current click targets for layer nodes.
click_targets: HashMap<NodeId, Vec<ClickTarget>>,
/// Vector data in Path nodes.
vector_modify: HashMap<NodeId, VectorData>,
/// The current upstream transforms for nodes.
upstream_transforms: HashMap<NodeId, (Footprint, DAffine2)>,
}
/// Messages passed from the editor thread to the node runtime thread.
@ -83,9 +73,6 @@ pub struct ExecutionResponse {
execution_id: u64,
result: Result<TaggedValue, String>,
responses: VecDeque<FrontendMessage>,
new_click_targets: HashMap<LayerNodeIdentifier, Vec<ClickTarget>>,
new_vector_modify: HashMap<NodeId, VectorData>,
new_upstream_transforms: HashMap<NodeId, (Footprint, DAffine2)>,
transform: DAffine2,
}
@ -144,9 +131,6 @@ impl NodeRuntime {
monitor_nodes: Vec::new(),
thumbnail_renders: Default::default(),
click_targets: HashMap::new(),
vector_modify: HashMap::new(),
upstream_transforms: HashMap::new(),
}
}
@ -226,9 +210,6 @@ impl NodeRuntime {
execution_id,
result,
responses,
new_click_targets: self.click_targets.clone().into_iter().map(|(id, targets)| (LayerNodeIdentifier::new_unchecked(id), targets)).collect(),
new_vector_modify: self.vector_modify.clone(),
new_upstream_transforms: self.upstream_transforms.clone(),
transform,
});
}
@ -300,28 +281,9 @@ impl NodeRuntime {
};
if let Some(io) = introspected_data.downcast_ref::<IORecord<Footprint, graphene_core::GraphicElement>>() {
Self::process_graphic_element(&mut self.thumbnail_renders, &mut self.click_targets, parent_network_node_id, &io.output, responses, update_thumbnails)
Self::process_graphic_element(&mut self.thumbnail_renders, parent_network_node_id, &io.output, responses, update_thumbnails)
} else if let Some(io) = introspected_data.downcast_ref::<IORecord<Footprint, graphene_core::Artboard>>() {
Self::process_graphic_element(&mut self.thumbnail_renders, &mut self.click_targets, parent_network_node_id, &io.output, responses, update_thumbnails)
} else if let Some(record) = introspected_data.downcast_ref::<IORecord<Footprint, VectorData>>() {
// Insert the vector modify if we are dealing with vector data
self.vector_modify.insert(parent_network_node_id, record.output.clone());
}
// If this is `VectorData`, `ImageFrame`, or `GraphicElement` data:
// Update the stored upstream transforms for this layer/node.
if let Some(transform) = {
fn try_downcast<T: Transform + 'static>(value: &dyn std::any::Any) -> Option<(Footprint, DAffine2)> {
let io_data = value.downcast_ref::<IORecord<Footprint, T>>()?;
let transform = io_data.output.transform();
Some((io_data.input, transform))
}
None.or_else(|| try_downcast::<VectorData>(introspected_data.as_ref()))
.or_else(|| try_downcast::<ImageFrame<Color>>(introspected_data.as_ref()))
.or_else(|| try_downcast::<GraphicElement>(introspected_data.as_ref()))
.or_else(|| try_downcast::<graphene_core::Artboard>(introspected_data.as_ref()))
} {
self.upstream_transforms.insert(parent_network_node_id, transform);
Self::process_graphic_element(&mut self.thumbnail_renders, parent_network_node_id, &io.output, responses, update_thumbnails)
}
}
}
@ -330,16 +292,11 @@ impl NodeRuntime {
// Regenerate click targets and thumbnails for the layers in the graph, modifying the state and updating the UI.
fn process_graphic_element(
thumbnail_renders: &mut HashMap<NodeId, Vec<SvgSegment>>,
click_targets: &mut HashMap<NodeId, Vec<ClickTarget>>,
parent_network_node_id: NodeId,
graphic_element: &impl GraphicElementRendered,
responses: &mut VecDeque<FrontendMessage>,
update_thumbnails: bool,
) {
let click_targets = click_targets.entry(parent_network_node_id).or_default();
click_targets.clear();
graphic_element.add_click_targets(click_targets);
// RENDER THUMBNAIL
if !update_thumbnails {
@ -536,8 +493,12 @@ impl NodeGraphExecutor {
}
fn export(&self, node_graph_output: TaggedValue, export_config: ExportConfig, responses: &mut VecDeque<Message>) -> Result<(), String> {
let TaggedValue::RenderOutput(graphene_std::wasm_application_io::RenderOutput::Svg(svg)) = node_graph_output else {
return Err("Incorrect render type for exportign (expected RenderOutput::Svg)".to_string());
let TaggedValue::RenderOutput(RenderOutput {
data: graphene_std::wasm_application_io::RenderOutputType::Svg(svg),
..
}) = node_graph_output
else {
return Err("Incorrect render type for exporting (expected RenderOutput::Svg)".to_string());
};
let ExportConfig {
@ -572,10 +533,7 @@ impl NodeGraphExecutor {
let ExecutionResponse {
execution_id,
result,
new_click_targets,
responses: existing_responses,
new_vector_modify,
new_upstream_transforms,
transform,
} = execution_response;
@ -585,15 +543,13 @@ impl NodeGraphExecutor {
Ok(output) => output,
Err(e) => {
// Clear the click targets while the graph is in an un-renderable state
document.network_interface.document_metadata_mut().update_from_monitor(HashMap::new(), HashMap::new());
document.network_interface.update_click_targets(HashMap::new());
document.network_interface.update_vector_modify(HashMap::new());
return Err(format!("Node graph evaluation failed:\n{e}"));
}
};
responses.extend(existing_responses.into_iter().map(Into::into));
document.network_interface.document_metadata_mut().update_transforms(new_upstream_transforms);
document.network_interface.document_metadata_mut().update_from_monitor(new_click_targets, new_vector_modify);
let execution_context = self.futures.remove(&execution_id).ok_or_else(|| "Invalid generation ID".to_string())?;
if let Some(export_config) = execution_context.export_config {
@ -608,7 +564,10 @@ impl NodeGraphExecutor {
let type_delta = match result {
Err(e) => {
// Clear the click targets while the graph is in an un-renderable state
document.network_interface.document_metadata_mut().update_from_monitor(HashMap::new(), HashMap::new());
document.network_interface.update_click_targets(HashMap::new());
document.network_interface.update_vector_modify(HashMap::new());
log::trace!("{e}");
responses.add(NodeGraphMessage::UpdateTypes {
@ -654,25 +613,37 @@ impl NodeGraphExecutor {
fn process_node_graph_output(&mut self, node_graph_output: TaggedValue, transform: DAffine2, responses: &mut VecDeque<Message>) -> Result<(), String> {
match node_graph_output {
TaggedValue::SurfaceFrame(SurfaceFrame { .. }) => {
// TODO: Reimplement this now that document-legacy is gone
}
TaggedValue::RenderOutput(graphene_std::wasm_application_io::RenderOutput::Svg(svg)) => {
// Send to frontend
responses.add(FrontendMessage::UpdateDocumentArtwork { svg });
responses.add(DocumentMessage::RenderScrollbars);
responses.add(DocumentMessage::RenderRulers);
}
TaggedValue::RenderOutput(graphene_std::wasm_application_io::RenderOutput::CanvasFrame(frame)) => {
let matrix = format_transform_matrix(frame.transform);
let transform = if matrix.is_empty() { String::new() } else { format!(" transform=\"{}\"", matrix) };
let svg = format!(
r#"<svg><foreignObject width="{}" height="{}"{transform}><div data-canvas-placeholder="canvas{}"></div></foreignObject></svg>"#,
frame.resolution.x, frame.resolution.y, frame.surface_id.0
);
responses.add(FrontendMessage::UpdateDocumentArtwork { svg });
TaggedValue::RenderOutput(render_output) => {
let RenderMetadata {
footprints,
click_targets,
vector_data,
} = render_output.metadata;
match render_output.data {
graphene_std::wasm_application_io::RenderOutputType::Svg(svg) => {
// Send to frontend
responses.add(FrontendMessage::UpdateDocumentArtwork { svg });
}
graphene_std::wasm_application_io::RenderOutputType::CanvasFrame(frame) => {
let matrix = format_transform_matrix(frame.transform);
let transform = if matrix.is_empty() { String::new() } else { format!(" transform=\"{}\"", matrix) };
let svg = format!(
r#"<svg><foreignObject width="{}" height="{}"{transform}><div data-canvas-placeholder="canvas{}"></div></foreignObject></svg>"#,
frame.resolution.x, frame.resolution.y, frame.surface_id.0
);
responses.add(FrontendMessage::UpdateDocumentArtwork { svg });
}
_ => {
return Err(format!("Invalid node graph output type: {:#?}", render_output.data));
}
}
responses.add(DocumentMessage::UpdateUpstreamTransforms { upstream_transforms: footprints });
responses.add(DocumentMessage::UpdateClickTargets { click_targets });
responses.add(DocumentMessage::UpdateVectorModify { vector_modify: vector_data });
responses.add(DocumentMessage::RenderScrollbars);
responses.add(DocumentMessage::RenderRulers);
responses.add(OverlaysMessage::Draw);
}
TaggedValue::Bool(render_object) => Self::debug_render(render_object, transform, responses),
TaggedValue::String(render_object) => Self::debug_render(render_object, transform, responses),

View File

@ -499,8 +499,8 @@
// Dimming
&.selected {
// Halfway between 3-darkgray and 4-dimgray (this interpolation approach only works on grayscale values)
--component: calc((Max(var(--color-3-darkgray-rgb)) + Max(var(--color-4-dimgray-rgb))) / 2);
// 1/3 between 3-darkgray and 4-dimgray (this interpolation approach only works on grayscale values)
--component: calc((Max(var(--color-3-darkgray-rgb)) * 2 + Max(var(--color-4-dimgray-rgb))) / 3);
background: rgb(var(--component), var(--component), var(--component));
&.full-highlight {

View File

@ -622,11 +622,13 @@ impl EditorHandle {
self.dispatch(message);
let id = NodeId(id);
let message = NodeGraphMessage::DeleteNodes {
self.dispatch(NodeGraphMessage::DeleteNodes {
node_ids: vec![id],
delete_children: true,
};
self.dispatch(message);
});
self.dispatch(NodeGraphMessage::RunDocumentGraph);
self.dispatch(NodeGraphMessage::SelectedNodesUpdated);
self.dispatch(NodeGraphMessage::SendGraph);
}
/// Toggle lock state of a layer from the layer list
@ -733,7 +735,7 @@ impl EditorHandle {
for node_id in nodes_to_upgrade {
document
.network_interface
.set_implementation(&node_id, &[], DocumentNodeImplementation::proto("graphene_core::ConstructArtboardNode<_, _, _, _, _, _>"));
.replace_implementation(&node_id, &[], DocumentNodeImplementation::proto("graphene_core::ConstructArtboardNode<_, _, _, _, _, _>"));
document
.network_interface
.add_input(&node_id, &[], TaggedValue::IVec2(glam::IVec2::default()), false, 2, "".to_string());

View File

@ -1,6 +1,7 @@
use crate::application_io::TextureFrame;
use crate::raster::{BlendMode, ImageFrame};
use crate::transform::{Footprint, Transform, TransformMut};
use crate::uuid::NodeId;
use crate::vector::VectorData;
use crate::{Color, Node};
@ -42,7 +43,7 @@ impl AlphaBlending {
#[derive(Clone, Debug, PartialEq, DynAny, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct GraphicGroup {
elements: Vec<GraphicElement>,
elements: Vec<(GraphicElement, Option<NodeId>)>,
pub transform: DAffine2,
pub alpha_blending: AlphaBlending,
}
@ -64,7 +65,7 @@ impl GraphicGroup {
pub fn new(elements: Vec<GraphicElement>) -> Self {
Self {
elements,
elements: elements.into_iter().map(|element| (element, None)).collect(),
transform: DAffine2::IDENTITY,
alpha_blending: AlphaBlending::new(),
}
@ -216,7 +217,7 @@ impl Artboard {
#[derive(Clone, Default, Debug, Hash, PartialEq, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ArtboardGroup {
pub artboards: Vec<Artboard>,
pub artboards: Vec<(Artboard, Option<NodeId>)>,
}
impl ArtboardGroup {
@ -226,14 +227,15 @@ impl ArtboardGroup {
Default::default()
}
fn add_artboard(&mut self, artboard: Artboard) {
self.artboards.push(artboard);
fn add_artboard(&mut self, artboard: Artboard, node_id: Option<NodeId>) {
self.artboards.push((artboard, node_id));
}
}
pub struct ConstructLayerNode<Stack, GraphicElement> {
pub struct ConstructLayerNode<Stack, GraphicElement, NodePath> {
stack: Stack,
graphic_element: GraphicElement,
node_path: NodePath,
}
#[node_fn(ConstructLayerNode)]
@ -241,6 +243,7 @@ async fn construct_layer<Data: Into<GraphicElement> + Send>(
footprint: crate::transform::Footprint,
mut stack: impl Node<crate::transform::Footprint, Output = GraphicGroup>,
graphic_element: impl Node<crate::transform::Footprint, Output = Data>,
node_path: Vec<NodeId>,
) -> GraphicGroup {
let graphic_element = self.graphic_element.eval(footprint).await;
let mut stack = self.stack.eval(footprint).await;
@ -252,7 +255,9 @@ async fn construct_layer<Data: Into<GraphicElement> + Send>(
stack.transform = DAffine2::IDENTITY;
}
stack.push(element);
// Get the penultimate element of the node path, or None if the path is too short
let encapsulating_node_id = node_path.get(node_path.len().wrapping_sub(2)).copied();
stack.push((element, encapsulating_node_id));
stack
}
@ -301,17 +306,25 @@ async fn construct_artboard(
clip,
}
}
pub struct AddArtboardNode<ArtboardGroup, Artboard> {
pub struct AddArtboardNode<ArtboardGroup, Artboard, NodePath> {
artboards: ArtboardGroup,
artboard: Artboard,
node_path: NodePath,
}
#[node_fn(AddArtboardNode)]
async fn add_artboard<Data: Into<Artboard> + Send>(footprint: Footprint, artboards: impl Node<Footprint, Output = ArtboardGroup>, artboard: impl Node<Footprint, Output = Data>) -> ArtboardGroup {
async fn add_artboard<Data: Into<Artboard> + Send>(
footprint: Footprint,
artboards: impl Node<Footprint, Output = ArtboardGroup>,
artboard: impl Node<Footprint, Output = Data>,
node_path: Vec<NodeId>,
) -> ArtboardGroup {
let artboard = self.artboard.eval(footprint).await;
let mut artboards = self.artboards.eval(footprint).await;
artboards.add_artboard(artboard.into());
// Get the penultimate element of the node path, or None if the path is too short
let encapsulating_node_id = node_path.get(node_path.len().wrapping_sub(2)).copied();
artboards.add_artboard(artboard.into(), encapsulating_node_id);
artboards
}
@ -338,7 +351,7 @@ impl From<GraphicGroup> for GraphicElement {
}
impl Deref for GraphicGroup {
type Target = Vec<GraphicElement>;
type Target = Vec<(GraphicElement, Option<NodeId>)>;
fn deref(&self) -> &Self::Target {
&self.elements
}
@ -363,7 +376,7 @@ where
{
fn from(value: T) -> Self {
Self {
elements: (vec![value.into()]),
elements: (vec![(value.into(), None)]),
transform: DAffine2::IDENTITY,
alpha_blending: AlphaBlending::default(),
}

View File

@ -4,24 +4,26 @@ pub use quad::Quad;
pub use rect::Rect;
use crate::raster::{BlendMode, Image, ImageFrame};
use crate::transform::Transform;
use crate::uuid::generate_uuid;
use crate::transform::{Footprint, Transform};
use crate::uuid::{generate_uuid, NodeId};
use crate::vector::style::{Fill, Stroke, ViewMode};
use crate::vector::PointId;
use crate::Raster;
use crate::{vector::VectorData, Artboard, Color, GraphicElement, GraphicGroup};
use bezier_rs::Subpath;
use dyn_any::{DynAny, StaticType};
use base64::Engine;
use glam::{DAffine2, DVec2};
use num_traits::Zero;
use std::collections::HashMap;
use std::fmt::Write;
#[cfg(feature = "vello")]
use vello::*;
/// Represents a clickable target for the layer
#[derive(Clone, Debug)]
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct ClickTarget {
subpath: bezier_rs::Subpath<PointId>,
stroke_width: f64,
@ -268,16 +270,35 @@ pub fn to_transform(transform: DAffine2) -> usvg::Transform {
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)
}
#[derive(Debug, Clone, PartialEq, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct RenderMetadata {
pub footprints: HashMap<NodeId, (Footprint, DAffine2)>,
pub click_targets: HashMap<NodeId, Vec<ClickTarget>>,
pub vector_data: HashMap<NodeId, VectorData>,
}
pub trait GraphicElementRendered {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams);
fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]>;
fn add_click_targets(&self, click_targets: &mut Vec<ClickTarget>);
// The upstream click targets for each layer are collected during the render so that they do not have to be calculated for each click detection
fn add_upstream_click_targets(&self, _click_targets: &mut Vec<ClickTarget>) {}
// TODO: Store all click targets in a vec which contains the AABB, click target, and path
// fn add_click_targets(&self, click_targets: &mut Vec<([DVec2; 2], ClickTarget, Vec<NodeId>)>, current_path: Option<NodeId>) {}
// Recursively iterate over data in the render (including groups upstream from vector data in the case of a boolean operation) to collect the footprints, click targets, and vector modify
fn collect_metadata(&self, _metadata: &mut RenderMetadata, _footprint: Footprint, _element_id: Option<NodeId>) {}
#[cfg(feature = "vello")]
fn to_vello_scene(&self, transform: DAffine2, context: &mut RenderContext) -> Scene {
let mut scene = vello::Scene::new();
self.render_to_vello(&mut scene, transform, context);
scene
}
#[cfg(feature = "vello")]
fn render_to_vello(&self, _scene: &mut Scene, _transform: DAffine2, _render_condext: &mut RenderContext) {}
@ -305,7 +326,7 @@ impl GraphicElementRendered for GraphicGroup {
}
},
|render| {
for element in self.iter() {
for (element, _) in self.iter() {
element.render_svg(render, render_params);
}
},
@ -313,16 +334,35 @@ impl GraphicElementRendered for GraphicGroup {
}
fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]> {
self.iter().filter_map(|element| element.bounding_box(transform * self.transform)).reduce(Quad::combine_bounds)
self.iter().filter_map(|(element, _)| element.bounding_box(transform * self.transform)).reduce(Quad::combine_bounds)
}
fn add_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
for element in self.elements.iter() {
fn collect_metadata(&self, metadata: &mut RenderMetadata, mut footprint: Footprint, element_id: Option<NodeId>) {
footprint.transform *= self.transform;
for (element, element_id) in self.elements.iter() {
if let Some(element_id) = element_id {
element.collect_metadata(metadata, footprint, Some(*element_id));
}
}
if let Some(graphic_group_id) = element_id {
let mut all_upstream_click_targets = Vec::new();
self.add_upstream_click_targets(&mut all_upstream_click_targets);
metadata.click_targets.insert(graphic_group_id, all_upstream_click_targets);
}
}
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
for (element, _) in self.elements.iter() {
let mut new_click_targets = Vec::new();
element.add_click_targets(&mut new_click_targets);
element.add_upstream_click_targets(&mut new_click_targets);
for click_target in new_click_targets.iter_mut() {
click_target.apply_transform(element.transform())
}
click_targets.extend(new_click_targets);
}
}
@ -344,16 +384,18 @@ impl GraphicElementRendered for GraphicGroup {
&vello::kurbo::Rect::new(bounds[0].x, bounds[0].y, bounds[1].x, bounds[1].y),
);
}
for element in self.iter() {
for (element, _) in self.iter() {
element.render_to_vello(scene, child_transform, context);
}
if layer {
scene.pop_layer();
}
}
fn contains_artboard(&self) -> bool {
self.iter().any(|element| element.contains_artboard())
self.iter().any(|(element, _)| element.contains_artboard())
}
}
@ -402,7 +444,29 @@ impl GraphicElementRendered for VectorData {
self.bounding_box_with_transform(transform * self.transform).map(|[a, b]| [a - offset, b + offset])
}
fn add_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
fn collect_metadata(&self, metadata: &mut RenderMetadata, mut footprint: Footprint, element_id: Option<NodeId>) {
if let Some(element_id) = element_id {
let stroke_width = self.style.stroke().as_ref().map_or(0., Stroke::weight);
let filled = self.style.fill() != &Fill::None;
let fill = |mut subpath: bezier_rs::Subpath<_>| {
if filled {
subpath.set_closed(true);
}
subpath
};
metadata
.click_targets
.insert(element_id, self.stroke_bezier_paths().map(fill).map(|subpath| ClickTarget::new(subpath, stroke_width)).collect());
metadata.vector_data.insert(element_id, self.clone());
}
if let Some(upstream_graphic_group) = &self.upstream_graphic_group {
footprint.transform *= self.transform;
upstream_graphic_group.collect_metadata(metadata, footprint, None);
}
}
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
let stroke_width = self.style.stroke().as_ref().map_or(0., Stroke::weight);
let filled = self.style.fill() != &Fill::None;
let fill = |mut subpath: bezier_rs::Subpath<_>| {
@ -558,21 +622,13 @@ impl GraphicElementRendered for Artboard {
"g",
// Group tag attributes
|attributes| {
let matrix = format_transform_matrix(DAffine2::from_translation(self.location.as_dvec2()) * self.graphic_group.transform);
if !matrix.is_empty() {
attributes.push("transform", matrix);
}
if self.clip {
let id = format!("artboard-{}", generate_uuid());
let selector = format!("url(#{id})");
let matrix = format_transform_matrix(self.graphic_group.transform.inverse());
let transform = if matrix.is_empty() { String::new() } else { format!(r#" transform="{matrix}""#) };
write!(
&mut attributes.0.svg_defs,
r##"<clipPath id="{id}"><rect x="0" y="0" width="{}" height="{}"{transform} /></clipPath>"##,
r##"<clipPath id="{id}"><rect x="0" y="0" width="{}" height="{}" /></clipPath>"##,
self.dimensions.x, self.dimensions.y
)
.unwrap();
@ -581,9 +637,7 @@ impl GraphicElementRendered for Artboard {
},
// Artboard contents
|render| {
for element in self.graphic_group.iter() {
element.render_svg(render, render_params);
}
self.graphic_group.render_svg(render, render_params);
},
);
}
@ -597,6 +651,25 @@ impl GraphicElementRendered for Artboard {
}
}
fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option<NodeId>) {
if let Some(element_id) = element_id {
let subpath = Subpath::new_rect(DVec2::ZERO, self.dimensions.as_dvec2());
metadata.click_targets.insert(element_id, vec![ClickTarget::new(subpath, 0.)]);
metadata.footprints.insert(element_id, (footprint, DAffine2::from_translation(self.location.as_dvec2())));
}
self.graphic_group.collect_metadata(metadata, footprint, None);
}
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
let mut subpath = Subpath::new_rect(DVec2::ZERO, self.dimensions.as_dvec2());
if self.graphic_group.transform.matrix2.determinant() != 0. {
subpath.apply_transform(self.graphic_group.transform.inverse());
click_targets.push(ClickTarget::new(subpath, 0.));
}
}
#[cfg(feature = "vello")]
fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext) {
use vello::peniko;
@ -621,14 +694,6 @@ impl GraphicElementRendered for Artboard {
}
}
fn add_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
let mut subpath = Subpath::new_rect(DVec2::ZERO, self.dimensions.as_dvec2());
if self.graphic_group.transform.matrix2.determinant() != 0. {
subpath.apply_transform(self.graphic_group.transform.inverse());
click_targets.push(ClickTarget::new(subpath, 0.));
}
}
fn contains_artboard(&self) -> bool {
true
}
@ -636,24 +701,30 @@ impl GraphicElementRendered for Artboard {
impl GraphicElementRendered for crate::ArtboardGroup {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
for artboard in &self.artboards {
for (artboard, _) in &self.artboards {
artboard.render_svg(render, render_params);
}
}
fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]> {
self.artboards.iter().filter_map(|element| element.bounding_box(transform)).reduce(Quad::combine_bounds)
self.artboards.iter().filter_map(|(element, _)| element.bounding_box(transform)).reduce(Quad::combine_bounds)
}
fn add_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
for artboard in &self.artboards {
artboard.add_click_targets(click_targets);
fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, _element_id: Option<NodeId>) {
for (artboard, element_id) in &self.artboards {
artboard.collect_metadata(metadata, footprint, *element_id);
}
}
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
for (artboard, _) in &self.artboards {
artboard.add_upstream_click_targets(click_targets);
}
}
#[cfg(feature = "vello")]
fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext) {
for artboard in &self.artboards {
for (artboard, _) in &self.artboards {
artboard.render_to_vello(scene, transform, context)
}
}
@ -704,7 +775,15 @@ impl GraphicElementRendered for ImageFrame<Color> {
(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 collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option<NodeId>) {
let Some(element_id) = element_id else { return };
let subpath = Subpath::new_rect(DVec2::ZERO, DVec2::ONE);
metadata.click_targets.insert(element_id, vec![ClickTarget::new(subpath, 0.)]);
metadata.footprints.insert(element_id, (footprint, self.transform));
}
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
let subpath = Subpath::new_rect(DVec2::ZERO, DVec2::ONE);
click_targets.push(ClickTarget::new(subpath, 0.));
}
@ -774,7 +853,15 @@ impl GraphicElementRendered for Raster {
(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 collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option<NodeId>) {
let Some(element_id) = element_id else { return };
let subpath = Subpath::new_rect(DVec2::ZERO, DVec2::ONE);
metadata.click_targets.insert(element_id, vec![ClickTarget::new(subpath, 0.)]);
metadata.footprints.insert(element_id, (footprint, self.transform()));
}
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
let subpath = Subpath::new_rect(DVec2::ZERO, DVec2::ONE);
click_targets.push(ClickTarget::new(subpath, 0.));
}
@ -836,11 +923,23 @@ impl GraphicElementRendered for GraphicElement {
}
}
fn add_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option<NodeId>) {
if let Some(element_id) = element_id {
metadata.footprints.insert(element_id, (footprint, self.transform()));
}
match self {
GraphicElement::VectorData(vector_data) => vector_data.add_click_targets(click_targets),
GraphicElement::Raster(raster) => raster.add_click_targets(click_targets),
GraphicElement::GraphicGroup(graphic_group) => graphic_group.add_click_targets(click_targets),
GraphicElement::VectorData(vector_data) => vector_data.collect_metadata(metadata, footprint, element_id),
GraphicElement::Raster(raster) => raster.collect_metadata(metadata, footprint, element_id),
GraphicElement::GraphicGroup(graphic_group) => graphic_group.collect_metadata(metadata, footprint, element_id),
}
}
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
match self {
GraphicElement::VectorData(vector_data) => vector_data.add_upstream_click_targets(click_targets),
GraphicElement::Raster(raster) => raster.add_upstream_click_targets(click_targets),
GraphicElement::GraphicGroup(graphic_group) => graphic_group.add_upstream_click_targets(click_targets),
}
}
@ -884,8 +983,6 @@ impl<T: Primitive> GraphicElementRendered for T {
fn bounding_box(&self, _transform: DAffine2) -> Option<[DVec2; 2]> {
None
}
fn add_click_targets(&self, _click_targets: &mut Vec<ClickTarget>) {}
}
impl GraphicElementRendered for Option<Color> {
@ -911,8 +1008,6 @@ impl GraphicElementRendered for Option<Color> {
fn bounding_box(&self, _transform: DAffine2) -> Option<[DVec2; 2]> {
None
}
fn add_click_targets(&self, _click_targets: &mut Vec<ClickTarget>) {}
}
impl GraphicElementRendered for Vec<Color> {
@ -934,8 +1029,6 @@ impl GraphicElementRendered for Vec<Color> {
fn bounding_box(&self, _transform: DAffine2) -> Option<[DVec2; 2]> {
None
}
fn add_click_targets(&self, _click_targets: &mut Vec<ClickTarget>) {}
}
/// A segment of an svg string to allow for embedding blob urls

View File

@ -103,7 +103,7 @@ impl TransformMut for VectorData {
impl Transform for Artboard {
fn transform(&self) -> DAffine2 {
DAffine2::from_translation(self.location.as_dvec2()) * self.graphic_group.transform
DAffine2::from_translation(self.location.as_dvec2())
}
fn local_pivot(&self, pivot: DVec2) -> DVec2 {
self.location.as_dvec2() + self.dimensions.as_dvec2() * pivot

View File

@ -1,3 +1,8 @@
pub use uuid_generation::*;
use dyn_any::DynAny;
use dyn_any::StaticType;
#[derive(Clone, Copy, serde::Serialize, serde::Deserialize, specta::Type)]
pub struct Uuid(
#[serde(with = "u64_string")]
@ -66,4 +71,19 @@ mod uuid_generation {
}
}
pub use uuid_generation::*;
#[repr(transparent)]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize, specta::Type, DynAny)]
pub struct NodeId(pub u64);
// TODO: Find and replace all `NodeId(generate_uuid())` with `NodeId::new()`.
impl NodeId {
pub fn new() -> Self {
Self(generate_uuid())
}
}
impl core::fmt::Display for NodeId {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "{}", self.0)
}
}

View File

@ -742,6 +742,12 @@ impl PathStyle {
self.fill = fill;
}
pub fn set_stroke_transform(&mut self, transform: DAffine2) {
if let Some(stroke) = &mut self.stroke {
stroke.transform = transform;
}
}
/// Replace the path's [Stroke] with a provided one.
///
/// # Example

View File

@ -27,6 +27,9 @@ pub struct VectorData {
pub point_domain: PointDomain,
pub segment_domain: SegmentDomain,
pub region_domain: RegionDomain,
// Used to store the upstream graphic group during destructive Boolean Operations (and other nodes with a similar effect) so that click targets can be preserved.
pub upstream_graphic_group: Option<crate::GraphicGroup>,
}
impl core::hash::Hash for VectorData {
@ -52,6 +55,7 @@ impl VectorData {
point_domain: PointDomain::new(),
segment_domain: SegmentDomain::new(),
region_domain: RegionDomain::new(),
upstream_graphic_group: None,
}
}

View File

@ -23,7 +23,7 @@ pub struct AssignColorsNode<Fill, Stroke, Gradient, Reverse, Randomize, Seed, Re
#[node_macro::node_fn(AssignColorsNode)]
fn assign_colors_node(group: GraphicGroup, fill: bool, stroke: bool, gradient: GradientStops, reverse: bool, randomize: bool, seed: u32, repeat_every: u32) -> GraphicGroup {
let mut group = group;
let vector_data_list: Vec<_> = group.iter_mut().filter_map(|element| element.as_vector_data_mut()).collect();
let vector_data_list: Vec<_> = group.iter_mut().filter_map(|(element, _)| element.as_vector_data_mut()).collect();
let list = (vector_data_list.len(), vector_data_list.into_iter());
assign_colors(
@ -298,9 +298,9 @@ pub trait ConcatElement {
impl ConcatElement for GraphicGroup {
fn concat(&mut self, other: &Self, transform: DAffine2) {
// TODO: Decide if we want to keep this behavior whereby the layers are flattened
for mut element in other.iter().cloned() {
for (mut element, footprint_mapping) in other.iter().cloned() {
*element.transform_mut() = transform * element.transform() * other.transform();
self.push(element);
self.push((element, footprint_mapping));
}
self.alpha_blending = other.alpha_blending;
}

View File

@ -4,7 +4,9 @@ use crate::proto::{ConstructionArgs, ProtoNetwork, ProtoNode, ProtoNodeInput};
use dyn_any::{DynAny, StaticType};
use graphene_core::memo::MemoHashGuard;
pub use graphene_core::uuid::generate_uuid;
pub use graphene_core::uuid::NodeId;
use graphene_core::{Cow, MemoHash, ProtoNodeIdentifier, Type};
pub mod value;
use glam::IVec2;
use log::Metadata;
@ -13,25 +15,6 @@ use std::collections::hash_map::DefaultHasher;
use std::collections::HashMap;
use std::hash::{Hash, Hasher};
pub mod value;
#[repr(transparent)]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize, specta::Type, DynAny)]
pub struct NodeId(pub u64);
// TODO: Find and replace all `NodeId(generate_uuid())` with `NodeId::new()`.
impl NodeId {
pub fn new() -> Self {
Self(generate_uuid())
}
}
impl core::fmt::Display for NodeId {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "{}", self.0)
}
}
/// Hash two IDs together, returning a new ID that is always consistent for two input IDs in a specific order.
/// This is used during [`NodeNetwork::flatten`] in order to ensure consistent yet non-conflicting IDs for inner networks.
fn merge_ids(a: NodeId, b: NodeId) -> NodeId {

View File

@ -1,15 +1,16 @@
use super::DocumentNode;
use crate::document::NodeId;
pub use crate::imaginate_input::{ImaginateCache, ImaginateController, ImaginateMaskStartingFill, ImaginateSamplingMethod};
use crate::proto::{Any as DAny, FutureAny};
use crate::wasm_application_io::WasmEditorApi;
use graphene_core::raster::brush_cache::BrushCache;
use graphene_core::raster::{BlendMode, LuminanceCalculation};
use graphene_core::{Color, MemoHash, Node, Type};
use dyn_any::DynAny;
pub use dyn_any::StaticType;
use graphene_core::raster::brush_cache::BrushCache;
use graphene_core::raster::{BlendMode, LuminanceCalculation};
use graphene_core::renderer::RenderMetadata;
use graphene_core::uuid::NodeId;
use graphene_core::{Color, MemoHash, Node, Type};
pub use glam::{DAffine2, DVec2, IVec2, UVec2};
use std::fmt::Display;
use std::hash::Hash;
@ -248,14 +249,27 @@ impl<T: AsRef<U> + Sync + Send, U: Sync + Send> UpcastAsRefNode<T, U> {
}
}
#[derive(Debug, Clone, PartialEq, dyn_any::DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct RenderOutput {
pub data: RenderOutputType,
pub metadata: RenderMetadata,
}
#[derive(Debug, Clone, PartialEq, dyn_any::DynAny, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum RenderOutput {
pub enum RenderOutputType {
CanvasFrame(graphene_core::SurfaceFrame),
Svg(String),
Image(Vec<u8>),
}
impl Hash for RenderOutput {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.data.hash(state)
}
}
/// We hash the floats and so-forth despite it not being reproducible because all inputs to the node graph must be hashed otherwise the graph execution breaks (so sorry about this hack)
trait FakeHash {
fn hash<H: core::hash::Hasher>(&self, state: &mut H);

View File

@ -42,7 +42,7 @@ fn boolean_operation_node(group_of_paths: GraphicGroup, operation: BooleanOperat
fn collect_vector_data(graphic_group: &GraphicGroup) -> Vec<VectorData> {
// Ensure all non vector data in the graphic group is converted to vector data
let vector_data = graphic_group.iter().map(union_vector_data);
let vector_data = graphic_group.iter().map(|(element, _)| union_vector_data(element));
// Apply the transform from the parent graphic group
let transformed_vector_data = vector_data.map(|mut vector_data| {
vector_data.transform = graphic_group.transform * vector_data.transform;
@ -174,7 +174,15 @@ fn boolean_operation_node(group_of_paths: GraphicGroup, operation: BooleanOperat
}
// The first index is the bottom of the stack
boolean_operation_on_vector_data(&collect_vector_data(&group_of_paths), operation)
let mut boolean_operation_result = boolean_operation_on_vector_data(&collect_vector_data(&group_of_paths), operation);
let transform = boolean_operation_result.transform;
VectorData::transform(&mut boolean_operation_result, transform);
boolean_operation_result.style.set_stroke_transform(DAffine2::IDENTITY);
boolean_operation_result.transform = DAffine2::IDENTITY;
boolean_operation_result.upstream_graphic_group = Some(group_of_paths);
boolean_operation_result
}
fn to_svg_string(vector: &VectorData, transform: DAffine2) -> String {

View File

@ -1,4 +1,5 @@
pub use graph_craft::document::value::RenderOutput;
use graph_craft::document::value::RenderOutput;
pub use graph_craft::document::value::RenderOutputType;
pub use graph_craft::wasm_application_io::*;
#[cfg(target_arch = "wasm32")]
use graphene_core::application_io::SurfaceHandle;
@ -7,6 +8,7 @@ use graphene_core::application_io::{ApplicationIo, ExportFormat, RenderConfig};
use graphene_core::raster::bbox::Bbox;
use graphene_core::raster::Image;
use graphene_core::raster::ImageFrame;
use graphene_core::renderer::RenderMetadata;
use graphene_core::renderer::{format_transform_matrix, GraphicElementRendered, ImageRenderMode, RenderParams, RenderSvgSegmentList, SvgRender};
use graphene_core::transform::Footprint;
use graphene_core::Node;
@ -16,6 +18,7 @@ use graphene_core::{Color, WasmNotSend};
use base64::Engine;
#[cfg(target_arch = "wasm32")]
use glam::DAffine2;
use std::collections::HashMap;
use std::sync::Arc;
#[cfg(target_arch = "wasm32")]
use wasm_bindgen::Clamped;
@ -86,7 +89,7 @@ fn decode_image_node<'a: 'input>(data: Arc<[u8]>) -> ImageFrame<Color> {
image
}
fn render_svg(data: impl GraphicElementRendered, mut render: SvgRender, render_params: RenderParams, footprint: Footprint) -> RenderOutput {
fn render_svg(data: impl GraphicElementRendered, mut render: SvgRender, render_params: RenderParams, footprint: Footprint) -> RenderOutputType {
if !data.contains_artboard() && !render_params.hide_artboards {
render.leaf_tag("rect", |attributes| {
attributes.push("x", "0");
@ -102,42 +105,43 @@ fn render_svg(data: impl GraphicElementRendered, mut render: SvgRender, render_p
}
data.render_svg(&mut render, &render_params);
render.wrap_with_transform(footprint.transform, Some(footprint.resolution.as_dvec2()));
RenderOutput::Svg(render.svg.to_svg_string())
RenderOutputType::Svg(render.svg.to_svg_string())
}
#[cfg(feature = "vello")]
#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
async fn render_canvas(render_config: RenderConfig, data: impl GraphicElementRendered, editor: &WasmEditorApi, surface_handle: wgpu_executor::WgpuSurface) -> RenderOutput {
async fn render_canvas(render_config: RenderConfig, data: impl GraphicElementRendered, editor: &WasmEditorApi, surface_handle: wgpu_executor::WgpuSurface) -> RenderOutputType {
use graphene_core::SurfaceFrame;
if let Some(exec) = editor.application_io.as_ref().unwrap().gpu_executor() {
use vello::*;
let footprint = render_config.viewport;
let mut scene = Scene::new();
let mut child = Scene::new();
let mut context = wgpu_executor::RenderContext::default();
data.render_to_vello(&mut child, glam::DAffine2::IDENTITY, &mut context);
// TODO: Instead of applying the transform here, pass the transform during the translation to avoid the O(Nr cost
scene.append(&child, Some(kurbo::Affine::new(footprint.transform.to_cols_array())));
exec.render_vello_scene(&scene, &surface_handle, footprint.resolution.x, footprint.resolution.y, &context)
.await
.expect("Failed to render Vello scene");
} else {
let footprint = render_config.viewport;
let Some(exec) = editor.application_io.as_ref().unwrap().gpu_executor() else {
unreachable!("Attempted to render with Vello when no GPU executor is available");
}
};
use vello::*;
let mut scene = Scene::new();
let mut child = Scene::new();
let mut context = wgpu_executor::RenderContext::default();
data.render_to_vello(&mut child, glam::DAffine2::IDENTITY, &mut context);
// TODO: Instead of applying the transform here, pass the transform during the translation to avoid the O(Nr cost
scene.append(&child, Some(kurbo::Affine::new(footprint.transform.to_cols_array())));
exec.render_vello_scene(&scene, &surface_handle, footprint.resolution.x, footprint.resolution.y, &context)
.await
.expect("Failed to render Vello scene");
let frame = SurfaceFrame {
surface_id: surface_handle.window_id,
resolution: render_config.viewport.resolution,
transform: glam::DAffine2::IDENTITY,
};
RenderOutput::CanvasFrame(frame)
RenderOutputType::CanvasFrame(frame)
}
#[cfg(target_arch = "wasm32")]
@ -225,13 +229,23 @@ async fn render_node<'a: 'input, T: 'input + GraphicElementRendered + WasmNotSen
#[cfg(all(feature = "vello", target_arch = "wasm32"))]
let use_vello = use_vello && surface_handle.is_some();
let mut metadata = RenderMetadata {
footprints: HashMap::new(),
click_targets: HashMap::new(),
vector_data: HashMap::new(),
};
data.collect_metadata(&mut metadata, footprint, None);
let output_format = render_config.export_format;
match output_format {
let data = match output_format {
ExportFormat::Svg => render_svg(data, SvgRender::new(), render_params, footprint),
ExportFormat::Canvas => {
if use_vello && editor_api.application_io.as_ref().unwrap().gpu_executor().is_some() {
#[cfg(all(feature = "vello", target_arch = "wasm32"))]
return render_canvas(render_config, data, editor_api, surface_handle.unwrap()).await;
return RenderOutput {
data: render_canvas(render_config, data, editor_api, surface_handle.unwrap()).await,
metadata,
};
#[cfg(not(all(feature = "vello", target_arch = "wasm32")))]
render_svg(data, SvgRender::new(), render_params, footprint)
} else {
@ -239,5 +253,6 @@ async fn render_node<'a: 'input, T: 'input + GraphicElementRendered + WasmNotSen
}
}
_ => todo!("Non-SVG render output for {output_format:?}"),
}
};
RenderOutput { data, metadata }
}

View File

@ -1,3 +1,5 @@
use dyn_any::StaticType;
use graph_craft::document::value::RenderOutput;
use graph_craft::imaginate_input::{ImaginateController, ImaginateMaskStartingFill, ImaginateSamplingMethod};
use graph_craft::proto::{NodeConstructor, TypeErasedBox};
use graphene_core::fn_type;
@ -19,6 +21,7 @@ use graphene_core::{Node, NodeIO, NodeIOTypes};
use graphene_std::any::{ComposeTypeErased, DowncastBothNode, DynAnyNode, FutureWrapperNode, IntoTypeErasedNode};
use graphene_std::application_io::{RenderConfig, TextureFrame};
use graphene_std::raster::*;
use graphene_std::uuid::NodeId;
use graphene_std::vector::style::GradientStops;
use graphene_std::wasm_application_io::*;
use graphene_std::GraphicElement;
@ -26,7 +29,6 @@ use graphene_std::GraphicElement;
use wgpu_executor::{CommandBuffer, ShaderHandle, ShaderInputFrame, WgpuExecutor, WgpuShaderInput};
use wgpu_executor::{WgpuSurface, WindowHandle};
use dyn_any::StaticType;
use glam::{DAffine2, DVec2, UVec2};
use once_cell::sync::Lazy;
use std::collections::HashMap;
@ -794,7 +796,7 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
register_node!(graphene_core::vector::PathModify<_>, input: VectorData, params: [graphene_core::vector::VectorModification]),
register_node!(graphene_core::text::TextGeneratorNode<_, _, _>, input: &WasmEditorApi, params: [String, graphene_core::text::Font, f64]),
register_node!(graphene_std::brush::VectorPointsNode, input: VectorData, params: []),
async_node!(graphene_core::ConstructLayerNode<_, _>, input: Footprint, output: GraphicGroup, fn_params: [Footprint => GraphicGroup, Footprint => graphene_core::GraphicElement]),
async_node!(graphene_core::ConstructLayerNode<_, _, _>, input: Footprint, output: GraphicGroup, fn_params: [Footprint => GraphicGroup, Footprint => graphene_core::GraphicElement, () => Vec<NodeId>]),
register_node!(graphene_core::ToGraphicElementNode, input: graphene_core::vector::VectorData, params: []),
register_node!(graphene_core::ToGraphicElementNode, input: ImageFrame<Color>, params: []),
register_node!(graphene_core::ToGraphicElementNode, input: GraphicGroup, params: []),
@ -803,7 +805,7 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
register_node!(graphene_core::ToGraphicGroupNode, input: ImageFrame<Color>, params: []),
register_node!(graphene_core::ToGraphicGroupNode, input: GraphicGroup, params: []),
async_node!(graphene_core::ConstructArtboardNode<_, _, _, _, _, _>, input: Footprint, output: Artboard, fn_params: [Footprint => GraphicGroup, () => String, () => glam::IVec2, () => glam::IVec2, () => Color, () => bool]),
async_node!(graphene_core::AddArtboardNode<_, _>, input: Footprint, output: ArtboardGroup, fn_params: [Footprint => ArtboardGroup, Footprint => Artboard]),
async_node!(graphene_core::AddArtboardNode<_, _, _>, input: Footprint, output: ArtboardGroup, fn_params: [Footprint => ArtboardGroup, Footprint => Artboard, () => Vec<NodeId>]),
];
let mut map: HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeConstructor>> = HashMap::new();
for (id, c, types) in node_types.into_iter().flatten() {