mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 22:28:10 +08:00
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:
@@ -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(),
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user