Replace the 'Vec2 to Point' node with automatic conversion machinery (#4236)

* Replace the 'Vec2 to Point' node with an 'As Vector' node and automatic conversion

* Accept a Vec2 as content for the Wrap Graphic node

* Rename the type-assertion nodes from 'To' to 'As'

* Fix the Transform node reversing a Vec2 input's direction

* Fix text migration bug
This commit is contained in:
Keavon Chambers
2026-06-14 19:35:39 -07:00
parent b56af15e5e
commit 34e0fd7757
19 changed files with 89 additions and 41 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -787,9 +787,9 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
inputs: vec![NodeInput::node(NodeId(1), 0)],
..Default::default()
},
// 3: Vec2 to Point
// 3: As Vector (auto-converts the decomposed translation into a single-anchor List<Vector>)
DocumentNode {
implementation: DocumentNodeImplementation::ProtoNode(vector_nodes::vec_2_to_point::IDENTIFIER),
implementation: DocumentNodeImplementation::ProtoNode(vector::as_vector::IDENTIFIER),
inputs: vec![NodeInput::node(NodeId(2), 0)],
..Default::default()
},
@@ -858,7 +858,7 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
},
..Default::default()
},
// 3: Vec2 to Point
// 3: As Vector
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(21, 1)),

View File

@@ -404,16 +404,16 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[
aliases: &["graphene_math_nodes::TangentInverseNode", "graphene_core::ops::TangentInverseNode"],
},
NodeReplacement {
node: graphene_std::math_nodes::to_f_64::IDENTIFIER,
aliases: &["graphene_math_nodes::ToF64Node", "graphene_core::ops::ToF64Node"],
node: graphene_std::math_nodes::as_f_64::IDENTIFIER,
aliases: &["graphene_math_nodes::ToF64Node", "graphene_core::ops::ToF64Node", "math_nodes::ToF64Node"],
},
NodeReplacement {
node: graphene_std::math_nodes::to_u_32::IDENTIFIER,
node: graphene_std::math_nodes::as_u_32::IDENTIFIER,
aliases: &["graphene_math_nodes::ToU32Node", "graphene_core::ops::ToU32Node", "math_nodes::ToU32Node"],
},
NodeReplacement {
node: graphene_std::math_nodes::to_u_64::IDENTIFIER,
aliases: &["graphene_math_nodes::ToU64Node", "graphene_core::ops::ToU64Node"],
node: graphene_std::math_nodes::as_u_64::IDENTIFIER,
aliases: &["graphene_math_nodes::ToU64Node", "graphene_core::ops::ToU64Node", "math_nodes::ToU64Node"],
},
NodeReplacement {
node: graphene_std::math_nodes::vec_2_value::IDENTIFIER,
@@ -676,8 +676,8 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[
aliases: &["graphene_core::logic::SwitchNode"],
},
NodeReplacement {
node: graphene_std::text_nodes::to_string::IDENTIFIER,
aliases: &["graphene_core::logic::ToStringNode"],
node: graphene_std::text_nodes::as_string::IDENTIFIER,
aliases: &["graphene_core::logic::ToStringNode", "text_nodes::ToStringNode"],
},
NodeReplacement {
node: graphene_std::text_nodes::json::query_json::IDENTIFIER,
@@ -958,11 +958,12 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[
aliases: &["graphene_core::vector::TangentOnPathNode"],
},
NodeReplacement {
node: graphene_std::vector::vec_2_to_point::IDENTIFIER,
node: graphene_std::vector::as_vector::IDENTIFIER,
aliases: &[
"graphene_core::vector::vector_nodes::PositionToPointNode",
"graphene_core::vector::PositionToPointNode",
"graphene_core::vector::Vec2ToPointNode",
"core_types::vector::Vec2ToPointNode",
],
},
];
@@ -1574,6 +1575,8 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
for i in 10..=12 {
document.network_interface.set_input(&InputConnector::node(*node_id, i), old_inputs[i - 2].clone(), network_path);
}
inputs_count = 13;
}
// Upgrade Sine, Cosine, and Tangent nodes to include a boolean input for whether the output should be in radians, which was previously the only option but is now not the default

View File

