Lay groundwork for adaptive resolution system (#1395)

* Make transform node accept footprint as input and pass it along to its input

use f32 instead of f64 and add default to document node definition

* Add cull node

* Fix types for Transform and Cull Nodes

* Add render config struct

* Add Render Node skeleton

* Add Render Node to node_registry

* Make types macro use macro hygiene

* Place Render Node as output

* Start making DownresNode footprint aware

* Correctly calculate footprint in Transform Node

* Add cropping and resizing to downres node

* Fix Output node declaration

* Fix image transform

* Fix Vector Data rendering

* Add concept of ImageRenderMode

* Take base image size into account when calculating the final image size

* Supply viewport transform to the node graph

* Start adapting document graph to resolution agnosticism

* Make document node short circuting not shift the input index

* Apply clippy lints
This commit is contained in:
Dennis Kobert 2023-09-06 12:39:21 +02:00 committed by Keavon Chambers
parent 239ca698e5
commit d82f133514
33 changed files with 836 additions and 305 deletions

View file

@ -9,10 +9,25 @@ license = "MIT OR Apache-2.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
std = ["dyn-any", "dyn-any/std", "alloc", "glam/std", "specta", "num-traits/std", "rustybuzz"]
std = [
"dyn-any",
"dyn-any/std",
"alloc",
"glam/std",
"specta",
"num-traits/std",
"rustybuzz",
"image",
]
default = ["async", "serde", "kurbo", "log", "std", "rand_chacha", "wasm"]
log = ["dep:log"]
serde = ["dep:serde", "glam/serde", "bezier-rs/serde", "bezier-rs/serde", "base64"]
serde = [
"dep:serde",
"glam/serde",
"bezier-rs/serde",
"bezier-rs/serde",
"base64",
]
gpu = ["spirv-std", "glam/bytemuck", "dyn-any", "glam/libm"]
async = ["async-trait", "alloc"]
nightly = []
@ -46,6 +61,9 @@ glam = { version = "0.24", default-features = false, features = [
] }
node-macro = { path = "../node-macro" }
base64 = { version = "0.21", optional = true }
image = { version = "0.24", optional = true, default-features = false, features = [
"png",
] }
specta.workspace = true
specta.optional = true

View file

@ -1,6 +1,6 @@
use crate::raster::ImageFrame;
use crate::text::FontCache;
use crate::transform::{Transform, TransformMut};
use crate::transform::{Footprint, Transform, TransformMut};
use crate::{Color, Node};
use dyn_any::{StaticType, StaticTypeSized};
@ -43,7 +43,7 @@ impl<S> From<SurfaceHandleFrame<S>> for SurfaceFrame {
}
}
#[derive(Clone, Debug)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SurfaceHandle<Surface> {
pub surface_id: SurfaceId,
pub surface: Surface,
@ -53,7 +53,7 @@ unsafe impl<T: 'static> StaticType for SurfaceHandle<T> {
type Static = SurfaceHandle<T>;
}
#[derive(Clone, Debug)]
#[derive(Clone, Debug, PartialEq)]
pub struct SurfaceHandleFrame<Surface> {
pub surface_handle: Arc<SurfaceHandle<Surface>>,
pub transform: DAffine2,
@ -136,12 +136,30 @@ pub trait GetImaginatePreferences {
fn get_host_name(&self) -> &str;
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ExportFormat {
#[default]
Svg,
Png {
transparent: bool,
},
Jpeg,
Canvas,
}
#[derive(Debug, Default, Clone, Copy, PartialEq)]
pub struct RenderConfig {
pub viewport: Footprint,
pub export_format: ExportFormat,
}
pub struct EditorApi<'a, Io> {
pub image_frame: Option<ImageFrame<Color>>,
pub font_cache: &'a FontCache,
pub application_io: &'a Io,
pub node_graph_message_sender: &'a dyn NodeGraphUpdateSender,
pub imaginate_preferences: &'a dyn GetImaginatePreferences,
pub render_config: RenderConfig,
}
impl<'a, Io> Clone for EditorApi<'a, Io> {
@ -152,6 +170,7 @@ impl<'a, Io> Clone for EditorApi<'a, Io> {
application_io: self.application_io,
node_graph_message_sender: self.node_graph_message_sender,
imaginate_preferences: self.imaginate_preferences,
render_config: self.render_config,
}
}
}

