From 2101d0ecef4416d70194d750585e8a81538dec10 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Sat, 27 Dec 2025 00:47:31 -0800 Subject: [PATCH 1/2] New node: Pointer Position --- .../portfolio/portfolio_message_handler.rs | 18 ++++++- editor/src/node_graph_executor.rs | 9 +++- node-graph/interpreted-executor/src/util.rs | 2 +- .../libraries/application-io/src/lib.rs | 1 + .../libraries/core-types/src/context.rs | 49 +++++++++++++++++-- .../src/vector/vector_attributes.rs | 11 ++--- node-graph/node-macro/src/parsing.rs | 2 + node-graph/nodes/gcore/src/animation.rs | 9 +++- node-graph/nodes/gstd/src/render_node.rs | 1 + node-graph/nodes/math/src/lib.rs | 33 ++++++++++++- 10 files changed, 118 insertions(+), 17 deletions(-) diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index fb7c52687e..2a3e15b0db 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -383,6 +383,16 @@ impl MessageHandler> for Portfolio for document_id in self.document_ids.iter() { let node_to_inspect = self.node_to_inspect(); + let Some(document) = self.documents.get_mut(document_id) else { + log::error!("Tried to render non-existent document"); + continue; + }; + + let document_to_viewport = document + .navigation_handler + .calculate_offset_transform(viewport.center_in_viewport_space().into(), &document.document_ptz); + let pointer_position = document_to_viewport.inverse().transform_point2(ipp.mouse.position); + let scale = viewport.scale(); // Use exact physical dimensions from browser (via ResizeObserver's devicePixelContentBoxSize) let physical_resolution = viewport.size().to_physical().into_dvec2().round().as_uvec2(); @@ -395,6 +405,7 @@ impl MessageHandler> for Portfolio timing_information, node_to_inspect, true, + pointer_position, ) { responses.add_front(message); } @@ -1054,13 +1065,18 @@ impl MessageHandler> for Portfolio return; }; + let document_to_viewport = document + .navigation_handler + .calculate_offset_transform(viewport.center_in_viewport_space().into(), &document.document_ptz); + let pointer_position = document_to_viewport.inverse().transform_point2(ipp.mouse.position); + let scale = viewport.scale(); // Use exact physical dimensions from browser (via ResizeObserver's devicePixelContentBoxSize) let physical_resolution = viewport.size().to_physical().into_dvec2().round().as_uvec2(); let result = self .executor - .submit_node_graph_evaluation(document, document_id, physical_resolution, scale, timing_information, node_to_inspect, ignore_hash); + .submit_node_graph_evaluation(document, document_id, physical_resolution, scale, timing_information, node_to_inspect, ignore_hash, pointer_position); match result { Err(description) => { diff --git a/editor/src/node_graph_executor.rs b/editor/src/node_graph_executor.rs index fb9b2eb37d..0d60d4cb83 100644 --- a/editor/src/node_graph_executor.rs +++ b/editor/src/node_graph_executor.rs @@ -1,6 +1,6 @@ use crate::messages::frontend::utility_types::{ExportBounds, FileType}; use crate::messages::prelude::*; -use glam::{DAffine2, UVec2}; +use glam::{DAffine2, DVec2, UVec2}; use graph_craft::document::value::{RenderOutput, TaggedValue}; use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput}; use graph_craft::proto::GraphErrors; @@ -81,6 +81,7 @@ impl NodeGraphExecutor { }; (node_runtime, node_executor) } + /// Execute the network by flattening it and creating a borrow stack. fn queue_execution(&mut self, render_config: RenderConfig) -> u64 { let execution_id = self.current_execution_id; @@ -140,6 +141,7 @@ impl NodeGraphExecutor { viewport_resolution: UVec2, viewport_scale: f64, time: TimingInformation, + pointer: DVec2, ) -> Result { let viewport = Footprint { transform: document.metadata().document_to_viewport, @@ -150,6 +152,7 @@ impl NodeGraphExecutor { viewport, scale: viewport_scale, time, + pointer, export_format: graphene_std::application_io::ExportFormat::Raster, render_mode: document.render_mode, hide_artboards: false, @@ -175,9 +178,10 @@ impl NodeGraphExecutor { time: TimingInformation, node_to_inspect: Option, ignore_hash: bool, + pointer: DVec2, ) -> Result { self.update_node_graph(document, node_to_inspect, ignore_hash)?; - self.submit_current_node_graph_evaluation(document, document_id, viewport_resolution, viewport_scale, time) + self.submit_current_node_graph_evaluation(document, document_id, viewport_resolution, viewport_scale, time, pointer) } /// Evaluates a node graph for export @@ -208,6 +212,7 @@ impl NodeGraphExecutor { }, scale: export_config.scale_factor, time: Default::default(), + pointer: DVec2::ZERO, export_format, render_mode: document.render_mode, hide_artboards: export_config.transparent_background, diff --git a/node-graph/interpreted-executor/src/util.rs b/node-graph/interpreted-executor/src/util.rs index 21704e3125..b3609fc59d 100644 --- a/node-graph/interpreted-executor/src/util.rs +++ b/node-graph/interpreted-executor/src/util.rs @@ -57,7 +57,7 @@ pub fn wrap_network_in_scope(mut network: NodeNetwork, editor_api: Arc Option; } +pub trait ExtractPointer { + fn try_pointer(&self) -> Option; +} + pub trait ExtractIndex { fn try_index(&self) -> Option>; } @@ -47,6 +52,7 @@ pub trait CloneVarArgs: ExtractVarArgs { pub trait InjectFootprint {} pub trait InjectRealTime {} pub trait InjectAnimationTime {} +pub trait InjectPointer {} pub trait InjectIndex {} pub trait InjectVarArgs {} @@ -54,23 +60,26 @@ pub trait InjectVarArgs {} pub trait ModifyFootprint: ExtractFootprint + InjectFootprint {} pub trait ModifyRealTime: ExtractRealTime + InjectRealTime {} pub trait ModifyAnimationTime: ExtractAnimationTime + InjectAnimationTime {} +pub trait ModifyPointer: ExtractPointer + InjectPointer {} pub trait ModifyIndex: ExtractIndex + InjectIndex {} pub trait ModifyVarArgs: ExtractVarArgs + InjectVarArgs {} -pub trait ExtractAll: ExtractFootprint + ExtractIndex + ExtractRealTime + ExtractAnimationTime + ExtractVarArgs {} +pub trait ExtractAll: ExtractFootprint + ExtractIndex + ExtractRealTime + ExtractAnimationTime + ExtractPointer + ExtractVarArgs {} -impl ExtractAll for T {} +impl ExtractAll for T {} impl InjectFootprint for T {} impl InjectRealTime for T {} impl InjectIndex for T {} impl InjectAnimationTime for T {} +impl InjectPointer for T {} impl InjectVarArgs for T {} impl ModifyFootprint for T {} impl ModifyRealTime for T {} impl ModifyIndex for T {} impl ModifyAnimationTime for T {} +impl ModifyPointer for T {} impl ModifyVarArgs for T {} // Public enum for flexible node macro codegen @@ -79,11 +88,13 @@ pub enum ContextFeature { ExtractFootprint, ExtractRealTime, ExtractAnimationTime, + ExtractPointer, ExtractIndex, ExtractVarArgs, InjectFootprint, InjectRealTime, InjectAnimationTime, + InjectPointer, InjectIndex, InjectVarArgs, } @@ -96,8 +107,9 @@ bitflags! { const FOOTPRINT = 1 << 0; const REAL_TIME = 1 << 1; const ANIMATION_TIME = 1 << 2; - const INDEX = 1 << 3; - const VARARGS = 1 << 4; + const POINTER = 1 << 3; + const INDEX = 1 << 4; + const VARARGS = 1 << 5; } } @@ -116,6 +128,7 @@ impl From<&[ContextFeature]> for ContextDependencies { ContextFeature::ExtractFootprint => ContextFeatures::FOOTPRINT, ContextFeature::ExtractRealTime => ContextFeatures::REAL_TIME, ContextFeature::ExtractAnimationTime => ContextFeatures::ANIMATION_TIME, + ContextFeature::ExtractPointer => ContextFeatures::POINTER, ContextFeature::ExtractIndex => ContextFeatures::INDEX, ContextFeature::ExtractVarArgs => ContextFeatures::VARARGS, _ => ContextFeatures::empty(), @@ -124,6 +137,7 @@ impl From<&[ContextFeature]> for ContextDependencies { ContextFeature::InjectFootprint => ContextFeatures::FOOTPRINT, ContextFeature::InjectRealTime => ContextFeatures::REAL_TIME, ContextFeature::InjectAnimationTime => ContextFeatures::ANIMATION_TIME, + ContextFeature::InjectPointer => ContextFeatures::POINTER, ContextFeature::InjectIndex => ContextFeatures::INDEX, ContextFeature::InjectVarArgs => ContextFeatures::VARARGS, _ => ContextFeatures::empty(), @@ -174,6 +188,11 @@ impl ExtractAnimationTime for Option { self.as_ref().and_then(|x| x.try_animation_time()) } } +impl ExtractPointer for Option { + fn try_pointer(&self) -> Option { + self.as_ref().and_then(|x| x.try_pointer()) + } +} impl ExtractIndex for Option { fn try_index(&self) -> Option> { self.as_ref().and_then(|x| x.try_index()) @@ -211,6 +230,11 @@ impl ExtractAnimationTime for Arc { (**self).try_animation_time() } } +impl ExtractPointer for Arc { + fn try_pointer(&self) -> Option { + (**self).try_pointer() + } +} impl ExtractIndex for Arc { fn try_index(&self) -> Option> { (**self).try_index() @@ -303,6 +327,11 @@ impl ExtractAnimationTime for OwnedContextImpl { self.animation_time } } +impl ExtractPointer for OwnedContextImpl { + fn try_pointer(&self) -> Option { + self.pointer + } +} impl ExtractIndex for OwnedContextImpl { fn try_index(&self) -> Option> { self.index.clone().map(|x| x.into_iter().rev()) @@ -363,6 +392,7 @@ pub struct OwnedContextImpl { index: Option>, real_time: Option, animation_time: Option, + pointer: Option, } impl std::fmt::Debug for OwnedContextImpl { @@ -374,6 +404,7 @@ impl std::fmt::Debug for OwnedContextImpl { .field("index", &self.index) .field("real_time", &self.real_time) .field("animation_time", &self.animation_time) + .field("pointer", &self.pointer) .finish() } } @@ -392,6 +423,7 @@ impl Hash for OwnedContextImpl { self.index.hash(state); self.real_time.map(|x| x.to_bits()).hash(state); self.animation_time.map(|x| x.to_bits()).hash(state); + self.pointer.map(|v| (v.x.to_bits(), v.y.to_bits())).hash(state); } } @@ -400,12 +432,14 @@ impl OwnedContextImpl { pub fn from(value: T) -> Self { OwnedContextImpl::from_flags(value, ContextFeatures::all()) } + #[track_caller] pub fn from_flags(value: T, bitflags: ContextFeatures) -> Self { let footprint = bitflags.contains(ContextFeatures::FOOTPRINT).then(|| value.try_footprint().copied()).flatten(); let index = bitflags.contains(ContextFeatures::INDEX).then(|| value.try_index()).flatten(); let real_time = bitflags.contains(ContextFeatures::REAL_TIME).then(|| value.try_real_time()).flatten(); let animation_time = bitflags.contains(ContextFeatures::ANIMATION_TIME).then(|| value.try_animation_time()).flatten(); + let pointer = bitflags.contains(ContextFeatures::POINTER).then(|| value.try_pointer()).flatten(); let parent = bitflags .contains(ContextFeatures::VARARGS) .then(|| match value.varargs_len() { @@ -421,8 +455,10 @@ impl OwnedContextImpl { index: index.map(|x| x.collect()), real_time, animation_time, + pointer, } } + pub const fn empty() -> Self { OwnedContextImpl { footprint: None, @@ -431,6 +467,7 @@ impl OwnedContextImpl { index: None, real_time: None, animation_time: None, + pointer: None, } } } @@ -475,6 +512,10 @@ impl OwnedContextImpl { self.animation_time = Some(animation_time); self } + pub fn with_pointer(mut self, pointer: DVec2) -> Self { + self.pointer = Some(pointer); + self + } pub fn with_vararg(mut self, value: Box) -> Self { assert!(self.varargs.is_none_or(|value| value.is_empty())); self.varargs = Some(Arc::new([value])); diff --git a/node-graph/libraries/vector-types/src/vector/vector_attributes.rs b/node-graph/libraries/vector-types/src/vector/vector_attributes.rs index 5aaaec5da1..b8cfff2304 100644 --- a/node-graph/libraries/vector-types/src/vector/vector_attributes.rs +++ b/node-graph/libraries/vector-types/src/vector/vector_attributes.rs @@ -1112,11 +1112,11 @@ impl Vector { for neighbors in &mut adjacency { neighbors.sort_by(|a, b| { let angle = [a, b].map(|side| { - let curve = PathSeg::from(self.path_segment_from_index( + let curve = self.path_segment_from_index( self.segment_domain.start_point[side.segment_index], self.segment_domain.end_point[side.segment_index], self.segment_domain.handles[side.segment_index], - )); + ); let curve = if side.reversed { curve.reverse() } else { curve }; let tangent = curve.tangent_at_start(); tangent.y.atan2(tangent.x) @@ -1140,20 +1140,19 @@ impl Vector { } } - return FaceIterator::new(faces, self); + FaceIterator::new(faces, self) } - fn construct_face(&self, adjacency: &Vec>, first: FaceSide, faces: &mut Faces, seen: &mut FaceSideSet) -> Option<()> { + fn construct_face(&self, adjacency: &[Vec], first: FaceSide, faces: &mut Faces, seen: &mut FaceSideSet) -> Option<()> { faces.start_new_face(); let max_iterations = self.segment_domain.id.len() * 2; let mut side = first; for _iteration in 1..max_iterations { if seen.contains(side) { - log::debug!("Encountered seen side {:?}, aborting face construction", side); return None; } seen.insert(side); - faces.add_side(side.clone()); + faces.add_side(side); let next_vertex = if side.reversed { self.segment_domain.start_point[side.segment_index] } else { diff --git a/node-graph/node-macro/src/parsing.rs b/node-graph/node-macro/src/parsing.rs index 573da92185..74f547b95a 100644 --- a/node-graph/node-macro/src/parsing.rs +++ b/node-graph/node-macro/src/parsing.rs @@ -414,11 +414,13 @@ fn parse_context_feature_idents(ty: &Type) -> Vec { "ExtractFootprint" | "ExtractRealTime" | "ExtractAnimationTime" + | "ExtractPointer" | "ExtractIndex" | "ExtractVarArgs" | "InjectFootprint" | "InjectRealTime" | "InjectAnimationTime" + | "InjectPointer" | "InjectIndex" | "InjectVarArgs" => { features.push(segment.ident.clone()); diff --git a/node-graph/nodes/gcore/src/animation.rs b/node-graph/nodes/gcore/src/animation.rs index 545807f3fa..b2aca80802 100644 --- a/node-graph/nodes/gcore/src/animation.rs +++ b/node-graph/nodes/gcore/src/animation.rs @@ -1,4 +1,5 @@ -use core_types::{Ctx, ExtractAnimationTime, ExtractRealTime}; +use core_types::{Ctx, ExtractAnimationTime, ExtractPointer, ExtractRealTime}; +use glam::DVec2; const DAY: f64 = 1000. * 3600. * 24.; @@ -47,6 +48,12 @@ fn animation_time(ctx: impl Ctx + ExtractAnimationTime) -> f64 { ctx.try_animation_time().unwrap_or_default() } +/// Produces the current position of the user's pointer within the document canvas. +#[node_macro::node(category("Animation"))] +fn pointer_position(ctx: impl Ctx + ExtractPointer) -> DVec2 { + ctx.try_pointer().unwrap_or_default() +} + // TODO: These nodes require more sophisticated algorithms for giving the correct result // #[node_macro::node(category("Animation"))] // fn month(ctx: impl Ctx + ExtractRealTime) -> f64 { diff --git a/node-graph/nodes/gstd/src/render_node.rs b/node-graph/nodes/gstd/src/render_node.rs index 462ee85985..f043253a5c 100644 --- a/node-graph/nodes/gstd/src/render_node.rs +++ b/node-graph/nodes/gstd/src/render_node.rs @@ -113,6 +113,7 @@ async fn create_context<'a: 'n>( .with_footprint(footprint) .with_real_time(render_config.time.time) .with_animation_time(render_config.time.animation_time.as_secs_f64()) + .with_pointer(render_config.pointer) .with_vararg(Box::new(render_params)) .into_context(); diff --git a/node-graph/nodes/math/src/lib.rs b/node-graph/nodes/math/src/lib.rs index f478f9b2ba..2185a0b334 100644 --- a/node-graph/nodes/math/src/lib.rs +++ b/node-graph/nodes/math/src/lib.rs @@ -471,12 +471,41 @@ fn ceiling( value.ceil() } +trait AbsoluteValue { + fn abs(self) -> Self; +} +impl AbsoluteValue for DVec2 { + fn abs(self) -> Self { + DVec2::new(self.x.abs(), self.y.abs()) + } +} +impl AbsoluteValue for f32 { + fn abs(self) -> Self { + self.abs() + } +} +impl AbsoluteValue for f64 { + fn abs(self) -> Self { + self.abs() + } +} +impl AbsoluteValue for i32 { + fn abs(self) -> Self { + self.abs() + } +} +impl AbsoluteValue for i64 { + fn abs(self) -> Self { + self.abs() + } +} + /// The absolute value function (`abs`) removes the negative sign from an input value, if present. #[node_macro::node(category("Math: Numeric"))] -fn absolute_value( +fn absolute_value( _: impl Ctx, /// The number to be made positive. - #[implementations(f64, f32, i32, i64)] + #[implementations(f64, f32, i32, i64, DVec2)] value: T, ) -> T { value.abs() From 257af8d23de1976fb2936901724bca5618af03b4 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Sat, 27 Dec 2025 15:23:03 -0800 Subject: [PATCH 2/2] Fix test --- editor/src/test_utils.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/editor/src/test_utils.rs b/editor/src/test_utils.rs index d7a69d3025..675a6aea59 100644 --- a/editor/src/test_utils.rs +++ b/editor/src/test_utils.rs @@ -48,7 +48,7 @@ impl EditorTestUtils { Err(e) => return Err(format!("update_node_graph_instrumented failed\n\n{e}")), }; - if let Err(e) = exector.submit_current_node_graph_evaluation(document, DocumentId(0), UVec2::ONE, 1., Default::default()) { + if let Err(e) = exector.submit_current_node_graph_evaluation(document, DocumentId(0), UVec2::ONE, 1., Default::default(), DVec2::ZERO) { return Err(format!("submit_current_node_graph_evaluation failed\n\n{e}")); } runtime.run().await;