@@ -33,7 +33,6 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
// INTO NODES
// ==========
into_node!(from: List<Graphic>, to: List<Graphic>),
into_node!(from: List<Vector>, to: List<Vector>),
into_node!(from: List<Raster<CPU>>, to: List<Raster<CPU>>),
#[cfg(feature = "gpu")]
into_node!(from: List<Raster<GPU>>, to: List<Raster<GPU>>),
@@ -96,6 +95,8 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
#[cfg(feature = "gpu")]
into_node!(from: &PlatformEditorApi, to: &WgpuExecutor),
convert_node!(from: DVec2, to: DVec2),
convert_node!(from: List<Vector>, to: List<Vector>),
convert_node!(from: DVec2, to: List<Vector>),
convert_node!(from: String, to: String),
convert_node!(from: bool, to: String),
convert_node!(from: DVec2, to: String),

View File

@@ -106,7 +106,19 @@ impl Convert<DVec2, ()> for DVec2 {
}
}
// TODO: Add a DVec2 to List<Vector> anchor point conversion implementation to replace the 'Vec2 to Point' node
/// Constructs `Self` from a single anchor point at the given position. Implemented by the vector crate's
/// path type so the `Convert` impl below can build a single-point path without core-types depending on
/// that crate (mirroring how [`ListConvert`] bridges per-item list conversions).
pub trait FromAnchorPosition {
fn from_anchor_position(position: DVec2) -> Self;
}
// Converts a position into a vector path composed of a single anchor point
impl<T: FromAnchorPosition + Send> Convert<List<T>, ()> for DVec2 {
async fn convert(self, _: Footprint, _: ()) -> List<T> {
List::new_from_item(Item::new_from_element(T::from_anchor_position(self)))
}
}
/// Implements the [`Convert`] trait for conversion between the cartesian product of Rust's primitive numeric types.
macro_rules! impl_convert {

View File

@@ -234,6 +234,6 @@ impl ApplyTransform for DVec2 {
*self = modification.transform_point2(*self);
}
fn left_apply_transform(&mut self, modification: &DAffine2) {
*self = modification.inverse().transform_point2(*self);
*self = modification.transform_point2(*self);
}
}

View File