View file

@ -5,6 +5,7 @@ use crate::{Color, Node};
use dyn_any::{DynAny, StaticType};
use node_macro::node_fn;
use core::future::Future;
use core::ops::{Deref, DerefMut};
use glam::IVec2;
@ -40,6 +41,20 @@ pub struct GraphicElement {
pub graphic_element_data: GraphicElementData,
}
impl Default for GraphicElement {
fn default() -> Self {
Self {
name: "".to_owned(),
blend_mode: BlendMode::Normal,
opacity: 1.,
visible: true,
locked: false,
collapsed: false,
graphic_element_data: GraphicElementData::VectorShape(Box::new(VectorData::empty())),
}
}
}
/// Some [`ArtboardData`] with some optional clipping bounds that can be exported.
/// Similar to an Inkscape page: https://media.inkscape.org/media/doc/release_notes/1.2/Inkscape_1.2.html#Page_tool
#[derive(Clone, Debug, Hash, PartialEq, DynAny)]
@ -64,7 +79,8 @@ impl Artboard {
}
}
pub struct ConstructLayerNode<Name, BlendMode, Opacity, Visible, Locked, Collapsed, Stack> {
pub struct ConstructLayerNode<GraphicElementData, Name, BlendMode, Opacity, Visible, Locked, Collapsed, Stack> {
graphic_element_data: GraphicElementData,
name: Name,
blend_mode: BlendMode,
opacity: Opacity,
@ -75,16 +91,19 @@ pub struct ConstructLayerNode<Name, BlendMode, Opacity, Visible, Locked, Collaps
}
#[node_fn(ConstructLayerNode)]
fn construct_layer<Data: Into<GraphicElementData>>(
graphic_element_data: Data,
async fn construct_layer<Data: Into<GraphicElementData>, Fut1: Future<Output = Data>, Fut2: Future<Output = GraphicGroup>>(
footprint: crate::transform::Footprint,
graphic_element_data: impl Node<crate::transform::Footprint, Output = Fut1>,
name: String,
blend_mode: BlendMode,
opacity: f32,
visible: bool,
locked: bool,
collapsed: bool,
mut stack: GraphicGroup,
mut stack: impl Node<crate::transform::Footprint, Output = Fut2>,
) -> GraphicGroup {
let graphic_element_data = self.graphic_element_data.eval(footprint).await;
let mut stack = self.stack.eval(footprint).await;
stack.push(GraphicElement {
name,
blend_mode,
@ -137,7 +156,6 @@ impl From<GraphicGroup> for GraphicElementData {
GraphicElementData::GraphicGroup(graphic_group)
}
}
impl From<Artboard> for GraphicElementData {
fn from(artboard: Artboard) -> Self {
GraphicElementData::Artboard(artboard)
@ -156,6 +174,28 @@ impl DerefMut for GraphicGroup {
}
}
/// This is a helper trait used for the Into Implementation.
/// We can't just implement this for all for which from is implemented
/// as that would conflict with the implementation for `Self`
trait ToGraphicElement: Into<GraphicElementData> {}
impl ToGraphicElement for VectorData {}
impl ToGraphicElement for ImageFrame<Color> {}
impl ToGraphicElement for Artboard {}
impl<T> From<T> for GraphicGroup
where
T: ToGraphicElement,
{
fn from(value: T) -> Self {
let element = GraphicElement {
graphic_element_data: value.into(),
..Default::default()
};
Self(vec![element])
}
}
impl GraphicGroup {
pub const EMPTY: Self = Self(Vec::new());
}

View file

@ -1,7 +1,9 @@
use crate::raster::{Image, ImageFrame};
use crate::uuid::{generate_uuid, ManipulatorGroupId};
use crate::{vector::VectorData, Artboard, Color, GraphicElementData, GraphicGroup};
use base64::Engine;
use bezier_rs::Subpath;
use image::ImageEncoder;
pub use quad::Quad;
use glam::{DAffine2, DVec2};
@ -124,16 +126,28 @@ impl Default for SvgRender {
}
}
pub enum ImageRenderMode {
BlobUrl,
Canvas,
Base64,
}
/// Static state used whilst rendering
pub struct RenderParams {
pub view_mode: crate::vector::style::ViewMode,
pub image_render_mode: ImageRenderMode,
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 }
pub fn new(view_mode: crate::vector::style::ViewMode, image_render_mode: ImageRenderMode, culling_bounds: Option<[DVec2; 2]>, thumbnail: bool) -> Self {
Self {
view_mode,
image_render_mode,
culling_bounds,
thumbnail,
}
}
}
@ -263,17 +277,48 @@ impl GraphicElementRendered for Artboard {
}
impl GraphicElementRendered for ImageFrame<Color> {
fn render_svg(&self, render: &mut SvgRender, _render_params: &RenderParams) {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
let transform: String = format_transform_matrix(self.transform * render.transform);
let uuid = generate_uuid();
render.leaf_tag("image", |attributes| {
attributes.push("width", 1.to_string());
attributes.push("height", 1.to_string());
attributes.push("preserveAspectRatio", "none");
attributes.push("transform", transform);
attributes.push("href", SvgSegment::BlobUrl(uuid))
});
render.image_data.push((uuid, self.image.clone()))
match render_params.image_render_mode {
ImageRenderMode::BlobUrl => {
render.leaf_tag("image", move |attributes| {
attributes.push("width", 1.to_string());
attributes.push("height", 1.to_string());
attributes.push("preserveAspectRatio", "none");
attributes.push("transform", transform);
attributes.push("href", SvgSegment::BlobUrl(uuid))
});
render.image_data.push((uuid, self.image.clone()))
}
ImageRenderMode::Base64 => {
let image = &self.image;
let (flat_data, _, _) = image.clone().into_flat_u8();
let mut output = Vec::new();
let encoder = image::codecs::png::PngEncoder::new(&mut output);
encoder
.write_image(&flat_data, image.width, image.height, image::ColorType::Rgba8)
.expect("failed to encode image as png");
let preamble = "data:image/png;base64,";
let mut base64_string = String::with_capacity(preamble.len() + output.len() * 4);
base64_string.push_str(preamble);
log::debug!("len: {}", image.data.len());
base64::engine::general_purpose::STANDARD.encode_string(output, &mut base64_string);
render.leaf_tag("image", |attributes| {
attributes.push("width", image.width.to_string());
attributes.push("height", image.height.to_string());
attributes.push("preserveAspectRatio", "none");
attributes.push("transform", transform);
attributes.push("href", base64_string)
});
}
ImageRenderMode::Canvas => {
todo!("Canvas rendering is not yet implemented")
}
}
}
fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]> {
let transform = self.transform * transform;

View file

@ -43,6 +43,7 @@ pub mod quantization;
use core::any::TypeId;
pub use raster::Color;
pub use types::Cow;
// pub trait Node: for<'n> NodeIO<'n> {
pub trait Node<'i, Input: 'i>: 'i {

View file

@ -616,7 +616,7 @@ fn dimensions_node<_P>(input: ImageSlice<'input, _P>) -> (u32, u32) {
}
#[cfg(feature = "alloc")]
pub use image::{CollectNode, Image, ImageFrame, ImageRefNode, MapImageSliceNode};
pub use self::image::{CollectNode, Image, ImageFrame, ImageRefNode, MapImageSliceNode};
#[cfg(feature = "alloc")]
pub(crate) mod image;

View file

@ -10,6 +10,7 @@ pub struct AxisAlignedBbox {
impl AxisAlignedBbox {
pub const ZERO: Self = Self { start: DVec2::ZERO, end: DVec2::ZERO };
pub const ONE: Self = Self { start: DVec2::ZERO, end: DVec2::ONE };
pub fn size(&self) -> DVec2 {
self.end - self.start
@ -44,6 +45,13 @@ impl AxisAlignedBbox {
}),
}
}
pub fn intersect(&self, other: &AxisAlignedBbox) -> AxisAlignedBbox {
AxisAlignedBbox {
start: DVec2::new(self.start.x.max(other.start.x), self.start.y.max(other.start.y)),
end: DVec2::new(self.end.x.min(other.end.x), self.end.y.min(other.end.y)),
}
}
}
#[cfg_attr(not(target_arch = "spirv"), derive(Debug))]

View file

@ -145,7 +145,7 @@ where
/// Flattens each channel cast to a u8
pub fn into_flat_u8(self) -> (Vec<u8>, u32, u32) {
let Image { width, height, data } = self;
assert!(data.len() == width as usize * height as usize);
assert_eq!(data.len(), width as usize * height as usize);
// Cache the last sRGB value we computed, speeds up fills.
let mut last_r = 0.;

View file

@ -1,7 +1,11 @@
use core::future::Future;
use dyn_any::StaticType;
use glam::DAffine2;
use glam::DVec2;
use crate::raster::bbox::AxisAlignedBbox;
use crate::raster::ImageFrame;
use crate::raster::Pixel;
use crate::vector::VectorData;
@ -116,7 +120,8 @@ impl TransformMut for DAffine2 {
}
#[derive(Debug, Clone, Copy)]
pub struct TransformNode<Translation, Rotation, Scale, Shear, Pivot> {
pub struct TransformNode<TransformTarget, Translation, Rotation, Scale, Shear, Pivot> {
pub(crate) transform_target: TransformTarget,
pub(crate) translate: Translation,
pub(crate) rotate: Rotation,
pub(crate) scale: Scale,
@ -124,11 +129,107 @@ pub struct TransformNode<Translation, Rotation, Scale, Shear, Pivot> {
pub(crate) pivot: Pivot,
}
#[node_macro::node_fn(TransformNode)]
pub(crate) fn transform_vector_data<Data: TransformMut>(mut data: Data, translate: DVec2, rotate: f32, scale: DVec2, shear: DVec2, pivot: DVec2) -> Data {
let pivot = DAffine2::from_translation(data.local_pivot(pivot));
#[derive(Debug, Clone, Copy, dyn_any::DynAny, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum RenderQuality {
/// Low quality, fast rendering
Preview,
/// Ensure that the render is available with at least the specified quality
/// A value of 0.5 means that the render is available with at least 50% of the final image resolution
Scale(f32),
/// Flip a coin to decide if the render should be available with the current quality or done at full quality
/// This should be used to gradually update the render quality of a cached node
Probabilty(f32),
/// Render at full quality
Full,
}
#[derive(Debug, Clone, Copy, dyn_any::DynAny, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Footprint {
/// Inverse of the transform which will be applied to the node output during the rendering process
pub transform: DAffine2,
/// Resolution of the target output area in pixels
pub resolution: glam::UVec2,
/// Quality of the render, this may be used by caching nodes to decide if the cached render is sufficient
pub quality: RenderQuality,
/// When the transform is set downstream, all upsream modifications have to be ignored
pub ignore_modifications: bool,
}
let modification = pivot * DAffine2::from_scale_angle_translation(scale, rotate as f64, translate) * DAffine2::from_cols_array(&[1., shear.y, shear.x, 1., 0., 0.]) * pivot.inverse();
impl Default for Footprint {
fn default() -> Self {
Self {
transform: DAffine2::IDENTITY,
resolution: glam::UVec2::new(1920, 1080),
quality: RenderQuality::Full,
ignore_modifications: false,
}
}
}
impl Footprint {
pub fn viewport_bounds_in_local_space(&self) -> AxisAlignedBbox {
let inverse = self.transform.inverse();
let start = inverse.transform_point2((0., 0.).into());
let end = inverse.transform_point2(self.resolution.as_dvec2());
AxisAlignedBbox { start, end }
}
}
#[derive(Debug, Clone, Copy)]
pub struct CullNode<VectorData> {
pub(crate) vector_data: VectorData,
}
#[node_macro::node_fn(CullNode)]
fn cull_vector_data<T>(footprint: Footprint, vector_data: T) -> T {
// TODO: Implement culling
vector_data
}
impl core::hash::Hash for Footprint {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.transform.to_cols_array().iter().for_each(|x| x.to_le_bytes().hash(state));
self.resolution.hash(state)
}
}
impl Transform for Footprint {
fn transform(&self) -> DAffine2 {
self.transform
}
}
impl TransformMut for Footprint {
fn transform_mut(&mut self) -> &mut DAffine2 {
&mut self.transform
}
}
#[node_macro::node_fn(TransformNode)]
pub(crate) async fn transform_vector_data<Fut: Future>(
mut footprint: Footprint,
transform_target: impl Node<Footprint, Output = Fut>,
translate: DVec2,
rotate: f32,
scale: DVec2,
shear: DVec2,
pivot: DVec2,
) -> Fut::Output
where
Fut::Output: TransformMut,
{
// TOOD: This is hack and might break for Vector data because the pivot may be incorrect
let transform = DAffine2::from_scale_angle_translation(scale, rotate as f64, translate) * DAffine2::from_cols_array(&[1., shear.y, shear.x, 1., 0., 0.]);
if !footprint.ignore_modifications {
let pivot_transform = DAffine2::from_translation(pivot);
let modification = pivot_transform * transform * pivot_transform.inverse();
*footprint.transform_mut() = footprint.transform() * modification;
}
let mut data = self.transform_target.eval(footprint).await;
let pivot_transform = DAffine2::from_translation(data.local_pivot(pivot));
let modification = pivot_transform * transform * pivot_transform.inverse();
let data_transform = data.transform_mut();
*data_transform = modification * (*data_transform);

View file

@ -26,9 +26,9 @@ impl NodeIOTypes {
#[macro_export]
macro_rules! concrete {
($type:ty) => {
Type::Concrete(TypeDescriptor {
$crate::Type::Concrete($crate::TypeDescriptor {
id: Some(core::any::TypeId::of::<$type>()),
name: Cow::Borrowed(core::any::type_name::<$type>()),
name: $crate::Cow::Borrowed(core::any::type_name::<$type>()),
size: core::mem::size_of::<$type>(),
align: core::mem::align_of::<$type>(),
})
@ -38,9 +38,9 @@ macro_rules! concrete {
#[macro_export]
macro_rules! concrete_with_name {
($type:ty, $name:expr) => {
Type::Concrete(TypeDescriptor {
$crate::Type::Concrete($crate::TypeDescriptor {
id: Some(core::any::TypeId::of::<$type>()),
name: Cow::Borrowed($name),
name: $crate::Cow::Borrowed($name),
size: core::mem::size_of::<$type>(),
align: core::mem::align_of::<$type>(),
})
@ -50,17 +50,17 @@ macro_rules! concrete_with_name {
#[macro_export]
macro_rules! generic {
($type:ty) => {{
Type::Generic(Cow::Borrowed(stringify!($type)))
$crate::Type::Generic($crate::Cow::Borrowed(stringify!($type)))
}};
}
#[macro_export]
macro_rules! fn_type {
($type:ty) => {
Type::Fn(Box::new(concrete!(())), Box::new(concrete!($type)))
$crate::Type::Fn(Box::new(concrete!(())), Box::new(concrete!($type)))
};
($in_type:ty, $type:ty) => {
Type::Fn(Box::new(concrete!(($in_type))), Box::new(concrete!($type)))
$crate::Type::Fn(Box::new(concrete!(($in_type))), Box::new(concrete!($type)))
};
}