New node: Pointer Position (#3535)

* New node: Pointer Position

* Fix test
This commit is contained in:
Keavon Chambers 2025-12-27 16:02:23 -08:00 committed by GitHub
parent 4ab75c9c1c
commit fa45efa9e2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 119 additions and 18 deletions

View File

@ -383,6 +383,16 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> 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<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
timing_information,
node_to_inspect,
true,
pointer_position,
) {
responses.add_front(message);
}
@ -1054,13 +1065,18 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> 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) => {

View File

@ -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<Message, String> {
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<NodeId>,
ignore_hash: bool,
pointer: DVec2,
) -> Result<Message, String> {
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,

View File

@ -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;

View File

@ -57,7 +57,7 @@ pub fn wrap_network_in_scope(mut network: NodeNetwork, editor_api: Arc<WasmEdito
implementation: DocumentNodeImplementation::ProtoNode(graphene_std::render_node::create_context::IDENTIFIER),
context_features: graphene_std::ContextDependencies {
extract: ContextFeatures::empty(),
inject: ContextFeatures::REAL_TIME | ContextFeatures::ANIMATION_TIME | ContextFeatures::FOOTPRINT | ContextFeatures::VARARGS,
inject: ContextFeatures::REAL_TIME | ContextFeatures::ANIMATION_TIME | ContextFeatures::POINTER | ContextFeatures::FOOTPRINT | ContextFeatures::VARARGS,
},
..Default::default()
},

View File

@ -239,6 +239,7 @@ pub struct RenderConfig {
pub scale: f64,
pub export_format: ExportFormat,
pub time: TimingInformation,
pub pointer: DVec2,
#[serde(alias = "view_mode")]
pub render_mode: RenderMode,
pub hide_artboards: bool,

View File

@ -1,4 +1,5 @@
use crate::transform::Footprint;
use glam::DVec2;
pub use no_std_types::context::{ArcCtx, Ctx};
use std::any::Any;
use std::borrow::Borrow;
@ -26,6 +27,10 @@ pub trait ExtractAnimationTime {
fn try_animation_time(&self) -> Option<f64>;
}
pub trait ExtractPointer {
fn try_pointer(&self) -> Option<DVec2>;
}
pub trait ExtractIndex {
fn try_index(&self) -> Option<impl Iterator<Item = usize>>;
}
@ -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<T: ?Sized + ExtractFootprint + ExtractIndex + ExtractRealTime + ExtractAnimationTime + ExtractVarArgs> ExtractAll for T {}
impl<T: ?Sized + ExtractFootprint + ExtractIndex + ExtractRealTime + ExtractAnimationTime + ExtractPointer + ExtractVarArgs> ExtractAll for T {}
impl<T: Ctx> InjectFootprint for T {}
impl<T: Ctx> InjectRealTime for T {}
impl<T: Ctx> InjectIndex for T {}
impl<T: Ctx> InjectAnimationTime for T {}
impl<T: Ctx> InjectPointer for T {}
impl<T: Ctx> InjectVarArgs for T {}
impl<T: Ctx + InjectFootprint + ExtractFootprint> ModifyFootprint for T {}
impl<T: Ctx + InjectRealTime + ExtractRealTime> ModifyRealTime for T {}
impl<T: Ctx + InjectIndex + ExtractIndex> ModifyIndex for T {}
impl<T: Ctx + InjectAnimationTime + ExtractAnimationTime> ModifyAnimationTime for T {}
impl<T: Ctx + InjectPointer + ExtractPointer> ModifyPointer for T {}
impl<T: Ctx + InjectVarArgs + ExtractVarArgs> 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<T: ExtractAnimationTime + Sync> ExtractAnimationTime for Option<T> {
self.as_ref().and_then(|x| x.try_animation_time())
}
}
impl<T: ExtractPointer + Sync> ExtractPointer for Option<T> {
fn try_pointer(&self) -> Option<DVec2> {
self.as_ref().and_then(|x| x.try_pointer())
}
}
impl<T: ExtractIndex> ExtractIndex for Option<T> {
fn try_index(&self) -> Option<impl Iterator<Item = usize>> {
self.as_ref().and_then(|x| x.try_index())
@ -211,6 +230,11 @@ impl<T: ExtractAnimationTime + Sync> ExtractAnimationTime for Arc<T> {
(**self).try_animation_time()
}
}
impl<T: ExtractPointer + Sync> ExtractPointer for Arc<T> {
fn try_pointer(&self) -> Option<DVec2> {
(**self).try_pointer()
}
}
impl<T: ExtractIndex> ExtractIndex for Arc<T> {
fn try_index(&self) -> Option<impl Iterator<Item = usize>> {
(**self).try_index()
@ -303,6 +327,11 @@ impl ExtractAnimationTime for OwnedContextImpl {
self.animation_time
}
}
impl ExtractPointer for OwnedContextImpl {
fn try_pointer(&self) -> Option<DVec2> {
self.pointer
}
}
impl ExtractIndex for OwnedContextImpl {
fn try_index(&self) -> Option<impl Iterator<Item = usize>> {
self.index.clone().map(|x| x.into_iter().rev())
@ -363,6 +392,7 @@ pub struct OwnedContextImpl {
index: Option<Vec<usize>>,
real_time: Option<f64>,
animation_time: Option<f64>,
pointer: Option<DVec2>,
}
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<T: ExtractAll + CloneVarArgs>(value: T) -> Self {
OwnedContextImpl::from_flags(value, ContextFeatures::all())
}
#[track_caller]
pub fn from_flags<T: ExtractAll + CloneVarArgs>(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<dyn AnyHash + Send + Sync>) -> Self {
assert!(self.varargs.is_none_or(|value| value.is_empty()));
self.varargs = Some(Arc::new([value]));

View File

@ -1112,11 +1112,11 @@ impl<Upstream> Vector<Upstream> {
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<Upstream> Vector<Upstream> {
}
}
return FaceIterator::new(faces, self);
FaceIterator::new(faces, self)
}
fn construct_face(&self, adjacency: &Vec<Vec<FaceSide>>, first: FaceSide, faces: &mut Faces, seen: &mut FaceSideSet) -> Option<()> {
fn construct_face(&self, adjacency: &[Vec<FaceSide>], 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 {

View File

@ -414,11 +414,13 @@ fn parse_context_feature_idents(ty: &Type) -> Vec<Ident> {
"ExtractFootprint"
| "ExtractRealTime"
| "ExtractAnimationTime"
| "ExtractPointer"
| "ExtractIndex"
| "ExtractVarArgs"
| "InjectFootprint"
| "InjectRealTime"
| "InjectAnimationTime"
| "InjectPointer"
| "InjectIndex"
| "InjectVarArgs" => {
features.push(segment.ident.clone());

View File

@ -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 {

View File

@ -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();

View File

@ -471,12 +471,41 @@ fn ceiling<T: num_traits::float::Float>(
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<T: num_traits::sign::Signed>(
fn absolute_value<T: AbsoluteValue>(
_: impl Ctx,
/// The number to be made positive.
#[implementations(f64, f32, i32, i64)]
#[implementations(f64, f32, i32, i64, DVec2)]
value: T,
) -> T {
value.abs()