Thumbnails for the layer node (#1210)

* Thumbnails for the layer node

* Raster node graph frames

* Downscale to a random resolution

* Cleanup and bug fixes

* Generate paths before duplicating outputs

* Fix stable id test

* Code review changes

* Code review pass with minor changes

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
0HyperCube
2023-05-18 05:12:50 +01:00
committed by Keavon Chambers
parent 727e5bdeb1
commit 6400953af5
24 changed files with 609 additions and 131 deletions

View File

@@ -8,6 +8,8 @@ use core::ops::{Deref, DerefMut};
use glam::IVec2;
use node_macro::node_fn;
pub mod renderer;
/// A list of [`GraphicElement`]s
#[derive(Clone, Debug, Hash, PartialEq, DynAny, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]

View File

@@ -0,0 +1,195 @@
use crate::raster::{Image, ImageFrame};
use crate::{uuid::generate_uuid, vector::VectorData, Artboard, Color, GraphicElementData, GraphicGroup};
use quad::Quad;
use glam::{DAffine2, DVec2};
mod quad;
/// Mutable state used whilst rendering to an SVG
pub struct SvgRender {
pub svg: SvgSegmentList,
pub svg_defs: String,
pub transform: DAffine2,
pub image_data: Vec<(u64, Image<Color>)>,
}
impl SvgRender {
pub fn new() -> Self {
Self {
svg: SvgSegmentList::default(),
svg_defs: String::new(),
transform: DAffine2::IDENTITY,
image_data: Vec::new(),
}
}
/// Add an outer `<svg />` tag with a `viewBox` and the `<defs />`
pub fn format_svg(&mut self, bounds_min: DVec2, bounds_max: DVec2) {
let (x, y) = bounds_min.into();
let (size_x, size_y) = (bounds_max - bounds_min).into();
let defs = &self.svg_defs;
let svg_header = format!(r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="{x} {y} {size_x} {size_y}"><defs>{defs}</defs>"#,);
self.svg.insert(0, svg_header.into());
self.svg.push("</svg>".into());
}
}
impl Default for SvgRender {
fn default() -> Self {
Self::new()
}
}
/// Static state used whilst rendering
pub struct RenderParams {
pub view_mode: crate::vector::style::ViewMode,
pub culling_bounds: Option<[DVec2; 2]>,
pub thumbnail: bool,
}
impl RenderParams {
pub fn new(view_mode: crate::vector::style::ViewMode, culling_bounds: Option<[DVec2; 2]>, thumbnail: bool) -> Self {
Self { view_mode, culling_bounds, thumbnail }
}
}
fn format_transform_matrix(transform: DAffine2) -> String {
transform.to_cols_array().iter().map(ToString::to_string).collect::<Vec<_>>().join(",")
}
pub trait GraphicElementRendered {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams);
fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]>;
}
impl GraphicElementRendered for GraphicGroup {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
self.iter().for_each(|element| element.graphic_element_data.render_svg(render, render_params))
}
fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]> {
self.iter().filter_map(|element| element.graphic_element_data.bounding_box(transform)).reduce(Quad::combine_bounds)
}
}
impl GraphicElementRendered for VectorData {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
let layer_bounds = self.bounding_box().unwrap_or_default();
let transformed_bounds = self.bounding_box_with_transform(render.transform).unwrap_or_default();
render.svg.push("<path d=\"".into());
let mut path = String::new();
for subpath in &self.subpaths {
let _ = subpath.subpath_to_svg(&mut path, self.transform * render.transform);
}
render.svg.push(path.into());
render.svg.push("\"".into());
let style = self.style.render(render_params.view_mode, &mut render.svg_defs, render.transform, layer_bounds, transformed_bounds);
render.svg.push(style.into());
render.svg.push("/>".into());
}
fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]> {
self.bounding_box_with_transform(self.transform * transform)
}
}
impl GraphicElementRendered for Artboard {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
self.graphic_group.render_svg(render, render_params)
}
fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]> {
let artboard_bounds = self.bounds.map(|[a, b]| (transform * Quad::from_box([a.as_dvec2(), b.as_dvec2()])).bounding_box());
[self.graphic_group.bounding_box(transform), artboard_bounds].into_iter().flatten().reduce(Quad::combine_bounds)
}
}
impl GraphicElementRendered for ImageFrame<Color> {
fn render_svg(&self, render: &mut SvgRender, _render_params: &RenderParams) {
let transform: String = format_transform_matrix(self.transform * render.transform);
render
.svg
.push(format!(r#"<image width="1" height="1" preserveAspectRatio="none" transform="matrix({transform})" href=""#).into());
let uuid = generate_uuid();
render.svg.push(SvgSegment::BlobUrl(uuid));
render.svg.push("\" />".into());
render.image_data.push((uuid, self.image.clone()))
}
fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]> {
let transform = self.transform * transform;
(transform.matrix2 != glam::DMat2::ZERO).then(|| (transform * Quad::from_box([DVec2::ZERO, DVec2::ONE])).bounding_box())
}
}
impl GraphicElementRendered for GraphicElementData {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
match self {
GraphicElementData::VectorShape(vector_data) => vector_data.render_svg(render, render_params),
GraphicElementData::ImageFrame(image_frame) => image_frame.render_svg(render, render_params),
GraphicElementData::Text(_) => todo!("Render a text GraphicElementData"),
GraphicElementData::GraphicGroup(graphic_group) => graphic_group.render_svg(render, render_params),
GraphicElementData::Artboard(artboard) => artboard.render_svg(render, render_params),
}
}
fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]> {
match self {
GraphicElementData::VectorShape(vector_data) => GraphicElementRendered::bounding_box(&**vector_data, transform),
GraphicElementData::ImageFrame(image_frame) => image_frame.bounding_box(transform),
GraphicElementData::Text(_) => todo!("Bounds of a text GraphicElementData"),
GraphicElementData::GraphicGroup(graphic_group) => graphic_group.bounding_box(transform),
GraphicElementData::Artboard(artboard) => artboard.bounding_box(transform),
}
}
}
/// A segment of an svg string to allow for embedding blob urls
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SvgSegment {
Slice(&'static str),
String(String),
BlobUrl(u64),
}
impl From<String> for SvgSegment {
fn from(value: String) -> Self {
Self::String(value)
}
}
impl From<&'static str> for SvgSegment {
fn from(value: &'static str) -> Self {
Self::Slice(value)
}
}
/// A list of [`SvgSegment`]s.
///
/// Can be modified with `list.push("hello".into())`. Use `list.to_string()` to convert the segments into one string.
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct SvgSegmentList(Vec<SvgSegment>);
impl core::ops::Deref for SvgSegmentList {
type Target = Vec<SvgSegment>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl core::ops::DerefMut for SvgSegmentList {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl core::fmt::Display for SvgSegmentList {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
for segment in self.iter() {
f.write_str(match segment {
SvgSegment::Slice(x) => x,
SvgSegment::String(x) => x,
SvgSegment::BlobUrl(_) => "<!-- Blob url not yet loaded -->",
})?;
}
Ok(())
}
}

View File

@@ -0,0 +1,51 @@
use glam::{DAffine2, DVec2};
#[derive(Debug, Clone, Default, Copy)]
/// A quad defined by four vertices.
pub struct Quad([DVec2; 4]);
impl Quad {
/// Convert a box defined by two corner points to a quad.
pub fn from_box(bbox: [DVec2; 2]) -> Self {
let size = bbox[1] - bbox[0];
Self([bbox[0], bbox[0] + size * DVec2::X, bbox[1], bbox[0] + size * DVec2::Y])
}
/// Get all the edges in the quad.
pub fn lines_glam(&self) -> impl Iterator<Item = bezier_rs::Bezier> + '_ {
[[self.0[0], self.0[1]], [self.0[1], self.0[2]], [self.0[2], self.0[3]], [self.0[3], self.0[0]]]
.into_iter()
.map(|[start, end]| bezier_rs::Bezier::from_linear_dvec2(start, end))
}
/// Generates a [crate::vector::Subpath] of the quad
pub fn subpath(&self) -> crate::vector::Subpath {
crate::vector::Subpath::from_points(self.0.into_iter(), true)
}
/// Generates the axis aligned bounding box of the quad
pub fn bounding_box(&self) -> [DVec2; 2] {
[
self.0.into_iter().reduce(|a, b| a.min(b)).unwrap_or_default(),
self.0.into_iter().reduce(|a, b| a.max(b)).unwrap_or_default(),
]
}
/// Gets the center of a quad
pub fn center(&self) -> DVec2 {
self.0.iter().sum::<DVec2>() / 4.
}
/// Take the outside bounds of two axis aligned rectangles, which are defined by two corner points.
pub fn combine_bounds(a: [DVec2; 2], b: [DVec2; 2]) -> [DVec2; 2] {
[a[0].min(b[0]), a[1].max(b[1])]
}
}
impl core::ops::Mul<Quad> for DAffine2 {
type Output = Quad;
fn mul(self, rhs: Quad) -> Self::Output {
Quad(rhs.0.map(|point| self.transform_point2(point)))
}
}

