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
28 changed files with 1003 additions and 670 deletions

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;
}