mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-16 23:08:05 +08:00
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:
@@ -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))]
|
||||
|
||||
195
node-graph/gcore/src/graphic_element/renderer.rs
Normal file
195
node-graph/gcore/src/graphic_element/renderer.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
51
node-graph/gcore/src/graphic_element/renderer/quad.rs
Normal file
51
node-graph/gcore/src/graphic_element/renderer/quad.rs
Normal 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)))
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
|
||||
@@ -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| {
|
||||
|
||||
Reference in New Issue
Block a user