View File

@@ -37,7 +37,7 @@ mod base64_serde {
}
}
#[derive(Clone, Debug, PartialEq, Default, specta::Type)]
#[derive(Clone, PartialEq, Default, specta::Type)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Image<P: Pixel> {
pub width: u32,
@@ -46,6 +46,17 @@ pub struct Image<P: Pixel> {
pub data: Vec<P>,
}
impl<P: Pixel + Debug> Debug for Image<P> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let length = self.data.len();
f.debug_struct("Image")
.field("width", &self.width)
.field("height", &self.height)
.field("data", if length < 100 { &self.data } else { &length })
.finish()
}
}
unsafe impl<P: StaticTypeSized + Pixel> StaticType for Image<P>
where
P::Static: Pixel,

View File

@@ -243,6 +243,10 @@ impl DocumentNodeImplementation {
_ => None,
}
}
pub const fn proto(name: &'static str) -> Self {
Self::Unresolved(NodeIdentifier::new(name))
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, DynAny, specta::Type, Hash)]
@@ -812,6 +816,28 @@ impl NodeNetwork {
nodes: nodes.clone(),
})
}
/// Create a [`RecursiveNodeIter`] that iterates over all [`DocumentNode`]s, including ones that are deeply nested.
pub fn recursive_nodes(&self) -> RecursiveNodeIter {
let nodes = self.nodes.iter().map(|(id, node)| (node, self, vec![*id])).collect();
RecursiveNodeIter { nodes }
}
}
/// An iterator over all [`DocumentNode`]s, including ones that are deeply nested.
pub struct RecursiveNodeIter<'a> {
nodes: Vec<(&'a DocumentNode, &'a NodeNetwork, Vec<NodeId>)>,
}
impl<'a> Iterator for RecursiveNodeIter<'a> {
type Item = (&'a DocumentNode, &'a NodeNetwork, Vec<NodeId>);
fn next(&mut self) -> Option<Self::Item> {
let (node, network, path) = self.nodes.pop()?;
if let DocumentNodeImplementation::Network(network) = &node.implementation {
self.nodes.extend(network.nodes.iter().map(|(id, node)| (node, network, [path.as_slice(), &[*id]].concat())));
}
Some((node, network, path))
}
}
#[cfg(test)]

