Let vector modifier nodes operate on graphic content by recursing into groups (#4431)

* Make the 'Solidify Stroke' node preserve graphic structure instead of flattening its input

* Add a shared trait letting element-wise vector modifiers recurse into graphic groups

* Make 20 element-wise vector modifier nodes accept graphic input

* Make 'Separate Subpaths' and 'Points to Polyline' accept graphic input

* Make the 'Map Points' node accept graphic input

* Add TODO comments to the measure nodes deferred until the Graphic lowering refactor
This commit is contained in:
Keavon Chambers
2026-08-11 17:14:04 -07:00
committed by GitHub
parent 3994312440
commit a034923695
2 changed files with 995 additions and 803 deletions
+281 -89
View File
@@ -104,6 +104,93 @@ impl VectorItemMut for Item<Graphic> {
} }
} }
/// Geometry counterpart to [`VectorItemMut`] for the element-wise modifier nodes, running a per-item transformation over
/// every vector reachable from the content. Geometry is never inherited, so this recurses into nested groups.
trait MapVectorItems: Sized {
fn map_vector_items(content: Item<Self>, f: impl FnMut(Item<Vector>) -> Item<Vector>) -> Item<Self>;
/// Mutable access to each reachable vector at once, for callers that must await between elements.
fn vector_elements_mut(content: &mut Item<Self>) -> Vec<&mut Vector>;
}
impl MapVectorItems for Vector {
fn map_vector_items(content: Item<Vector>, mut f: impl FnMut(Item<Vector>) -> Item<Vector>) -> Item<Vector> {
f(content)
}
fn vector_elements_mut(content: &mut Item<Vector>) -> Vec<&mut Vector> {
vec![content.element_mut()]
}
}
impl MapVectorItems for Graphic {
fn map_vector_items(content: Item<Graphic>, mut f: impl FnMut(Item<Vector>) -> Item<Vector>) -> Item<Graphic> {
fn map_nested(graphic: &mut Graphic, f: &mut impl FnMut(Item<Vector>) -> Item<Vector>) {
match graphic {
// Collecting from zero items would drop the attribute columns, so an empty list is left alone
Graphic::Vector(list) if !list.is_empty() => *list = std::mem::take(list).into_iter().map(&mut *f).collect(),
Graphic::Graphic(list) => list.iter_element_values_mut().for_each(|nested| map_nested(nested, f)),
_ => {}
}
}
let mut content = content;
map_nested(content.element_mut(), &mut f);
content
}
fn vector_elements_mut(content: &mut Item<Graphic>) -> Vec<&mut Vector> {
fn collect<'a>(graphic: &'a mut Graphic, elements: &mut Vec<&'a mut Vector>) {
match graphic {
Graphic::Vector(list) => elements.extend(list.iter_element_values_mut()),
Graphic::Graphic(list) => list.iter_element_values_mut().for_each(|nested| collect(nested, elements)),
_ => {}
}
}
let mut elements = Vec::new();
collect(content.element_mut(), &mut elements);
elements
}
}
/// Counterpart to [`MapVectorItems`] for modifiers whose per-item result is a whole list rather than one item.
trait ExpandVectorItems: Sized {
fn expand_vector_items(content: Item<Self>, f: impl FnMut(Item<Vector>) -> List<Vector>) -> List<Self>;
}
impl ExpandVectorItems for Vector {
fn expand_vector_items(content: Item<Vector>, mut f: impl FnMut(Item<Vector>) -> List<Vector>) -> List<Vector> {
f(content)
}
}
impl ExpandVectorItems for Graphic {
fn expand_vector_items(content: Item<Graphic>, mut f: impl FnMut(Item<Vector>) -> List<Vector>) -> List<Graphic> {
fn expand_nested(graphic: &mut Graphic, f: &mut impl FnMut(Item<Vector>) -> List<Vector>) {
match graphic {
// Collecting from zero items would drop the attribute columns, so an empty list is left alone
Graphic::Vector(list) if !list.is_empty() => {
let mut expanded = List::with_capacity(list.len());
for item in std::mem::take(list) {
expanded.extend(f(item));
}
*list = expanded;
}
Graphic::Graphic(list) => list.iter_element_values_mut().for_each(|nested| expand_nested(nested, f)),
_ => {}
}
}
let mut content = content;
expand_nested(content.element_mut(), &mut f);
List::new_from_item(content)
}
}
/// Uniquely sets the fill and/or stroke style of every vector element to individual colors sampled along a chosen gradient. /// Uniquely sets the fill and/or stroke style of every vector element to individual colors sampled along a chosen gradient.
#[node_macro::node(category("Vector: Style"), path(graphene_core::vector))] #[node_macro::node(category("Vector: Style"), path(graphene_core::vector))]
async fn assign_colors<T>( async fn assign_colors<T>(
@@ -429,9 +516,9 @@ async fn copy_to_points<I: 'n + Send + Clone>(
} }
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
async fn round_corners( async fn round_corners<V: MapVectorItems + 'n + Send>(
_: impl Ctx, _: impl Ctx,
source: Item<Vector>, #[implementations(Graphic, Vector)] source: Item<V>,
#[hard(0..)] #[hard(0..)]
#[default(10.)] #[default(10.)]
radius: Item<PixelLength>, radius: Item<PixelLength>,
@@ -444,7 +531,8 @@ async fn round_corners(
#[hard(0..180)] #[hard(0..180)]
#[default(5.)] #[default(5.)]
min_angle_threshold: Item<Angle>, min_angle_threshold: Item<Angle>,
) -> Item<Vector> { ) -> Item<V> {
V::map_vector_items(source, |source| {
let (radius, roundness, edge_length_limit, min_angle_threshold) = (*radius.element(), *roundness.element(), *edge_length_limit.element(), *min_angle_threshold.element()); let (radius, roundness, edge_length_limit, min_angle_threshold) = (*radius.element(), *roundness.element(), *edge_length_limit.element(), *min_angle_threshold.element());
let source_transform: DAffine2 = source.attribute_cloned_or_default(ATTR_TRANSFORM); let source_transform: DAffine2 = source.attribute_cloned_or_default(ATTR_TRANSFORM);
let source_transform_inverse = source_transform.inverse(); let source_transform_inverse = source_transform.inverse();
@@ -534,20 +622,23 @@ async fn round_corners(
} }
Item::from_parts(result, attributes) Item::from_parts(result, attributes)
})
} }
#[node_macro::node(name("Merge by Distance"), category("Vector: Modifier"), path(core_types::vector))] #[node_macro::node(name("Merge by Distance"), category("Vector: Modifier"), path(core_types::vector))]
pub fn merge_by_distance( fn merge_by_distance<V: MapVectorItems + Send + Sync + 'static>(
_: impl Ctx, _: impl Ctx,
content: Item<Vector>, #[implementations(Graphic, Vector)] content: Item<V>,
#[default(0.1)] #[default(0.1)]
#[hard(0.0001..)] #[hard(0.0001..)]
distance: Item<PixelLength>, distance: Item<PixelLength>,
algorithm: Item<MergeByDistanceAlgorithm>, algorithm: Item<MergeByDistanceAlgorithm>,
) -> Item<Vector> { ) -> Item<V> {
let mut content = content;
let (distance, algorithm) = (*distance.element(), *algorithm.element()); let (distance, algorithm) = (*distance.element(), *algorithm.element());
V::map_vector_items(content, |content| {
let mut content = content;
match algorithm { match algorithm {
MergeByDistanceAlgorithm::Spatial => { MergeByDistanceAlgorithm::Spatial => {
let transform: DAffine2 = content.attribute_cloned_or_default(ATTR_TRANSFORM); let transform: DAffine2 = content.attribute_cloned_or_default(ATTR_TRANSFORM);
@@ -557,6 +648,7 @@ pub fn merge_by_distance(
} }
content content
})
} }
pub mod extrude_algorithms { pub mod extrude_algorithms {
@@ -757,19 +849,22 @@ pub mod extrude_algorithms {
} }
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
async fn extrude(_: impl Ctx, source: Item<Vector>, direction: Item<DVec2>, joining_algorithm: Item<ExtrudeJoiningAlgorithm>) -> Item<Vector> { async fn extrude<V: MapVectorItems + 'n + Send>(_: impl Ctx, #[implementations(Graphic, Vector)] source: Item<V>, direction: Item<DVec2>, joining_algorithm: Item<ExtrudeJoiningAlgorithm>) -> Item<V> {
V::map_vector_items(source, |source| {
let mut source = source; let mut source = source;
let (direction, joining_algorithm) = (*direction.element(), *joining_algorithm.element()); let (direction, joining_algorithm) = (*direction.element(), *joining_algorithm.element());
extrude_algorithms::extrude(source.element_mut(), direction, joining_algorithm); extrude_algorithms::extrude(source.element_mut(), direction, joining_algorithm);
source source
})
} }
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
async fn box_warp(_: impl Ctx, content: Item<Vector>, #[expose] rectangle: Item<Vector>) -> Item<Vector> { async fn box_warp<V: MapVectorItems + 'n + Send>(_: impl Ctx, #[implementations(Graphic, Vector)] content: Item<V>, #[expose] rectangle: Item<Vector>) -> Item<V> {
let target_transform: DAffine2 = rectangle.attribute_cloned_or_default(ATTR_TRANSFORM); let target_transform: DAffine2 = rectangle.attribute_cloned_or_default(ATTR_TRANSFORM);
let target = rectangle.into_element(); let target = rectangle.into_element();
V::map_vector_items(content, |content| {
let mut row = content; let mut row = content;
{ {
let transform: DAffine2 = row.attribute_cloned_or_default(ATTR_TRANSFORM); let transform: DAffine2 = row.attribute_cloned_or_default(ATTR_TRANSFORM);
@@ -835,6 +930,7 @@ async fn box_warp(_: impl Ctx, content: Item<Vector>, #[expose] rectangle: Item<
row.set_attribute(ATTR_TRANSFORM, DAffine2::IDENTITY); row.set_attribute(ATTR_TRANSFORM, DAffine2::IDENTITY);
} }
row row
})
} }
// Interpolate within a quadrilateral using normalized coordinates (0-1) // Interpolate within a quadrilateral using normalized coordinates (0-1)
@@ -972,9 +1068,9 @@ where
/// Automatically constructs tangents (Bézier handles) for anchor points in a vector path. /// Automatically constructs tangents (Bézier handles) for anchor points in a vector path.
#[node_macro::node(category("Vector: Modifier"), name("Auto-Tangents"), path(core_types::vector))] #[node_macro::node(category("Vector: Modifier"), name("Auto-Tangents"), path(core_types::vector))]
async fn auto_tangents( async fn auto_tangents<V: MapVectorItems + 'n + Send>(
_: impl Ctx, _: impl Ctx,
source: Item<Vector>, #[implementations(Graphic, Vector)] source: Item<V>,
/// The amount of spread for the auto-tangents, from 0 (sharp corner) to 1 (full spread). /// The amount of spread for the auto-tangents, from 0 (sharp corner) to 1 (full spread).
#[default(0.5)] #[default(0.5)]
#[range] #[range]
@@ -983,7 +1079,8 @@ async fn auto_tangents(
/// If active, existing non-zero handles won't be affected. /// If active, existing non-zero handles won't be affected.
#[default(true)] #[default(true)]
preserve_existing: Item<bool>, preserve_existing: Item<bool>,
) -> Item<Vector> { ) -> Item<V> {
V::map_vector_items(source, |source| {
let (spread, preserve_existing) = (*spread.element(), *preserve_existing.element()); let (spread, preserve_existing) = (*spread.element(), *preserve_existing.element());
let transform: DAffine2 = source.attribute_cloned_or_default(ATTR_TRANSFORM); let transform: DAffine2 = source.attribute_cloned_or_default(ATTR_TRANSFORM);
@@ -1120,10 +1217,13 @@ async fn auto_tangents(
} }
Item::from_parts(result, attributes) Item::from_parts(result, attributes)
})
} }
// TODO: After the Graphic lowering refactor, measure a group as one enclosing box instead of one box per shape
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
async fn bounding_box(_: impl Ctx, content: Item<Vector>) -> Item<Vector> { async fn bounding_box<V: MapVectorItems + 'n + Send>(_: impl Ctx, #[implementations(Graphic, Vector)] content: Item<V>) -> Item<V> {
V::map_vector_items(content, |content| {
let mut content = content; let mut content = content;
let mut result = content let mut result = content
.element() .element()
@@ -1140,8 +1240,10 @@ async fn bounding_box(_: impl Ctx, content: Item<Vector>) -> Item<Vector> {
*content.element_mut() = result; *content.element_mut() = result;
content content
})
} }
// TODO: Accept graphic input once the Graphic lowering refactor gives group leaves a single Vector to measure
#[node_macro::node(category("Vector: Measure"), path(core_types::vector))] #[node_macro::node(category("Vector: Measure"), path(core_types::vector))]
async fn dimensions(_: impl Ctx, content: Item<Vector>) -> Item<DVec2> { async fn dimensions(_: impl Ctx, content: Item<Vector>) -> Item<DVec2> {
let dimensions = content let dimensions = content
@@ -1161,10 +1263,11 @@ fn as_vector(_: impl Ctx, value: Item<Vector>) -> Item<Vector> {
/// Creates a polyline from a series of vector points, replacing any existing segments and regions that may already exist. /// Creates a polyline from a series of vector points, replacing any existing segments and regions that may already exist.
#[node_macro::node(category("Vector"), name("Points to Polyline"), path(core_types::vector))] #[node_macro::node(category("Vector"), name("Points to Polyline"), path(core_types::vector))]
async fn points_to_polyline(_: impl Ctx, points: Item<Vector>, #[default(true)] closed: Item<bool>) -> Item<Vector> { async fn points_to_polyline<V: MapVectorItems + 'n + Send>(_: impl Ctx, #[implementations(Graphic, Vector)] points: Item<V>, #[default(true)] closed: Item<bool>) -> Item<V> {
let mut points = points;
let closed = *closed.element(); let closed = *closed.element();
V::map_vector_items(points, |points| {
let mut points = points;
let vector = points.element_mut(); let vector = points.element_mut();
let mut segment_domain = SegmentDomain::new(); let mut segment_domain = SegmentDomain::new();
@@ -1189,19 +1292,22 @@ async fn points_to_polyline(_: impl Ctx, points: Item<Vector>, #[default(true)]
vector.segment_domain = segment_domain; vector.segment_domain = segment_domain;
points points
})
} }
/// Evens out the distances between points by applying Lloyd's relaxation, moving every interior point toward the center of its Voronoi cell. /// Evens out the distances between points by applying Lloyd's relaxation, moving every interior point toward the center of its Voronoi cell.
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
async fn relax_points( async fn relax_points<V: MapVectorItems + 'n + Send>(
_: impl Ctx, _: impl Ctx,
/// A vector path or point cloud to relax. /// A vector path or point cloud to relax.
source: Item<Vector>, #[implementations(Graphic, Vector)]
source: Item<V>,
/// The number of relaxation steps to apply. A fractional value runs the whole steps and then blends partway toward one more step, so the amount of relaxation can be animated smoothly. /// The number of relaxation steps to apply. A fractional value runs the whole steps and then blends partway toward one more step, so the amount of relaxation can be animated smoothly.
#[default(1.)] #[default(1.)]
#[hard(0..1000)] #[hard(0..1000)]
iterations: Item<f64>, iterations: Item<f64>,
) -> Item<Vector> { ) -> Item<V> {
V::map_vector_items(source, |source| {
let mut source = source; let mut source = source;
let iterations = *iterations.element(); let iterations = *iterations.element();
@@ -1212,13 +1318,15 @@ async fn relax_points(
} }
source source
})
} }
/// Builds a Voronoi diagram from the anchor points. Each point claims the region of space closest to it, and those regions tessellate the plane. Cells around the outside are clipped to the convex hull of the points so the diagram stays finite. /// Builds a Voronoi diagram from the anchor points. Each point claims the region of space closest to it, and those regions tessellate the plane. Cells around the outside are clipped to the convex hull of the points so the diagram stays finite.
/// ///
/// When Connect Cells is off, every cell becomes its own closed, fillable subpath. When on, the cells share their common points and segments, forming a single connected mesh with no fillable regions. /// When Connect Cells is off, every cell becomes its own closed, fillable subpath. When on, the cells share their common points and segments, forming a single connected mesh with no fillable regions.
#[node_macro::node(category("Vector"), path(core_types::vector))] #[node_macro::node(category("Vector"), path(core_types::vector))]
async fn voronoi_cells(_: impl Ctx, source: Item<Vector>, connect_cells: Item<bool>) -> Item<Vector> { async fn voronoi_cells<V: MapVectorItems + 'n + Send>(_: impl Ctx, #[implementations(Graphic, Vector)] source: Item<V>, connect_cells: Item<bool>) -> Item<V> {
V::map_vector_items(source, |source| {
let mut source = source; let mut source = source;
let connect_cells = *connect_cells.element(); let connect_cells = *connect_cells.element();
@@ -1230,13 +1338,15 @@ async fn voronoi_cells(_: impl Ctx, source: Item<Vector>, connect_cells: Item<bo
} }
source source
})
} }
/// Builds a Delaunay triangulation connecting the anchor points. It is the geometric dual of the **Voronoi** node: a mesh of triangles in which no point lies inside any triangle's circumscribed circle. /// Builds a Delaunay triangulation connecting the anchor points. It is the geometric dual of the **Voronoi** node: a mesh of triangles in which no point lies inside any triangle's circumscribed circle.
/// ///
/// When Connect Cells is off, every triangle becomes its own closed, fillable subpath. When on, the triangles share their common points and segments, forming a single connected mesh with no fillable regions. /// When Connect Cells is off, every triangle becomes its own closed, fillable subpath. When on, the triangles share their common points and segments, forming a single connected mesh with no fillable regions.
#[node_macro::node(category("Vector"), path(core_types::vector))] #[node_macro::node(category("Vector"), path(core_types::vector))]
async fn triangulate(_: impl Ctx, source: Item<Vector>, connect_cells: Item<bool>) -> Item<Vector> { async fn triangulate<V: MapVectorItems + 'n + Send>(_: impl Ctx, #[implementations(Graphic, Vector)] source: Item<V>, connect_cells: Item<bool>) -> Item<V> {
V::map_vector_items(source, |source| {
let mut source = source; let mut source = source;
let connect_cells = *connect_cells.element(); let connect_cells = *connect_cells.element();
@@ -1251,6 +1361,7 @@ async fn triangulate(_: impl Ctx, source: Item<Vector>, connect_cells: Item<bool
} }
source source
})
} }
/// Replaces a vector's geometry (points, segments, and regions) with the given closed polygons, preserving its style. /// Replaces a vector's geometry (points, segments, and regions) with the given closed polygons, preserving its style.
@@ -1354,7 +1465,14 @@ fn mesh_weld_tolerance(polygons: &[Vec<DVec2>]) -> f64 {
} }
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector), properties("offset_path_properties"))] #[node_macro::node(category("Vector: Modifier"), path(core_types::vector), properties("offset_path_properties"))]
async fn offset_path(_: impl Ctx, content: Item<Vector>, distance: Item<f64>, join: Item<StrokeJoin>, #[default(4.)] miter_limit: Item<f64>) -> Item<Vector> { async fn offset_path<V: MapVectorItems + 'n + Send>(
_: impl Ctx,
#[implementations(Graphic, Vector)] content: Item<V>,
distance: Item<f64>,
join: Item<StrokeJoin>,
#[default(4.)] miter_limit: Item<f64>,
) -> Item<V> {
V::map_vector_items(content, |content| {
let mut content = content; let mut content = content;
let (distance, join, miter_limit) = (*distance.element(), *join.element(), *miter_limit.element()); let (distance, join, miter_limit) = (*distance.element(), *join.element(), *miter_limit.element());
@@ -1393,22 +1511,67 @@ async fn offset_path(_: impl Ctx, content: Item<Vector>, distance: Item<f64>, jo
*content.element_mut() = result; *content.element_mut() = result;
content content
})
} }
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] /// Element-level dispatch for the Solidify Stroke node, which changes how many vector items its content holds.
async fn solidify_stroke<T: 'n + Send>(_: impl Ctx, #[implementations(Graphic, Vector)] content: Item<T>) -> List<Vector> /// Implementors rebuild their vector content in place so the structure it arrived in survives.
where trait SolidifyStroke: Sized {
List<T>: IntoGraphicList, fn solidify_strokes(content: Item<Self>) -> List<Self>;
{ }
// TODO: Make this node support stroke align, which it currently ignores
let graphic_list = List::new_from_item(content).into_graphic_list(); impl SolidifyStroke for Vector {
let flattened: List<Vector> = graphic_list.clone().into_flattened_list(); fn solidify_strokes(content: Item<Vector>) -> List<Vector> {
solidify_stroke_list_with_snapshot(List::new_from_item(content))
}
}
impl SolidifyStroke for Graphic {
fn solidify_strokes(content: Item<Graphic>) -> List<Graphic> {
fn solidify_nested(graphic: &mut Graphic) {
match graphic {
Graphic::Vector(list) if !list.is_empty() => *list = solidify_stroke_list_with_snapshot(std::mem::take(list)),
Graphic::Graphic(list) => list.iter_element_values_mut().for_each(solidify_nested),
_ => {}
}
}
let mut content = content;
solidify_nested(content.element_mut());
List::new_from_item(content)
}
}
/// Solidifies the list, stashing the original as a merged-layers snapshot since outlining discards the editable path.
fn solidify_stroke_list_with_snapshot(source: List<Vector>) -> List<Vector> {
let mut output = solidify_stroke_list(source.clone());
if !output.is_empty() {
// Pre-compensate by item 0's inverse so the renderer's `upstream_footprint *= item_0_transform`
// recursion cancels out and leaves the snapshot's original transforms intact
let mut snapshot = source.into_graphic_list();
let item_0_transform: DAffine2 = output.attribute_cloned_or_default(ATTR_TRANSFORM, 0);
if item_0_transform.matrix2.determinant().abs() > f64::EPSILON {
let inverse = item_0_transform.inverse();
for transform in snapshot.iter_attribute_values_mut_or_default::<DAffine2>(ATTR_TRANSFORM) {
*transform = inverse * *transform;
}
}
output.set_attribute(ATTR_EDITOR_MERGED_LAYERS, 0, snapshot);
}
output
}
/// Replaces each item's stroke with filled outline geometry, emitting a separate fill item beside it when the
/// original carried a fill. Grows the item count by up to 2x.
fn solidify_stroke_list(content: List<Vector>) -> List<Vector> {
// A fill exists when the canonical attribute carries paint // A fill exists when the canonical attribute carries paint
let has_fills: Vec<bool> = (0..flattened.len()).map(|index| has_paint_at(&flattened, index, ATTR_FILL)).collect(); let has_fills: Vec<bool> = (0..content.len()).map(|index| has_paint_at(&content, index, ATTR_FILL)).collect();
let mut output: List<Vector> = flattened content
.into_iter() .into_iter()
.zip(has_fills) .zip(has_fills)
.flat_map(|(row, has_fill)| { .flat_map(|(row, has_fill)| {
@@ -1483,31 +1646,19 @@ where
PaintOrder::StrokeBelow => std::iter::once(stroke_row).chain(fill_row).collect::<Vec<_>>(), PaintOrder::StrokeBelow => std::iter::once(stroke_row).chain(fill_row).collect::<Vec<_>>(),
} }
}) })
.collect(); .collect()
// Snapshot the upstream content so the renderer can recurse into it for editor click-target preservation
// and surface the original pre-solidified `Vector` to the Path tool for editing.
if !output.is_empty() {
// Row 0 carries a composed transform inherited from the flattened input, but the merged_layers
// already holds the original transforms; pre-compensate by row 0's inverse so the renderer's
// `upstream_footprint *= row_0_transform` recursion cancels out and leaves the originals intact.
let mut graphic_list = graphic_list;
let row_0_transform: DAffine2 = output.attribute_cloned_or_default(ATTR_TRANSFORM, 0);
if row_0_transform.matrix2.determinant().abs() > f64::EPSILON {
let inverse = row_0_transform.inverse();
for transform in graphic_list.iter_attribute_values_mut_or_default::<DAffine2>(ATTR_TRANSFORM) {
*transform = inverse * *transform;
}
}
output.set_attribute(ATTR_EDITOR_MERGED_LAYERS, 0, graphic_list);
}
output
} }
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
async fn separate_subpaths(_: impl Ctx, content: Item<Vector>) -> List<Vector> { async fn solidify_stroke<V: SolidifyStroke + 'n + Send>(_: impl Ctx, #[implementations(Graphic, Vector)] content: Item<V>) -> List<V> {
// TODO: Make this node support stroke align, which it currently ignores
V::solidify_strokes(content)
}
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
async fn separate_subpaths<V: ExpandVectorItems + 'n + Send>(_: impl Ctx, #[implementations(Graphic, Vector)] content: Item<V>) -> List<V> {
V::expand_vector_items(content, |content| {
let bezpaths = content.element().stroke_bezpath_iter().collect::<Vec<_>>(); let bezpaths = content.element().stroke_bezpath_iter().collect::<Vec<_>>();
// Pass the original element through unchanged when it has no subpaths, so its attributes // Pass the original element through unchanged when it has no subpaths, so its attributes
@@ -1529,8 +1680,10 @@ async fn separate_subpaths(_: impl Ctx, content: Item<Vector>) -> List<Vector> {
Item::from_parts(vector, attributes.clone()) Item::from_parts(vector, attributes.clone())
}) })
.collect() .collect()
})
} }
// TODO: Accept graphic input once the Graphic lowering refactor gives group leaves a single Vector to measure
/// Determines if the subpath at the given index is closed, meaning its ends are connected together forming a loop. /// Determines if the subpath at the given index is closed, meaning its ends are connected together forming a loop.
#[node_macro::node(name("Path is Closed"), category("Vector: Measure"), path(core_types::vector))] #[node_macro::node(name("Path is Closed"), category("Vector: Measure"), path(core_types::vector))]
async fn path_is_closed( async fn path_is_closed(
@@ -1547,14 +1700,20 @@ async fn path_is_closed(
} }
#[node_macro::node(category("Vector"), path(graphene_core::vector))] #[node_macro::node(category("Vector"), path(graphene_core::vector))]
async fn map_points(ctx: impl Ctx + CloneVarArgs + ExtractAll, content: Item<Vector>, mapped: impl Node<Context<'static>, Output = Item<DVec2>>) -> Item<Vector> { async fn map_points<V: MapVectorItems + 'n + Send>(
ctx: impl Ctx + CloneVarArgs + ExtractAll,
#[implementations(Graphic, Vector)] content: Item<V>,
mapped: impl Node<Context<'static>, Output = Item<DVec2>>,
) -> Item<V> {
let mut content = content; let mut content = content;
for (index, (_, position)) in content.element_mut().point_domain.positions_mut().enumerate() { for vector in V::vector_elements_mut(&mut content) {
for (index, (_, position)) in vector.point_domain.positions_mut().enumerate() {
let owned_ctx = OwnedContextImpl::from(ctx.clone()).with_index(index).with_position(*position); let owned_ctx = OwnedContextImpl::from(ctx.clone()).with_index(index).with_position(*position);
*position = mapped.eval(owned_ctx.into_context()).await.into_element(); *position = mapped.eval(owned_ctx.into_context()).await.into_element();
} }
}
content content
} }
@@ -1615,9 +1774,9 @@ pub async fn combine_paths<T: IntoGraphicList>(_: impl Ctx, #[implementations(Li
/// Convert vector geometry into a polyline composed of evenly spaced points. /// Convert vector geometry into a polyline composed of evenly spaced points.
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector), properties("sample_polyline_properties"), memoize)] #[node_macro::node(category("Vector: Modifier"), path(core_types::vector), properties("sample_polyline_properties"), memoize)]
async fn sample_polyline( async fn sample_polyline<V: MapVectorItems + 'n + Send>(
_: impl Ctx, _: impl Ctx,
content: Item<Vector>, #[implementations(Graphic, Vector)] content: Item<V>,
spacing: Item<PointSpacingType>, spacing: Item<PointSpacingType>,
#[default(100.)] #[default(100.)]
#[hard(0..)] #[hard(0..)]
@@ -1633,7 +1792,8 @@ async fn sample_polyline(
#[unit(" px")] #[unit(" px")]
stop_offset: Item<f64>, stop_offset: Item<f64>,
adaptive_spacing: Item<bool>, adaptive_spacing: Item<bool>,
) -> Item<Vector> { ) -> Item<V> {
V::map_vector_items(content, |content| {
let mut content = content; let mut content = content;
let (spacing, separation, quantity) = (*spacing.element(), *separation.element(), *quantity.element()); let (spacing, separation, quantity) = (*spacing.element(), *separation.element(), *quantity.element());
let (start_offset, stop_offset, adaptive_spacing) = (*start_offset.element(), *stop_offset.element(), *adaptive_spacing.element()); let (start_offset, stop_offset, adaptive_spacing) = (*start_offset.element(), *stop_offset.element(), *adaptive_spacing.element());
@@ -1696,20 +1856,21 @@ async fn sample_polyline(
*content.element_mut() = result; *content.element_mut() = result;
content content
})
} }
/// Simplifies vector paths by reducing the number of curve segments while preserving the overall shape within the given tolerance. /// Simplifies vector paths by reducing the number of curve segments while preserving the overall shape within the given tolerance.
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
async fn simplify( async fn simplify<V: MapVectorItems + 'n + Send>(
_: impl Ctx, _: impl Ctx,
/// The vector paths to simplify. /// The vector paths to simplify.
content: Item<Vector>, #[implementations(Graphic, Vector)]
content: Item<V>,
/// The maximum distance the simplified path may deviate from the original. /// The maximum distance the simplified path may deviate from the original.
#[default(5.)] #[default(5.)]
#[unit(" px")] #[unit(" px")]
tolerance: Item<Length>, tolerance: Item<Length>,
) -> Item<Vector> { ) -> Item<V> {
let mut content = content;
let tolerance = *tolerance.element(); let tolerance = *tolerance.element();
if tolerance <= 0. { if tolerance <= 0. {
@@ -1718,16 +1879,17 @@ async fn simplify(
let options = SimplifyOptions::default(); let options = SimplifyOptions::default();
let transform_attribute: DAffine2 = content.attribute_cloned_or_default(ATTR_TRANSFORM); V::map_vector_items(content, |mut item| {
let transform_attribute: DAffine2 = item.attribute_cloned_or_default(ATTR_TRANSFORM);
let transform = Affine::new(transform_attribute.to_cols_array()); let transform = Affine::new(transform_attribute.to_cols_array());
let inverse_transform = transform.inverse(); let inverse_transform = transform.inverse();
let mut result = Vector { let mut result = Vector {
stroke: std::mem::take(&mut content.element_mut().stroke), stroke: std::mem::take(&mut item.element_mut().stroke),
..Default::default() ..Default::default()
}; };
for mut bezpath in content.element().stroke_bezpath_iter() { for mut bezpath in item.element().stroke_bezpath_iter() {
bezpath.apply_affine(transform); bezpath.apply_affine(transform);
let mut simplified = simplify_bezpath(bezpath, tolerance, &options); let mut simplified = simplify_bezpath(bezpath, tolerance, &options);
@@ -1736,21 +1898,24 @@ async fn simplify(
result.append_bezpath(simplified); result.append_bezpath(simplified);
} }
*content.element_mut() = result; *item.element_mut() = result;
content item
})
} }
/// Decimates vector paths into polylines by sampling any curves into line segments, then removing points that don't significantly contribute to the shape using the Ramer-Douglas-Peucker algorithm. /// Decimates vector paths into polylines by sampling any curves into line segments, then removing points that don't significantly contribute to the shape using the Ramer-Douglas-Peucker algorithm.
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
async fn decimate( async fn decimate<V: MapVectorItems + 'n + Send>(
_: impl Ctx, _: impl Ctx,
/// The vector paths to decimate. /// The vector paths to decimate.
content: Item<Vector>, #[implementations(Graphic, Vector)]
content: Item<V>,
/// The maximum distance a point can deviate from the simplified path before it is kept. /// The maximum distance a point can deviate from the simplified path before it is kept.
#[default(5.)] #[default(5.)]
#[unit(" px")] #[unit(" px")]
tolerance: Item<Length>, tolerance: Item<Length>,
) -> Item<Vector> { ) -> Item<V> {
V::map_vector_items(content, |content| {
let mut content = content; let mut content = content;
let tolerance = *tolerance.element(); let tolerance = *tolerance.element();
@@ -1862,23 +2027,26 @@ async fn decimate(
*content.element_mut() = result; *content.element_mut() = result;
content content
})
} }
/// Cuts a path at a given progression from 0 to 1 along the path, creating two new subpaths from the original one (if the path is initially open) or one open subpath (if the path is initially closed). /// Cuts a path at a given progression from 0 to 1 along the path, creating two new subpaths from the original one (if the path is initially open) or one open subpath (if the path is initially closed).
/// ///
/// If multiple subpaths make up the path, the whole number part of the progression value selects the subpath and the decimal part determines the position along it. /// If multiple subpaths make up the path, the whole number part of the progression value selects the subpath and the decimal part determines the position along it.
#[node_macro::node(category("Vector: Modifier"), path(graphene_core::vector))] #[node_macro::node(category("Vector: Modifier"), path(graphene_core::vector))]
async fn cut_path( async fn cut_path<V: MapVectorItems + 'n + Send>(
_: impl Ctx, _: impl Ctx,
/// The path to insert a cut into. /// The path to insert a cut into.
content: Item<Vector>, #[implementations(Graphic, Vector)]
content: Item<V>,
/// The factor from the start to the end of the path, 0–1 for one subpath, 1–2 for a second subpath, and so on. /// The factor from the start to the end of the path, 0–1 for one subpath, 1–2 for a second subpath, and so on.
progression: Item<Progression>, progression: Item<Progression>,
/// Swap the direction of the path. /// Swap the direction of the path.
reverse: Item<bool>, reverse: Item<bool>,
/// Traverse the path using each segment's Bézier curve parameterization instead of the Euclidean distance. Faster to compute but doesn't respect actual distances. /// Traverse the path using each segment's Bézier curve parameterization instead of the Euclidean distance. Faster to compute but doesn't respect actual distances.
parameterized_distance: Item<bool>, parameterized_distance: Item<bool>,
) -> Item<Vector> { ) -> Item<V> {
V::map_vector_items(content, |content| {
let mut content = content; let mut content = content;
let (progression, reverse, parameterized_distance) = (*progression.element(), *reverse.element(), *parameterized_distance.element()); let (progression, reverse, parameterized_distance) = (*progression.element(), *reverse.element(), *parameterized_distance.element());
@@ -1914,11 +2082,13 @@ async fn cut_path(
} }
content content
})
} }
/// Cuts path segments into separate disconnected pieces where each is a distinct subpath. /// Cuts path segments into separate disconnected pieces where each is a distinct subpath.
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
async fn cut_segments(_: impl Ctx, content: Item<Vector>) -> Item<Vector> { async fn cut_segments<V: MapVectorItems + 'n + Send>(_: impl Ctx, #[implementations(Graphic, Vector)] content: Item<V>) -> Item<V> {
V::map_vector_items(content, |content| {
let mut content = content; let mut content = content;
// Iterate through every segment and make a copy of each of its endpoints, then reassign each segment's endpoints to its own unique point copy // Iterate through every segment and make a copy of each of its endpoints, then reassign each segment's endpoints to its own unique point copy
let vector = content.element_mut(); let vector = content.element_mut();
@@ -1972,8 +2142,10 @@ async fn cut_segments(_: impl Ctx, content: Item<Vector>) -> Item<Vector> {
} }
content content
})
} }
// TODO: Accept graphic input once the Graphic lowering refactor gives group leaves a single Vector to measure
/// Determines the position of a point on the path, given by its progression from 0 to 1 along the path. /// Determines the position of a point on the path, given by its progression from 0 to 1 along the path.
/// ///
/// If multiple subpaths make up the path, the whole number part of the progression value selects the subpath and the decimal part determines the position along it. /// If multiple subpaths make up the path, the whole number part of the progression value selects the subpath and the decimal part determines the position along it.
@@ -2011,6 +2183,7 @@ async fn position_on_path(
Item::new_from_element(position) Item::new_from_element(position)
} }
// TODO: Accept graphic input once the Graphic lowering refactor gives group leaves a single Vector to measure
/// Determines the angle of the tangent at a point on the path, given by its progression from 0 to 1 along the path. /// Determines the angle of the tangent at a point on the path, given by its progression from 0 to 1 along the path.
/// ///
/// If multiple subpaths make up the path, the whole number part of the progression value selects the subpath and the decimal part determines the position along it. /// If multiple subpaths make up the path, the whole number part of the progression value selects the subpath and the decimal part determines the position along it.
@@ -2060,9 +2233,9 @@ async fn tangent_on_path(
} }
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector), memoize)] #[node_macro::node(category("Vector: Modifier"), path(core_types::vector), memoize)]
async fn scatter_points( async fn scatter_points<V: MapVectorItems + 'n + Send>(
_: impl Ctx, _: impl Ctx,
content: Item<Vector>, #[implementations(Graphic, Vector)] content: Item<V>,
#[unit(" px")] #[unit(" px")]
#[default(10.)] #[default(10.)]
#[range] #[range]
@@ -2070,7 +2243,8 @@ async fn scatter_points(
#[soft(1..100)] #[soft(1..100)]
separation: Item<f64>, separation: Item<f64>,
seed: Item<SeedValue>, seed: Item<SeedValue>,
) -> Item<Vector> { ) -> Item<V> {
V::map_vector_items(content, |content| {
let mut content = content; let mut content = content;
let (separation, seed) = (*separation.element(), *seed.element()); let (separation, seed) = (*separation.element(), *seed.element());
@@ -2105,10 +2279,12 @@ async fn scatter_points(
*content.element_mut() = result; *content.element_mut() = result;
content content
})
} }
#[node_macro::node(name("Spline"), category("Vector: Modifier"), path(core_types::vector))] #[node_macro::node(name("Spline"), category("Vector: Modifier"), path(core_types::vector))]
async fn spline(_: impl Ctx, content: Item<Vector>) -> Item<Vector> { async fn spline<V: MapVectorItems + 'n + Send>(_: impl Ctx, #[implementations(Graphic, Vector)] content: Item<V>) -> Item<V> {
V::map_vector_items(content, |content| {
let mut content = content; let mut content = content;
// Exit early if there are no points to generate splines from. // Exit early if there are no points to generate splines from.
if content.element().point_domain.positions().is_empty() { if content.element().point_domain.positions().is_empty() {
@@ -2147,6 +2323,7 @@ async fn spline(_: impl Ctx, content: Item<Vector>) -> Item<Vector> {
content.element_mut().segment_domain = segment_domain; content.element_mut().segment_domain = segment_domain;
content content
})
} }
/// Computes the inverse of a transform's linear (matrix2) part, handling singular transforms /// Computes the inverse of a transform's linear (matrix2) part, handling singular transforms
@@ -2204,10 +2381,11 @@ fn apply_point_deltas(element: &mut Vector, deltas: &[DVec2], transform: DAffine
/// Perturbs the positions of anchor points in vector geometry by random amounts and directions. /// Perturbs the positions of anchor points in vector geometry by random amounts and directions.
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
async fn jitter_points( async fn jitter_points<V: MapVectorItems + 'n + Send>(
_: impl Ctx, _: impl Ctx,
/// The vector geometry with points to be jittered. /// The vector geometry with points to be jittered.
content: Item<Vector>, #[implementations(Graphic, Vector)]
content: Item<V>,
/// The maximum extent of the random distance each point can be offset. /// The maximum extent of the random distance each point can be offset.
#[default(5.)] #[default(5.)]
#[unit(" px")] #[unit(" px")]
@@ -2217,7 +2395,8 @@ async fn jitter_points(
/// Whether to offset anchor points along their normal direction (perpendicular to the path) or in a random direction. Free-floating and branching points have no normal direction, so they receive a random-angled offset regardless of this setting. /// Whether to offset anchor points along their normal direction (perpendicular to the path) or in a random direction. Free-floating and branching points have no normal direction, so they receive a random-angled offset regardless of this setting.
#[default(true)] #[default(true)]
along_normals: Item<bool>, along_normals: Item<bool>,
) -> Item<Vector> { ) -> Item<V> {
V::map_vector_items(content, |content| {
let mut content = content; let mut content = content;
let (max_distance, seed, along_normals) = (*max_distance.element(), *seed.element(), *along_normals.element()); let (max_distance, seed, along_normals) = (*max_distance.element(), *seed.element(), *along_normals.element());
@@ -2251,20 +2430,23 @@ async fn jitter_points(
apply_point_deltas(content.element_mut(), &deltas, transform); apply_point_deltas(content.element_mut(), &deltas, transform);
content content
})
} }
/// Displaces anchor points along their normal direction (perpendicular to the path) by a set distance. /// Displaces anchor points along their normal direction (perpendicular to the path) by a set distance.
/// Points with 0 or 3+ segment connections have no well-defined normal and are left in place. /// Points with 0 or 3+ segment connections have no well-defined normal and are left in place.
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
async fn offset_points( async fn offset_points<V: MapVectorItems + 'n + Send>(
_: impl Ctx, _: impl Ctx,
/// The vector geometry with points to be offset. /// The vector geometry with points to be offset.
content: Item<Vector>, #[implementations(Graphic, Vector)]
content: Item<V>,
/// The distance to offset each anchor point along its normal. Positive values move outward, negative values move inward. /// The distance to offset each anchor point along its normal. Positive values move outward, negative values move inward.
#[default(10.)] #[default(10.)]
#[unit(" px")] #[unit(" px")]
distance: Item<f64>, distance: Item<f64>,
) -> Item<Vector> { ) -> Item<V> {
V::map_vector_items(content, |content| {
let mut content = content; let mut content = content;
let distance = *distance.element(); let distance = *distance.element();
let transform_attribute: DAffine2 = content.attribute_cloned_or_default(ATTR_TRANSFORM); let transform_attribute: DAffine2 = content.attribute_cloned_or_default(ATTR_TRANSFORM);
@@ -2289,6 +2471,7 @@ async fn offset_points(
apply_point_deltas(content.element_mut(), &deltas, transform); apply_point_deltas(content.element_mut(), &deltas, transform);
content content
})
} }
/// Interpolates the geometry, appearance, and transform between multiple vector layers, producing a single morphed vector shape. /// Interpolates the geometry, appearance, and transform between multiple vector layers, producing a single morphed vector shape.
@@ -3212,22 +3395,26 @@ fn bevel_algorithm(mut vector: Vector, transform: DAffine2, distance: f64) -> Ve
} }
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
fn bevel(_: impl Ctx, source: Item<Vector>, #[default(10.)] distance: Item<Length>) -> Item<Vector> { fn bevel<V: MapVectorItems + Send + Sync + 'static>(_: impl Ctx, #[implementations(Graphic, Vector)] source: Item<V>, #[default(10.)] distance: Item<Length>) -> Item<V> {
V::map_vector_items(source, |source| {
let distance = *distance.element(); let distance = *distance.element();
let transform: DAffine2 = source.attribute_cloned_or_default(ATTR_TRANSFORM); let transform: DAffine2 = source.attribute_cloned_or_default(ATTR_TRANSFORM);
let (element, attributes) = source.into_parts(); let (element, attributes) = source.into_parts();
Item::from_parts(bevel_algorithm(element, transform, distance), attributes) Item::from_parts(bevel_algorithm(element, transform, distance), attributes)
})
} }
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
fn close_path(_: impl Ctx, source: Item<Vector>) -> Item<Vector> { fn close_path<V: MapVectorItems + Send + Sync + 'static>(_: impl Ctx, #[implementations(Graphic, Vector)] source: Item<V>) -> Item<V> {
let mut source = source; V::map_vector_items(source, |mut item| {
source.element_mut().close_subpaths(); item.element_mut().close_subpaths();
source item
})
} }
// TODO: Accept graphic input once the Graphic lowering refactor gives group leaves a single Vector to measure
#[node_macro::node(category("Vector: Measure"), path(core_types::vector))] #[node_macro::node(category("Vector: Measure"), path(core_types::vector))]
fn point_inside(_: impl Ctx, source: Item<Vector>, point: Item<DVec2>) -> Item<bool> { fn point_inside(_: impl Ctx, source: Item<Vector>, point: Item<DVec2>) -> Item<bool> {
let point = point.into_element(); let point = point.into_element();
@@ -3244,6 +3431,7 @@ async fn list_length(_: impl Ctx, content: ListDyn) -> Item<f64> {
Item::new_from_element(content.len() as f64) Item::new_from_element(content.len() as f64)
} }
// TODO: Accept graphic input once the Graphic lowering refactor gives group leaves a single Vector to measure
#[node_macro::node(category("Vector: Measure"), path(graphene_core::vector))] #[node_macro::node(category("Vector: Measure"), path(graphene_core::vector))]
async fn count_points(_: impl Ctx, content: Item<Vector>) -> Item<f64> { async fn count_points(_: impl Ctx, content: Item<Vector>) -> Item<f64> {
let count = content.element().point_domain.positions().len() as f64; let count = content.element().point_domain.positions().len() as f64;
@@ -3251,6 +3439,7 @@ async fn count_points(_: impl Ctx, content: Item<Vector>) -> Item<f64> {
Item::new_from_element(count) Item::new_from_element(count)
} }
// TODO: Accept graphic input once the Graphic lowering refactor gives group leaves a single Vector to measure
/// Retrieves the vec2 position (in local space) of the anchor point at the specified index within a vector element. /// Retrieves the vec2 position (in local space) of the anchor point at the specified index within a vector element.
/// If no value exists at that index, the position (0, 0) is returned. /// If no value exists at that index, the position (0, 0) is returned.
#[node_macro::node(category("Vector: Measure"), path(graphene_core::vector))] #[node_macro::node(category("Vector: Measure"), path(graphene_core::vector))]
@@ -3279,6 +3468,7 @@ async fn index_points(
Item::new_from_element(positions[index]) Item::new_from_element(positions[index])
} }
// TODO: Accept graphic input once the Graphic lowering refactor gives group leaves a single Vector to measure
#[node_macro::node(category("Vector: Measure"), path(core_types::vector))] #[node_macro::node(category("Vector: Measure"), path(core_types::vector))]
async fn path_length(_: impl Ctx, source: Item<Vector>) -> Item<f64> { async fn path_length(_: impl Ctx, source: Item<Vector>) -> Item<f64> {
let transform: DAffine2 = source.attribute_cloned_or_default(ATTR_TRANSFORM); let transform: DAffine2 = source.attribute_cloned_or_default(ATTR_TRANSFORM);
@@ -3294,6 +3484,7 @@ async fn path_length(_: impl Ctx, source: Item<Vector>) -> Item<f64> {
Item::new_from_element(length) Item::new_from_element(length)
} }
// TODO: Accept graphic input once the Graphic lowering refactor gives group leaves a single Vector to measure
#[node_macro::node(category("Vector: Measure"), path(core_types::vector))] #[node_macro::node(category("Vector: Measure"), path(core_types::vector))]
async fn area(ctx: impl Ctx + CloneVarArgs + ExtractAll, content: impl Node<Context<'static>, Output = Item<Vector>>) -> Item<f64> { async fn area(ctx: impl Ctx + CloneVarArgs + ExtractAll, content: impl Node<Context<'static>, Output = Item<Vector>>) -> Item<f64> {
let new_ctx = OwnedContextImpl::from(ctx).with_footprint(Footprint::default()).into_context(); let new_ctx = OwnedContextImpl::from(ctx).with_footprint(Footprint::default()).into_context();
@@ -3306,6 +3497,7 @@ async fn area(ctx: impl Ctx + CloneVarArgs + ExtractAll, content: impl Node<Cont
Item::new_from_element(area) Item::new_from_element(area)
} }
// TODO: Accept graphic input once the Graphic lowering refactor gives group leaves a single Vector to measure
#[node_macro::node(category("Vector: Measure"), path(core_types::vector))] #[node_macro::node(category("Vector: Measure"), path(core_types::vector))]
async fn centroid(ctx: impl Ctx + CloneVarArgs + ExtractAll, content: impl Node<Context<'static>, Output = Item<Vector>>, centroid_type: Item<CentroidType>) -> Item<DVec2> { async fn centroid(ctx: impl Ctx + CloneVarArgs + ExtractAll, content: impl Node<Context<'static>, Output = Item<Vector>>, centroid_type: Item<CentroidType>) -> Item<DVec2> {
let centroid_type = centroid_type.into_element(); let centroid_type = centroid_type.into_element();
+2 -2
View File
@@ -385,8 +385,8 @@ mod tests {
#[test] #[test]
fn item_list_wire_pair_collapses_to_list() { fn item_list_wire_pair_collapses_to_list() {
let registry = core_types::registry::NODE_REGISTRY.lock().unwrap(); let registry = core_types::registry::NODE_REGISTRY.lock().unwrap();
let identifier = ProtoNodeIdentifier::new("core_types::vector::BoundingBoxNode"); let identifier = ProtoNodeIdentifier::new("core_types::vector::DimensionsNode");
let implementations = registry.get(&identifier).expect("Bounding Box should be registered"); let implementations = registry.get(&identifier).expect("Dimensions should be registered");
let primary_types: HashSet<_> = implementations.iter().map(|(_, node_io)| node_io.inputs[0].clone()).collect(); let primary_types: HashSet<_> = implementations.iter().map(|(_, node_io)| node_io.inputs[0].clone()).collect();
assert_eq!(primary_types.len(), 2, "An element-wise node should register Item and List wire variants for its primary input"); assert_eq!(primary_types.len(), 2, "An element-wise node should register Item and List wire variants for its primary input");