@@ -1,12 +1,12 @@
use core_types::bounds::{BoundingBox, RenderBoundingBox};
use core_types::graphene_hash::CacheHash;
use core_types::list::{ATTR_FILL, ATTR_STROKE, Item, List};
use core_types::ops::ListConvert;
use core_types::ops::{FromAnchorPosition, ListConvert};
use core_types::render_complexity::RenderComplexity;
use core_types::uuid::NodeId;
use core_types::{ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_GRADIENT_TYPE, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_SPREAD_METHOD, ATTR_TRANSFORM, Color};
use dyn_any::DynAny;
use glam::DAffine2;
use glam::{DAffine2, DVec2};
use raster_types::{CPU, GPU, Raster};
use std::borrow::Cow;
use vector_types::GradientStops;
@@ -393,6 +393,13 @@ impl From<DAffine2> for Graphic {
Graphic::default()
}
}
// DVec2
impl From<DVec2> for Graphic {
fn from(position: DVec2) -> Self {
Graphic::Vector(List::new_from_element(Vector::from_anchor_position(position)))
}
}
// Note: List conversions handled by blanket impl in gcore
impl Graphic {

View File

@@ -55,6 +55,23 @@ impl graphene_hash::CacheHash for Vector {
}
}
impl core_types::ops::FromAnchorPosition for Vector {
fn from_anchor_position(position: DVec2) -> Self {
let mut point_domain = PointDomain::new();
point_domain.push(PointId::generate(), position);
Self { point_domain, ..Default::default() }
}
}
// Identity item conversion so `List<Vector>` satisfies the blanket `Convert<List<U>, ()> for List<T>`, letting its
// auto-inserted input wrapper be a `ConvertNode` (which also accepts a `DVec2` anchor position) rather than an `IntoNode`.
impl core_types::ops::ListConvert<Vector> for Vector {
fn convert_item(self) -> Vector {
self
}
}
impl Vector {
/// Add a subpath to this vector path.
pub fn append_subpath(&mut self, subpath: impl Borrow<Subpath<PointId>>, preserve_id: bool) {
@@ -619,4 +636,17 @@ mod tests {
let generated = vector.stroke_bezier_paths().collect::<Vec<_>>();
assert_subpath_eq(&generated, &[curve, circle]);
}
// Verifies the `DVec2 -> List<Vector>` conversion that replaced the former "Vec2 to Point" node yields a path
// with exactly one anchor point at the given position and no segments
#[test]
fn anchor_position_builds_single_point_path() {
use core_types::ops::FromAnchorPosition;
let vector = Vector::from_anchor_position(DVec2::new(3., 4.));
assert_eq!(vector.point_domain.positions(), [DVec2::new(3., 4.)]);
assert_eq!(vector.point_domain.ids().len(), 1);
assert!(vector.segment_domain.ids().is_empty());
}
}

View File

@@ -549,6 +549,7 @@ pub async fn wrap_graphic<T: Into<Graphic> + 'n>(
List<Color>,
List<GradientStops>,
DAffine2,
DVec2,
)]
content: T,
) -> List<Graphic> {

View File

@@ -421,22 +421,22 @@ fn random(
// TODO: Test that these are no longer needed in all circumstances, then remove them and add a migration to convert these into Passthrough nodes. Note: these act more as type annotations than as identity functions.
/// Convert a number to an integer of the type u32, which may be the required type for certain node inputs.
#[node_macro::node(name("To u32"), category("Debug"))]
fn to_u32(_: impl Ctx, value: u32) -> u32 {
#[node_macro::node(name("As u32"), category("Debug"))]
fn as_u32(_: impl Ctx, value: u32) -> u32 {
value
}
// TODO: Test that these are no longer needed in all circumstances, then remove them and add a migration to convert these into Passthrough nodes. Note: these act more as type annotations than as identity functions.
/// Convert a number to an integer of the type u64, which may be the required type for certain node inputs.
#[node_macro::node(name("To u64"), category("Debug"))]
fn to_u64(_: impl Ctx, value: u64) -> u64 {
#[node_macro::node(name("As u64"), category("Debug"))]
fn as_u64(_: impl Ctx, value: u64) -> u64 {
value
}
// TODO: Test that these are no longer needed in all circumstances, then remove them and add a migration to convert these into Passthrough nodes. Note: these act more as type annotations than as identity functions.
/// Convert an integer to a decimal number of the type f64, which may be the required type for certain node inputs.
#[node_macro::node(name("To f64"), category("Debug"))]
fn to_f64(_: impl Ctx, value: f64) -> f64 {
#[node_macro::node(name("As f64"), category("Debug"))]
fn as_f64(_: impl Ctx, value: f64) -> f64 {
value
}

View File

@@ -188,7 +188,7 @@ fn string_value(_: impl Ctx, _primary: (), string: TextArea) -> String {
/// Type-asserts a value to be a string.
#[node_macro::node(category("Debug"))]
fn to_string(_: impl Ctx, value: String) -> String {
fn as_string(_: impl Ctx, value: String) -> String {
value
}

View File

@@ -1074,16 +1074,10 @@ async fn dimensions(_: impl Ctx, content: List<Vector>) -> DVec2 {
.unwrap_or_default()
}
// TODO: Replace this node with an automatic type conversion implementation of the `Convert` trait
/// Converts a vec2 value into a vector path composed of a single anchor point.
///
/// This is useful in conjunction with nodes that repeat it, followed by the "Points to Polyline" node to string together a path of the points.
#[node_macro::node(category("Vector"), name("Vec2 to Point"), path(core_types::vector))]
async fn vec2_to_point(_: impl Ctx, vec2: DVec2) -> List<Vector> {
let mut point_domain = PointDomain::new();
point_domain.push(PointId::generate(), vec2);
List::new_from_item(Item::new_from_element(Vector { point_domain, ..Default::default() }))
/// Type-asserts a value to be vector data.
#[node_macro::node(category("Vector"), name("As Vector"), path(core_types::vector))]
fn as_vector(_: impl Ctx, value: List<Vector>) -> List<Vector> {
value
}
/// Creates a polyline from a series of vector points, replacing any existing segments and regions that may already exist.