View File

@@ -10,7 +10,6 @@ pub struct Compiler {}
impl Compiler {
pub fn compile(&self, mut network: NodeNetwork, resolve_inputs: bool) -> impl Iterator<Item = ProtoNetwork> {
let node_ids = network.nodes.keys().copied().collect::<Vec<_>>();
network.generate_node_paths(&[]);
network.resolve_extract_nodes();
println!("flattening");
for id in node_ids {

View File

@@ -156,6 +156,7 @@ impl ProtoNode {
self.identifier.name.hash(&mut hasher);
self.construction_args.hash(&mut hasher);
self.document_node_path.hash(&mut hasher);
match self.input {
ProtoNodeInput::None => "none".hash(&mut hasher),
ProtoNodeInput::ShortCircut(ref ty) => {
@@ -629,12 +630,12 @@ mod test {
assert_eq!(
ids,
vec![
10739226043134366700,
17332796976541881019,
7897288931440576543,
7388412494950743023,
359700384277940942,
12822947441562012352
4471348669260178714,
12892313567093808068,
6883586777044498729,
13841339389284532934,
4412916056300566478,
15358108940336208665
]
);
}

View File

@@ -139,8 +139,8 @@ impl BorrowTree {
node.reset();
}
old_nodes.remove(&id);
self.source_map.retain(|_, nid| !old_nodes.contains(nid));
}
self.source_map.retain(|_, nid| !old_nodes.contains(nid));
Ok(old_nodes.into_iter().collect())
}

View File

@@ -155,6 +155,7 @@ fn node_registry() -> HashMap<NodeIdentifier, HashMap<NodeIOTypes, NodeConstruct
register_node!(graphene_std::raster::MaskImageNode<_, _, _>, input: ImageFrame<Color>, params: [ImageFrame<Luma>]),
register_node!(graphene_std::raster::EmptyImageNode<_, _>, input: DAffine2, params: [Color]),
register_node!(graphene_std::memo::MonitorNode<_>, input: ImageFrame<Color>, params: []),
register_node!(graphene_std::memo::MonitorNode<_>, input: graphene_core::GraphicGroup, params: []),
#[cfg(feature = "gpu")]
register_node!(graphene_std::executor::MapGpuSingleImageNode<_>, input: Image<Color>, params: [String]),
vec![(
@@ -366,6 +367,26 @@ fn node_registry() -> HashMap<NodeIdentifier, HashMap<NodeIOTypes, NodeConstruct
},
NodeIOTypes::new(generic!(T), concrete!(graphene_core::EditorApi), vec![value_fn!(VectorData)]),
),
(
NodeIdentifier::new("graphene_std::memo::EndLetNode<_>"),
|args| {
let input: DowncastBothNode<(), graphene_core::GraphicGroup> = DowncastBothNode::new(args[0]);
let node = graphene_std::memo::EndLetNode::new(input);
let any: DynAnyInRefNode<graphene_core::EditorApi, _, _> = graphene_std::any::DynAnyInRefNode::new(node);
any.into_type_erased()
},
NodeIOTypes::new(generic!(T), concrete!(graphene_core::EditorApi), vec![value_fn!(graphene_core::GraphicGroup)]),
),
(
NodeIdentifier::new("graphene_std::memo::EndLetNode<_>"),
|args| {
let input: DowncastBothNode<(), graphene_core::Artboard> = DowncastBothNode::new(args[0]);
let node = graphene_std::memo::EndLetNode::new(input);
let any: DynAnyInRefNode<graphene_core::EditorApi, _, _> = graphene_std::any::DynAnyInRefNode::new(node);
any.into_type_erased()
},
NodeIOTypes::new(generic!(T), concrete!(graphene_core::EditorApi), vec![value_fn!(graphene_core::Artboard)]),
),
(
NodeIdentifier::new("graphene_std::memo::RefNode<_, _>"),
|args| {