Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement footprint based artboard clipping #1914

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -651,7 +651,10 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
NodeInput::value(
TaggedValue::Footprint(Footprint {
transform: DAffine2::from_scale_angle_translation(DVec2::new(100., 100.), 0., DVec2::new(0., 0.)),
resolution: UVec2::new(100, 100),
clip: raster::bbox::AxisAlignedBbox {
start: DVec2::ZERO,
end: DVec2::new(100., 100.),
},
..Default::default()
}),
false,
Expand Down Expand Up @@ -2062,7 +2065,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
DocumentNode {
manual_composition: Some(concrete!(Footprint)),
inputs: vec![NodeInput::node(NodeId(1), 0)],
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::memo::ImpureMemoNode<_, _, _>")),
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::memo::MemoNode<_, _>")),
..Default::default()
},
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use graph_craft::document::value::TaggedValue;
use graph_craft::document::{DocumentNode, NodeId, NodeInput};
use graph_craft::imaginate_input::{ImaginateSamplingMethod, ImaginateServerStatus, ImaginateStatus};
use graphene_core::memo::IORecord;
use graphene_core::raster::bbox::AxisAlignedBbox;
use graphene_core::raster::{
BlendMode, CellularDistanceFunction, CellularReturnType, Color, DomainWarpType, FractalType, ImageFrame, LuminanceCalculation, NoiseType, RedGreenBlue, RedGreenBlueAlpha, RelativeAbsolute,
SelectiveColorChoice,
Expand Down Expand Up @@ -144,7 +145,7 @@ fn footprint_widget(document_node: &DocumentNode, node_id: NodeId, index: usize)
if let Some(&TaggedValue::Footprint(footprint)) = &document_node.inputs[index].as_non_exposed_value() {
let top_left = footprint.transform.transform_point2(DVec2::ZERO);
let bounds = footprint.scale();
let oversample = footprint.resolution.as_dvec2() / bounds;
let oversample = footprint.clip.size() / bounds;

location_widgets.extend_from_slice(&[
NumberInput::new(Some(top_left.x))
Expand All @@ -159,7 +160,7 @@ fn footprint_widget(document_node: &DocumentNode, node_id: NodeId, index: usize)

let footprint = Footprint {
transform: DAffine2::from_scale_angle_translation(scale, 0., offset),
resolution: (oversample * scale).as_uvec2(),
clip: AxisAlignedBbox::from_size(oversample * scale),
..footprint
};

Expand All @@ -183,7 +184,7 @@ fn footprint_widget(document_node: &DocumentNode, node_id: NodeId, index: usize)

let footprint = Footprint {
transform: DAffine2::from_scale_angle_translation(scale, 0., offset),
resolution: (oversample * scale).as_uvec2(),
clip: AxisAlignedBbox::from_size(oversample * scale),
..footprint
};

Expand All @@ -206,7 +207,7 @@ fn footprint_widget(document_node: &DocumentNode, node_id: NodeId, index: usize)

let footprint = Footprint {
transform: DAffine2::from_scale_angle_translation(scale, 0., offset),
resolution: (oversample * scale).as_uvec2(),
clip: AxisAlignedBbox::from_size(oversample * scale),
..footprint
};

Expand All @@ -227,7 +228,7 @@ fn footprint_widget(document_node: &DocumentNode, node_id: NodeId, index: usize)

let footprint = Footprint {
transform: DAffine2::from_scale_angle_translation(scale, 0., offset),
resolution: (oversample * scale).as_uvec2(),
clip: AxisAlignedBbox::from_size(oversample * scale),
..footprint
};

Expand All @@ -241,14 +242,15 @@ fn footprint_widget(document_node: &DocumentNode, node_id: NodeId, index: usize)
]);

resolution_widgets.push(
NumberInput::new(Some((footprint.resolution.as_dvec2() / bounds).x * 100.))
NumberInput::new(Some((footprint.clip.size() / bounds).x * 100.))
.label("Resolution")
.unit("%")
.on_update(update_value(
move |x: &NumberInput| {
let resolution = (bounds * x.value.unwrap_or(100.) / 100.).as_uvec2().max((1, 1).into()).min((4000, 4000).into());
let resolution = (bounds * x.value.unwrap_or(100.) / 100.).max((1., 1.).into()).min((4000., 4000.).into());
let clip = AxisAlignedBbox::from_size(resolution);

let footprint = Footprint { resolution, ..footprint };
let footprint = Footprint { clip, ..footprint };
TaggedValue::Footprint(footprint)
},
node_id,
Expand Down
10 changes: 8 additions & 2 deletions editor/src/node_graph_executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use graph_craft::proto::GraphErrors;
use graph_craft::wasm_application_io::EditorPreferences;
use graphene_core::application_io::{NodeGraphUpdateMessage, NodeGraphUpdateSender, RenderConfig};
use graphene_core::memo::IORecord;
use graphene_core::raster::bbox::AxisAlignedBbox;
use graphene_core::raster::ImageFrame;
use graphene_core::renderer::{ClickTarget, GraphicElementRendered, ImageRenderMode, RenderParams, SvgRender};
use graphene_core::renderer::{RenderSvgSegmentList, SvgSegment};
Expand Down Expand Up @@ -479,7 +480,7 @@ impl NodeGraphExecutor {
let render_config = RenderConfig {
viewport: Footprint {
transform: document.metadata().document_to_viewport,
resolution: viewport_resolution,
clip: AxisAlignedBbox::from_size(viewport_resolution.as_dvec2()),
..Default::default()
},
#[cfg(any(feature = "resvg", feature = "vello"))]
Expand Down Expand Up @@ -516,7 +517,7 @@ impl NodeGraphExecutor {
let render_config = RenderConfig {
viewport: Footprint {
transform: transform * DAffine2::from_scale(DVec2::splat(export_config.scale_factor)),
resolution: (size * export_config.scale_factor).as_uvec2(),
clip: AxisAlignedBbox::from_size(size * export_config.scale_factor),
..Default::default()
},
export_format: graphene_core::application_io::ExportFormat::Svg,
Expand Down Expand Up @@ -611,6 +612,11 @@ impl NodeGraphExecutor {
document.network_interface.document_metadata_mut().update_from_monitor(HashMap::new(), HashMap::new());
log::trace!("{e}");

responses.add(NodeGraphMessage::UpdateTypes {
resolved_types: ResolvedDocumentNodeTypesDelta::default(),
node_graph_errors,
});
responses.add(NodeGraphMessage::SendGraph);
return Err("Node graph evaluation failed".to_string());
}
Ok(result) => result,
Expand Down
32 changes: 30 additions & 2 deletions node-graph/gcore/src/graphic_element.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::application_io::TextureFrame;
use crate::raster::bbox::AxisAlignedBbox;
use crate::raster::{BlendMode, ImageFrame};
use crate::transform::{Footprint, Transform, TransformMut};
use crate::vector::VectorData;
Expand Down Expand Up @@ -221,8 +222,35 @@ async fn construct_artboard(
background: Color,
clip: bool,
) -> Artboard {
footprint.transform *= DAffine2::from_translation(location.as_dvec2());
let graphic_group = self.contents.eval(footprint).await;
let mut new_footprint = footprint;

let viewport_bounds = footprint.viewport_bounds_in_local_space();
let artboard_bounds = AxisAlignedBbox {
start: location.as_dvec2(),
end: (location + dimensions).as_dvec2(),
};
let intersection = viewport_bounds.intersect(&artboard_bounds);
let offset = intersection.start;
let scale = footprint.scale();
// let intersection = intersection.transformed(footprint.transform);
let resolution = (scale * intersection.size()).as_uvec2();
log::debug!("intersection: {intersection:?}");
log::debug!("offset: {offset:?}, resolution: {resolution:?}");

if clip {
new_footprint = Footprint {
transform: DAffine2::IDENTITY,
clip: intersection,
..footprint
};
}

let mut graphic_group = self.contents.eval(new_footprint).await;

if clip {
let mut data_transform = graphic_group.transform_mut();
// *data_transform = DAffine2::from_translation(offset) * *data_transform;
}

Artboard {
graphic_group,
Expand Down
29 changes: 27 additions & 2 deletions node-graph/gcore/src/raster/bbox.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,31 @@
use dyn_any::{DynAny, StaticType};
use glam::{DAffine2, DVec2};
use glam::{DAffine2, DVec2, Vec2Swizzles};

#[cfg_attr(not(target_arch = "spirv"), derive(Debug))]
#[derive(Clone, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Copy, dyn_any::DynAny, PartialEq)]
pub struct AxisAlignedBbox {
pub start: DVec2,
pub end: DVec2,
}

impl core::hash::Hash for AxisAlignedBbox {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.start.x.to_bits().hash(state);
self.start.y.to_bits().hash(state);
self.end.x.to_bits().hash(state);
self.end.y.to_bits().hash(state);
}
}

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 from_size(size: DVec2) -> Self {
Self { start: DVec2::ZERO, end: size }
}

pub fn size(&self) -> DVec2 {
self.end - self.start
}
Expand All @@ -28,12 +42,14 @@ impl AxisAlignedBbox {
other.start.x <= self.end.x && other.end.x >= self.start.x && other.start.y <= self.end.y && other.end.y >= self.start.y
}

#[must_use]
pub fn union(&self, other: &AxisAlignedBbox) -> AxisAlignedBbox {
AxisAlignedBbox {
start: DVec2::new(self.start.x.min(other.start.x), self.start.y.min(other.start.y)),
end: DVec2::new(self.end.x.max(other.end.x), self.end.y.max(other.end.y)),
}
}
#[must_use]
pub fn union_non_empty(&self, other: &AxisAlignedBbox) -> Option<AxisAlignedBbox> {
match (self.size() == DVec2::ZERO, other.size() == DVec2::ZERO) {
(true, true) => None,
Expand All @@ -46,12 +62,21 @@ impl AxisAlignedBbox {
}
}

#[must_use]
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)),
}
}

#[must_use]
pub fn transformed(&self, transform: DAffine2) -> Self {
Self {
start: transform.transform_point2(self.start),
end: transform.transform_point2(self.end),
}
}
}

#[cfg_attr(not(target_arch = "spirv"), derive(Debug))]
Expand Down
20 changes: 14 additions & 6 deletions node-graph/gcore/src/transform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use dyn_any::StaticType;
use glam::DAffine2;

use glam::DVec2;
use glam::UVec2;

use crate::raster::bbox::AxisAlignedBbox;
use crate::raster::ImageFrame;
Expand Down Expand Up @@ -140,8 +141,8 @@ pub enum RenderQuality {
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,
/// Target area which is displayed on the screen
pub clip: AxisAlignedBbox,
/// 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 upstream modifications have to be ignored
Expand All @@ -152,7 +153,10 @@ impl Default for Footprint {
fn default() -> Self {
Self {
transform: DAffine2::IDENTITY,
resolution: glam::UVec2::new(1920, 1080),
clip: AxisAlignedBbox {
start: DVec2::ZERO,
end: DVec2::new(1920., 1080.),
},
quality: RenderQuality::Full,
ignore_modifications: false,
}
Expand All @@ -162,8 +166,8 @@ impl Default for Footprint {
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());
let start = inverse.transform_point2(self.clip.start);
let end = inverse.transform_point2(self.clip.end);
AxisAlignedBbox { start, end }
}

Expand All @@ -174,6 +178,10 @@ impl Footprint {
pub fn offset(&self) -> DVec2 {
self.transform.transform_point2(DVec2::ZERO)
}

pub fn resolution(&self) -> UVec2 {
self.clip.size().as_uvec2()
}
}

#[derive(Debug, Clone, Copy)]
Expand All @@ -190,7 +198,7 @@ fn cull_vector_data<T>(footprint: Footprint, vector_data: T) -> T {
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)
self.clip.hash(state)
}
}

Expand Down
9 changes: 8 additions & 1 deletion node-graph/gstd/src/raster.rs
Original file line number Diff line number Diff line change
Expand Up @@ -643,6 +643,7 @@ fn noise_pattern(

let mut size = viewport_bounds.size();
let mut offset = viewport_bounds.start;
log::debug!("size: {size:?}, offset: {offset:?}");
if clip {
// TODO: Remove "clip" entirely (and its arbitrary 100x100 clipping square) once we have proper resolution-aware layer clipping
const CLIPPING_SQUARE_SIZE: f64 = 100.;
Expand All @@ -655,12 +656,17 @@ fn noise_pattern(

// If the image would not be visible, return an empty image
if size.x <= 0. || size.y <= 0. {
log::debug!("empty size, aborting");
return ImageFrame::empty();
}

let footprint_scale = footprint.scale();
let width = (size.x * footprint_scale.x) as u32;
let height = (size.y * footprint_scale.y) as u32;
log::debug!("w: {width} h: {height}");
// log::debug!("resolution: {:?}", footprint.resolution());
// let width = footprint.resolution().x;
// let height = footprint.resolution().y;

// All
let mut image = Image::new(width, height, Color::from_luminance(0.5));
Expand Down Expand Up @@ -761,10 +767,11 @@ fn noise_pattern(
}
}

log::debug!("clip: {:?}", footprint.clip);
// Return the coherent noise image
ImageFrame::<Color> {
image,
transform: DAffine2::from_translation(offset) * DAffine2::from_scale(size),
transform: DAffine2::from_translation(footprint.clip.start) * DAffine2::from_scale(footprint.clip.size() * footprint.scale()),
alpha_blending: AlphaBlending::default(),
}
}
Expand Down
13 changes: 7 additions & 6 deletions node-graph/gstd/src/wasm_application_io.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,8 @@ fn render_svg(data: impl GraphicElementRendered, mut render: SvgRender, render_p
render.leaf_tag("rect", |attributes| {
attributes.push("x", "0");
attributes.push("y", "0");
attributes.push("width", footprint.resolution.x.to_string());
attributes.push("height", footprint.resolution.y.to_string());
attributes.push("width", footprint.resolution().x.to_string());
attributes.push("height", footprint.resolution().y.to_string());
let matrix = format_transform_matrix(footprint.transform.inverse());
if !matrix.is_empty() {
attributes.push("transform", matrix);
Expand All @@ -102,7 +102,7 @@ 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()));
render.wrap_with_transform(footprint.transform, Some(footprint.clip.size()));

RenderOutput::Svg(render.svg.to_svg_string())
}
Expand All @@ -125,16 +125,17 @@ async fn render_canvas(render_config: RenderConfig, data: impl GraphicElementRen

// 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())));
let resolution = footprint.resolution();

exec.render_vello_scene(&scene, &surface_handle, footprint.resolution.x, footprint.resolution.y, &context)
exec.render_vello_scene(&scene, &surface_handle, resolution.x, resolution.y, &context)
.await
.expect("Failed to render Vello scene");
} else {
unreachable!("Attempted to render with Vello when no GPU executor is available");
}
let frame = SurfaceFrame {
surface_id: surface_handle.window_id,
resolution: render_config.viewport.resolution,
resolution: render_config.viewport.resolution(),
transform: glam::DAffine2::IDENTITY,
};
RenderOutput::CanvasFrame(frame)
Expand All @@ -161,7 +162,7 @@ async fn rasterize<_T: GraphicElementRendered + graphene_core::transform::Transf
}
let aabb = Bbox::from_transform(footprint.transform).to_axis_aligned_bbox();
let size = aabb.size();
let resolution = footprint.resolution;
let resolution = footprint.resolution();
let render_params = RenderParams {
culling_bounds: None,
..Default::default()
Expand Down
Loading
Loading