Convert the per-lane vector geometry nodes and the level measures to record kernels

This commit is contained in:
Dennis Kobert
2026-08-23 09:44:25 +00:00
parent 7b609ea781
commit 9d2031f7d0
2 changed files with 566 additions and 607 deletions
@@ -1,8 +1,7 @@
use core_types::attribute::{Attr, EditorLayerPath, RemoveAttr}; use core_types::attribute::{Attr, EditorLayerPath, RemoveAttr, Transform as TransformAttr};
use core_types::gpoll::{GraphError, Interrupt}; use core_types::gpoll::{GraphError, Interrupt};
use core_types::list::List;
use core_types::uuid::NodeId; use core_types::uuid::NodeId;
use core_types::{ATTR_TRANSFORM, Ctx, ExtractIndex, InjectIndex}; use core_types::{Ctx, ExtractIndex, InjectIndex};
use glam::DAffine2; use glam::DAffine2;
use graphic_types::Vector; use graphic_types::Vector;
use vector_types::markers::EditorClickTarget; use vector_types::markers::EditorClickTarget;
@@ -41,16 +40,12 @@ fn path_modify<'e>(
/// Applies the vector path's local transformation to its geometry and resets the transform to the identity. /// Applies the vector path's local transformation to its geometry and resets the transform to the identity.
#[node_macro::node(category("Vector"))] #[node_macro::node(category("Vector"))]
fn apply_transform(_ctx: impl Ctx, mut vector: List<Vector>) -> List<Vector> { fn apply_transform(_ctx: impl Ctx, (mut vector, transform): (Vector, Attr<TransformAttr>)) -> (Vector, Attr<TransformAttr>) {
let (elements, transforms) = vector.element_and_attribute_slices_mut::<DAffine2>(ATTR_TRANSFORM); let transform: DAffine2 = *transform;
for (element, transform) in elements.iter_mut().zip(transforms.iter_mut()) { for (_, point) in vector.point_domain.positions_mut() {
for (_, point) in element.point_domain.positions_mut() {
*point = transform.transform_point2(*point); *point = transform.transform_point2(*point);
} }
element.segment_domain.transform(*transform); vector.segment_domain.transform(transform);
*transform = DAffine2::IDENTITY; (vector, Attr(DAffine2::IDENTITY))
}
vector
} }
+208 -244
View File
@@ -1,24 +1,24 @@
use core::cmp::Ordering; use core::cmp::Ordering;
use core::f64::consts::{PI, TAU}; use core::f64::consts::{PI, TAU};
use core::hash::{Hash, Hasher}; use core::hash::{Hash, Hasher};
use core_types::attribute::Transform as TransformAttr;
use core_types::attribute::{Attr, BlendMode as BlendModeAttr, ClippingMask, EditorLayerPath, Opacity, OpacityFill};
use core_types::blending::BlendMode; use core_types::blending::BlendMode;
use core_types::bounds::{BoundingBox, RenderBoundingBox}; use core_types::bounds::{BoundingBox, RenderBoundingBox};
use core_types::gpoll::Interrupt;
use core_types::list::{Item, ItemAttributeValues, List, ListDyn};
use core_types::registry::types::{Angle, Length, Multiplier, Percentage, PixelLength, Progression, SeedValue};
use core_types::transform::{Footprint, Transform};
use core_types::uuid::NodeId;
use core_types::attribute::{Attr, BlendMode as BlendModeAttr, ClippingMask, EditorLayerPath, Opacity, OpacityFill};
use core_types::context::IndexLink; use core_types::context::IndexLink;
use core_types::extent::{ExtentIn, LevelIn, ListIn, ValueIn}; use core_types::extent::{ExtentIn, LevelIn, ListIn, ValueIn};
use core_types::gpoll::{Extent, GPoll};
use core_types::gpoll::GraphError; use core_types::gpoll::GraphError;
use core_types::gpoll::Interrupt;
use core_types::gpoll::{Extent, GPoll};
use core_types::list::{Item, ItemAttributeValues, List, ListDyn};
use core_types::registry::types::{Angle, Length, Multiplier, Percentage, PixelLength, Progression, SeedValue};
use core_types::transform::Transform;
use core_types::uuid::NodeId;
use core_types::{ATTR_BLEND_MODE, ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM, Color, Ctx, DeriveCtx, ExtractIndex, InjectIndex}; use core_types::{ATTR_BLEND_MODE, ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM, Color, Ctx, DeriveCtx, ExtractIndex, InjectIndex};
use graphic_types::markers::{EditorMergedLayers, Fill, Stroke as StrokeAttr};
use core_types::attribute::Transform as TransformAttr;
use glam::{DAffine2, DMat2, DVec2}; use glam::{DAffine2, DMat2, DVec2};
use graphic_types::Vector; use graphic_types::Vector;
use graphic_types::graphic::{bake_paint_transforms, graphic_list_at, has_paint_at, is_paint_present, set_paint_attribute_at}; use graphic_types::graphic::{bake_paint_transforms, graphic_list_at, has_paint_at, is_paint_present, set_paint_attribute_at};
use graphic_types::markers::{EditorMergedLayers, Fill, Stroke as StrokeAttr};
use graphic_types::raster_types::{CPU, GPU, Raster}; use graphic_types::raster_types::{CPU, GPU, Raster};
use graphic_types::{ATTR_EDITOR_MERGED_LAYERS, ATTR_FILL, ATTR_STROKE, Graphic, IntoGraphicList}; use graphic_types::{ATTR_EDITOR_MERGED_LAYERS, ATTR_FILL, ATTR_STROKE, Graphic, IntoGraphicList};
use kurbo::simplify::{SimplifyOptions, simplify_bezpath}; use kurbo::simplify::{SimplifyOptions, simplify_bezpath};
@@ -27,7 +27,6 @@ use rand::{Rng, SeedableRng};
use std::collections::hash_map::DefaultHasher; use std::collections::hash_map::DefaultHasher;
use vector_types::gradient::{build_transform_with_y_preservation, initial_gradient_transform_for_bounding_box}; use vector_types::gradient::{build_transform_with_y_preservation, initial_gradient_transform_for_bounding_box};
use vector_types::subpath::{BezierHandles, ManipulatorGroup}; use vector_types::subpath::{BezierHandles, ManipulatorGroup};
use vector_types::{ATTR_GRADIENT_TYPE, ATTR_SPREAD_METHOD};
use vector_types::vector::PointDomain; use vector_types::vector::PointDomain;
use vector_types::vector::algorithms::bezpath_algorithms::{self, TValue, eval_pathseg_euclidean, evaluate_bezpath, split_bezpath, tangent_on_bezpath}; use vector_types::vector::algorithms::bezpath_algorithms::{self, TValue, eval_pathseg_euclidean, evaluate_bezpath, split_bezpath, tangent_on_bezpath};
use vector_types::vector::algorithms::merge_by_distance::MergeByDistanceExt; use vector_types::vector::algorithms::merge_by_distance::MergeByDistanceExt;
@@ -39,6 +38,7 @@ use vector_types::vector::misc::{
}; };
use vector_types::vector::style::{GradientStops, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin}; use vector_types::vector::style::{GradientStops, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
use vector_types::vector::{FillId, PointId, RegionId, SegmentDomain, SegmentId, StrokeId, VectorExt}; use vector_types::vector::{FillId, PointId, RegionId, SegmentDomain, SegmentId, StrokeId, VectorExt};
use vector_types::{ATTR_GRADIENT_TYPE, ATTR_SPREAD_METHOD};
use vector_types::{GradientSpreadMethod, GradientType}; use vector_types::{GradientSpreadMethod, GradientType};
/// The standard row attributes a per-lane re-emission carries from its /// The standard row attributes a per-lane re-emission carries from its
@@ -101,9 +101,7 @@ fn assign_colors<'e>(
return Err(GraphError::past_end().into()); return Err(GraphError::past_end().into());
} }
let element = content.element_ref(lane).clone(); let element = content.element_ref(lane).clone();
let park_existing = |paint: Option<&List<Graphic>>| -> Result<Option<&'e List<Graphic>>, Interrupt> { let park_existing = |paint: Option<&List<Graphic>>| -> Result<Option<&'e List<Graphic>>, Interrupt> { paint.map(|paint| park_paint(ctx.arena(), paint.clone())).transpose() };
paint.map(|paint| park_paint(ctx.arena(), paint.clone())).transpose()
};
let existing_fill = park_existing(content.lane(lane).attr::<Fill>())?; let existing_fill = park_existing(content.lane(lane).attr::<Fill>())?;
let existing_stroke = park_existing(content.lane(lane).attr::<StrokeAttr>())?; let existing_stroke = park_existing(content.lane(lane).attr::<StrokeAttr>())?;
let carried = carried_lane_attrs(ctx.arena(), content.lane(lane))?; let carried = carried_lane_attrs(ctx.arena(), content.lane(lane))?;
@@ -161,8 +159,7 @@ fn assign_colors_extent(
fn assign_colors_graphic<'e>( fn assign_colors_graphic<'e>(
ctx: impl Ctx + ExtractArena<'e> + ExtractIndex + InjectIndex + Copy, ctx: impl Ctx + ExtractArena<'e> + ExtractIndex + InjectIndex + Copy,
content: IList<Graphic>, content: IList<Graphic>,
#[default(true)] #[default(true)] fill: bool,
fill: bool,
stroke: bool, stroke: bool,
gradient: IList<GradientStops>, gradient: IList<GradientStops>,
reverse: bool, reverse: bool,
@@ -316,8 +313,7 @@ fn fill<'e>(
fn fill_graphic_leveled<'e>( fn fill_graphic_leveled<'e>(
ctx: impl Ctx + ExtractArena<'e> + ExtractIndex + InjectIndex + Copy, ctx: impl Ctx + ExtractArena<'e> + ExtractIndex + InjectIndex + Copy,
(element, _content_fill): (Graphic, Attr<'e, Fill>), (element, _content_fill): (Graphic, Attr<'e, Fill>),
#[default(Color::BLACK)] #[default(Color::BLACK)] fill: IList<Graphic>,
fill: IList<Graphic>,
_backup_color: IList<Color>, _backup_color: IList<Color>,
_backup_gradient: IList<GradientStops>, _backup_gradient: IList<GradientStops>,
_gradient_type: GradientType, _gradient_type: GradientType,
@@ -415,20 +411,17 @@ fn for_each_interior_vector_mut(element: &mut Graphic, mut f: impl FnMut(&mut Ve
fn stroke_graphic_leveled<'e>( fn stroke_graphic_leveled<'e>(
ctx: impl Ctx + ExtractArena<'e> + ExtractIndex + InjectIndex + Copy, ctx: impl Ctx + ExtractArena<'e> + ExtractIndex + InjectIndex + Copy,
(element, content_transform): (Graphic, Attr<TransformAttr>), (element, content_transform): (Graphic, Attr<TransformAttr>),
#[default(Color::BLACK)] #[default(Color::BLACK)] paint: IList<Graphic>,
paint: IList<Graphic>,
#[unit(" px")] #[unit(" px")]
#[default(2.)] #[default(2.)]
weight: f64, weight: f64,
align: StrokeAlign, align: StrokeAlign,
cap: StrokeCap, cap: StrokeCap,
join: StrokeJoin, join: StrokeJoin,
#[default(4.)] #[default(4.)] miter_limit: f64,
miter_limit: f64,
paint_order: PaintOrder, paint_order: PaintOrder,
dash_lengths: IList<f64>, dash_lengths: IList<f64>,
#[unit(" px")] #[unit(" px")] dash_offset: f64,
dash_offset: f64,
) -> Result<(Graphic, Attr<TransformAttr>, Attr<'e, StrokeAttr>), Interrupt> { ) -> Result<(Graphic, Attr<TransformAttr>, Attr<'e, StrokeAttr>), Interrupt> {
let dash_lengths = (0..dash_lengths.len()).map(|index| dash_lengths.get(index).max(0.)).collect(); let dash_lengths = (0..dash_lengths.len()).map(|index| dash_lengths.get(index).max(0.)).collect();
let stroke = Stroke { let stroke = Stroke {
@@ -570,7 +563,7 @@ fn copy_to_points_extent(
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
fn round_corners( fn round_corners(
_: impl Ctx, _: impl Ctx,
source: List<Vector>, (source, transform): (Vector, Attr<TransformAttr>),
#[hard(0..)] #[hard(0..)]
#[default(10.)] #[default(10.)]
radius: PixelLength, radius: PixelLength,
@@ -583,13 +576,9 @@ fn round_corners(
#[hard(0..180)] #[hard(0..180)]
#[default(5.)] #[default(5.)]
min_angle_threshold: Angle, min_angle_threshold: Angle,
) -> List<Vector> { ) -> (Vector, Attr<TransformAttr>) {
(0..source.len()) let source_transform: DAffine2 = *transform;
.map(|index| {
let source_transform: DAffine2 = source.attribute_cloned_or_default(ATTR_TRANSFORM, index);
let source_transform_inverse = source_transform.inverse(); let source_transform_inverse = source_transform.inverse();
let attributes = source.clone_item_attributes(index);
let source = source.element(index).unwrap();
// Flip the roundness to help with user intuition // Flip the roundness to help with user intuition
let roundness = 1. - roundness; let roundness = 1. - roundness;
@@ -674,37 +663,23 @@ fn round_corners(
result.append_bezpath(rounded_subpath); result.append_bezpath(rounded_subpath);
} }
Item::from_parts(result, attributes) (result, Attr(source_transform))
})
.collect()
} }
#[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( pub fn merge_by_distance(
_: impl Ctx, _: impl Ctx,
content: List<Vector>, (mut content, transform): (Vector, Attr<TransformAttr>),
#[default(0.1)] #[default(0.1)]
#[hard(0.0001..)] #[hard(0.0001..)]
distance: PixelLength, distance: PixelLength,
algorithm: MergeByDistanceAlgorithm, algorithm: MergeByDistanceAlgorithm,
) -> List<Vector> { ) -> (Vector, Attr<TransformAttr>) {
match algorithm { match algorithm {
MergeByDistanceAlgorithm::Spatial => content MergeByDistanceAlgorithm::Spatial => content.merge_by_distance_spatial(*transform, distance),
.into_iter() MergeByDistanceAlgorithm::Topological => content.merge_by_distance_topological(distance),
.map(|mut row| {
let transform: DAffine2 = row.attribute_cloned_or_default(ATTR_TRANSFORM);
row.element_mut().merge_by_distance_spatial(transform, distance);
row
})
.collect(),
MergeByDistanceAlgorithm::Topological => content
.into_iter()
.map(|mut row| {
row.element_mut().merge_by_distance_topological(distance);
row
})
.collect(),
} }
(content, Attr(*transform))
} }
pub mod extrude_algorithms { pub mod extrude_algorithms {
@@ -905,23 +880,19 @@ pub mod extrude_algorithms {
} }
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
fn extrude(_: impl Ctx, mut source: List<Vector>, direction: DVec2, joining_algorithm: ExtrudeJoiningAlgorithm) -> List<Vector> { fn extrude(_: impl Ctx, mut source: Vector, direction: DVec2, joining_algorithm: ExtrudeJoiningAlgorithm) -> Vector {
for vector in source.iter_element_values_mut() { extrude_algorithms::extrude(&mut source, direction, joining_algorithm);
extrude_algorithms::extrude(vector, 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))]
fn box_warp(_: impl Ctx, content: List<Vector>, #[expose] rectangle: List<Vector>) -> List<Vector> { fn box_warp(_: impl Ctx + ExtractIndex + InjectIndex + Copy, (vector, transform): (Vector, Attr<TransformAttr>), #[expose] rectangle: IList<Vector>) -> (Vector, Attr<TransformAttr>) {
let Some(target) = rectangle.element(0).cloned() else { return content }; if rectangle.is_empty() {
let target_transform: DAffine2 = rectangle.attribute_cloned_or_default(ATTR_TRANSFORM, 0); return (vector, Attr(*transform));
}
content let target = rectangle.element_ref(0);
.into_iter() let target_transform: DAffine2 = rectangle.lane(0).attr::<TransformAttr>();
.map(|mut row| { let transform: DAffine2 = *transform;
let transform: DAffine2 = row.attribute_cloned_or_default(ATTR_TRANSFORM);
let vector = std::mem::take(row.element_mut());
// Get the bounding box of the source vector geometry // Get the bounding box of the source vector geometry
let source_bbox = vector.bounding_box_with_transform(transform).unwrap_or([DVec2::ZERO, DVec2::ONE]); let source_bbox = vector.bounding_box_with_transform(transform).unwrap_or([DVec2::ZERO, DVec2::ONE]);
@@ -978,12 +949,8 @@ fn box_warp(_: impl Ctx, content: List<Vector>, #[expose] rectangle: List<Vector
result.set_stroke_transform(DAffine2::IDENTITY); result.set_stroke_transform(DAffine2::IDENTITY);
// Add this to the `List` and reset the transform since we've applied it directly to the points // Reset the transform since we've applied it directly to the points
*row.element_mut() = result; (result, Attr(DAffine2::IDENTITY))
row.set_attribute(ATTR_TRANSFORM, DAffine2::IDENTITY);
row
})
.collect()
} }
// Interpolate within a quadrilateral using normalized coordinates (0-1) // Interpolate within a quadrilateral using normalized coordinates (0-1)
@@ -1121,7 +1088,7 @@ where
#[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))]
fn auto_tangents( fn auto_tangents(
_: impl Ctx, _: impl Ctx,
source: List<Vector>, (source, lane_transform): (Vector, Attr<TransformAttr>),
/// 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]
@@ -1130,12 +1097,8 @@ 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: bool, preserve_existing: bool,
) -> List<Vector> { ) -> (Vector, Attr<TransformAttr>) {
(0..source.len()) let transform: DAffine2 = *lane_transform;
.map(|index| {
let transform: DAffine2 = source.attribute_cloned_or_default(ATTR_TRANSFORM, index);
let attributes = source.clone_item_attributes(index);
let source = source.element(index).unwrap();
let mut result = Vector { let mut result = Vector {
stroke: source.stroke.clone(), stroke: source.stroke.clone(),
@@ -1267,18 +1230,11 @@ fn auto_tangents(
} }
} }
Item::from_parts(result, attributes) (result, Attr(transform))
})
.collect()
} }
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
fn bounding_box(_: impl Ctx, content: List<Vector>) -> List<Vector> { fn bounding_box(_: impl Ctx, vector: Vector) -> Vector {
content
.into_iter()
.map(|mut row| {
let vector = std::mem::take(row.element_mut());
let mut result = vector let mut result = vector
.bounding_box_rect() .bounding_box_rect()
.map(|bbox| { .map(|bbox| {
@@ -1291,16 +1247,13 @@ fn bounding_box(_: impl Ctx, content: List<Vector>) -> List<Vector> {
result.stroke = vector.stroke.clone(); result.stroke = vector.stroke.clone();
result.set_stroke_transform(DAffine2::IDENTITY); result.set_stroke_transform(DAffine2::IDENTITY);
*row.element_mut() = result; result
row
})
.collect()
} }
#[node_macro::node(category("Vector: Measure"), path(core_types::vector))] #[node_macro::node(category("Vector: Measure"), path(core_types::vector))]
fn dimensions(_: impl Ctx, content: List<Vector>) -> DVec2 { fn dimensions(_: impl Ctx + ExtractIndex + InjectIndex + Copy, content: IList<Vector>) -> DVec2 {
(0..content.len()) (0..content.len())
.filter_map(|index| content.element(index).unwrap().bounding_box_with_transform(content.attribute_cloned_or_default(ATTR_TRANSFORM, index))) .filter_map(|index| content.element_ref(index).bounding_box_with_transform(content.lane(index).attr::<TransformAttr>()))
.reduce(|[acc_top_left, acc_bottom_right], [top_left, bottom_right]| [acc_top_left.min(top_left), acc_bottom_right.max(bottom_right)]) .reduce(|[acc_top_left, acc_bottom_right], [top_left, bottom_right]| [acc_top_left.min(top_left), acc_bottom_right.max(bottom_right)])
.map(|[top_left, bottom_right]| bottom_right - top_left) .map(|[top_left, bottom_right]| bottom_right - top_left)
.unwrap_or_default() .unwrap_or_default()
@@ -1308,18 +1261,17 @@ fn dimensions(_: impl Ctx, content: List<Vector>) -> DVec2 {
/// Type-asserts a value to be vector data. /// Type-asserts a value to be vector data.
#[node_macro::node(category("Vector"), name("As Vector"), path(core_types::vector))] #[node_macro::node(category("Vector"), name("As Vector"), path(core_types::vector))]
fn as_vector(_: impl Ctx, value: List<Vector>) -> List<Vector> { fn as_vector(_: impl Ctx, value: Vector) -> Vector {
value value
} }
/// 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))]
fn points_to_polyline(_: impl Ctx, mut points: List<Vector>, #[default(true)] closed: bool) -> List<Vector> { fn points_to_polyline(_: impl Ctx, mut points: Vector, #[default(true)] closed: bool) -> Vector {
for vector in points.iter_element_values_mut() {
let mut segment_domain = SegmentDomain::new(); let mut segment_domain = SegmentDomain::new();
let mut next_id = SegmentId::ZERO; let mut next_id = SegmentId::ZERO;
let points_count = vector.point_domain.ids().len(); let points_count = points.point_domain.ids().len();
if points_count >= 2 { if points_count >= 2 {
(0..points_count - 1).for_each(|i| { (0..points_count - 1).for_each(|i| {
@@ -1329,26 +1281,21 @@ fn points_to_polyline(_: impl Ctx, mut points: List<Vector>, #[default(true)] cl
if closed && points_count != 2 { if closed && points_count != 2 {
segment_domain.push(next_id.next_id(), points_count - 1, 0, BezierHandles::Linear, StrokeId::generate()); segment_domain.push(next_id.next_id(), points_count - 1, 0, BezierHandles::Linear, StrokeId::generate());
vector points
.region_domain .region_domain
.push(RegionId::generate(), segment_domain.ids()[0]..=*segment_domain.ids().last().unwrap(), FillId::generate()); .push(RegionId::generate(), segment_domain.ids()[0]..=*segment_domain.ids().last().unwrap(), FillId::generate());
} }
} }
vector.segment_domain = segment_domain; points.segment_domain = segment_domain;
}
points points
} }
#[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"))]
fn offset_path(_: impl Ctx, content: List<Vector>, distance: f64, join: StrokeJoin, #[default(4.)] miter_limit: f64) -> List<Vector> { fn offset_path(_: impl Ctx, (vector, lane_transform): (Vector, Attr<TransformAttr>), distance: f64, join: StrokeJoin, #[default(4.)] miter_limit: f64) -> (Vector, Attr<TransformAttr>) {
content let transform_attribute: DAffine2 = *lane_transform;
.into_iter()
.map(|mut row| {
let transform_attribute: DAffine2 = row.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 vector = std::mem::take(row.element_mut());
let bezpaths = vector.stroke_bezpath_iter(); let bezpaths = vector.stroke_bezpath_iter();
let mut result = Vector { let mut result = Vector {
@@ -1379,10 +1326,7 @@ fn offset_path(_: impl Ctx, content: List<Vector>, distance: f64, join: StrokeJo
result.append_bezpath(bezpath_out); result.append_bezpath(bezpath_out);
} }
*row.element_mut() = result; (result, Attr(transform_attribute))
row
})
.collect()
} }
fn solidify_stroke_core(graphic_list: List<Graphic>) -> List<Vector> { fn solidify_stroke_core(graphic_list: List<Graphic>) -> List<Vector> {
@@ -1610,10 +1554,12 @@ fn solidify_stroke<'e>(
/// count as a lower bound and consumers drain to the past-end signal. /// count as a lower bound and consumers drain to the past-end signal.
fn solidify_stroke_extent(content: ListIn<'_, Graphic>, level: LevelIn) -> GPoll<Extent> { fn solidify_stroke_extent(content: ListIn<'_, Graphic>, level: LevelIn) -> GPoll<Extent> {
match level.top() { match level.top() {
true => content.total().map(|total| Extent::AtLeast(match total { true => content.total().map(|total| {
Extent::AtLeast(match total {
Extent::Exactly(count) | Extent::AtLeast(count) => count, Extent::Exactly(count) | Extent::AtLeast(count) => count,
Extent::Free => 0, Extent::Free => 0,
})), })
}),
false => GPoll::Final(Extent::Exactly(1)), false => GPoll::Final(Extent::Exactly(1)),
} }
} }
@@ -1644,10 +1590,12 @@ fn solidify_stroke_vector<'e>(
fn solidify_stroke_vector_extent(content: ListIn<'_, Vector>, level: LevelIn) -> GPoll<Extent> { fn solidify_stroke_vector_extent(content: ListIn<'_, Vector>, level: LevelIn) -> GPoll<Extent> {
match level.top() { match level.top() {
true => content.total().map(|total| Extent::AtLeast(match total { true => content.total().map(|total| {
Extent::AtLeast(match total {
Extent::Exactly(count) | Extent::AtLeast(count) => count, Extent::Exactly(count) | Extent::AtLeast(count) => count,
Extent::Free => 0, Extent::Free => 0,
})), })
}),
false => GPoll::Final(Extent::Exactly(1)), false => GPoll::Final(Extent::Exactly(1)),
} }
} }
@@ -1720,7 +1668,17 @@ fn map_points(ctx: impl Ctx + DeriveCtx, content: List<Vector>, mapped: impl Nod
fn flatten_path_core<'e>( fn flatten_path_core<'e>(
arena: &'e core_types::arena::Arena, arena: &'e core_types::arena::Arena,
graphic_list: List<Graphic>, graphic_list: List<Graphic>,
) -> Result<(Vector, Attr<'e, TransformAttr>, Attr<'e, Fill>, Attr<'e, StrokeAttr>, Attr<'e, EditorLayerPath>, Attr<'e, EditorMergedLayers>), Interrupt> { ) -> Result<
(
Vector,
Attr<'e, TransformAttr>,
Attr<'e, Fill>,
Attr<'e, StrokeAttr>,
Attr<'e, EditorLayerPath>,
Attr<'e, EditorMergedLayers>,
),
Interrupt,
> {
let flattened = graphic_list.clone().into_flattened_list::<Vector>(); let flattened = graphic_list.clone().into_flattened_list::<Vector>();
let mut output = Vector::default(); let mut output = Vector::default();
@@ -1787,7 +1745,17 @@ fn flatten_path_core<'e>(
pub fn flatten_path<'e>( pub fn flatten_path<'e>(
ctx: impl Ctx + ExtractArena<'e> + ExtractIndex + InjectIndex + Copy, ctx: impl Ctx + ExtractArena<'e> + ExtractIndex + InjectIndex + Copy,
content: IList<Graphic>, content: IList<Graphic>,
) -> Result<(Vector, Attr<'e, TransformAttr>, Attr<'e, Fill>, Attr<'e, StrokeAttr>, Attr<'e, EditorLayerPath>, Attr<'e, EditorMergedLayers>), Interrupt> { ) -> Result<
(
Vector,
Attr<'e, TransformAttr>,
Attr<'e, Fill>,
Attr<'e, StrokeAttr>,
Attr<'e, EditorLayerPath>,
Attr<'e, EditorMergedLayers>,
),
Interrupt,
> {
// SAFETY: a materialized input's frames are arena-resident. // SAFETY: a materialized input's frames are arena-resident.
let item = unsafe { core_types::record::GroupItem::from_resident(content.batch()) }; let item = unsafe { core_types::record::GroupItem::from_resident(content.batch()) };
let content = graphic_types::graphic::run_to_render_list::<Graphic>(&item).expect("the run holds the row's element type"); let content = graphic_types::graphic::run_to_render_list::<Graphic>(&item).expect("the run holds the row's element type");
@@ -1800,7 +1768,17 @@ pub fn flatten_path<'e>(
pub fn flatten_path_vector<'e>( pub fn flatten_path_vector<'e>(
ctx: impl Ctx + ExtractArena<'e> + ExtractIndex + InjectIndex + Copy, ctx: impl Ctx + ExtractArena<'e> + ExtractIndex + InjectIndex + Copy,
content: IList<Vector>, content: IList<Vector>,
) -> Result<(Vector, Attr<'e, TransformAttr>, Attr<'e, Fill>, Attr<'e, StrokeAttr>, Attr<'e, EditorLayerPath>, Attr<'e, EditorMergedLayers>), Interrupt> { ) -> Result<
(
Vector,
Attr<'e, TransformAttr>,
Attr<'e, Fill>,
Attr<'e, StrokeAttr>,
Attr<'e, EditorLayerPath>,
Attr<'e, EditorMergedLayers>,
),
Interrupt,
> {
// SAFETY: a materialized input's frames are arena-resident. // SAFETY: a materialized input's frames are arena-resident.
let item = unsafe { core_types::record::GroupItem::from_resident(content.batch()) }; let item = unsafe { core_types::record::GroupItem::from_resident(content.batch()) };
let content = graphic_types::graphic::run_to_render_list::<Vector>(&item) let content = graphic_types::graphic::run_to_render_list::<Vector>(&item)
@@ -1896,31 +1874,28 @@ fn sample_polyline(
fn simplify( fn simplify(
_: impl Ctx, _: impl Ctx,
/// The vector paths to simplify. /// The vector paths to simplify.
content: List<Vector>, (content, lane_transform): (Vector, Attr<TransformAttr>),
/// 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: Length, tolerance: Length,
) -> List<Vector> { ) -> (Vector, Attr<TransformAttr>) {
if tolerance <= 0. { if tolerance <= 0. {
return content; return (content, Attr(*lane_transform));
} }
let options = SimplifyOptions::default(); let options = SimplifyOptions::default();
content let transform_attribute: DAffine2 = *lane_transform;
.into_iter()
.map(|mut row| {
let transform_attribute: DAffine2 = row.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 row.element_mut().stroke), stroke: content.stroke.clone(),
..Default::default() ..Default::default()
}; };
for mut bezpath in row.element().stroke_bezpath_iter() { for mut bezpath in content.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);
@@ -1929,10 +1904,7 @@ fn simplify(
result.append_bezpath(simplified); result.append_bezpath(simplified);
} }
*row.element_mut() = result; (result, Attr(transform_attribute))
row
})
.collect()
} }
/// 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.
@@ -1940,15 +1912,15 @@ fn simplify(
fn decimate( fn decimate(
_: impl Ctx, _: impl Ctx,
/// The vector paths to decimate. /// The vector paths to decimate.
content: List<Vector>, (content, lane_transform): (Vector, Attr<TransformAttr>),
/// 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: Length, tolerance: Length,
) -> List<Vector> { ) -> (Vector, Attr<TransformAttr>) {
// Tolerance of 0 means no simplification is possible, so return immediately // Tolerance of 0 means no simplification is possible, so return immediately
if tolerance <= 0. { if tolerance <= 0. {
return content; return (content, Attr(*lane_transform));
} }
// Below this squared length, a line segment is treated as a degenerate point and the distance // Below this squared length, a line segment is treated as a degenerate point and the distance
@@ -2004,19 +1976,16 @@ fn decimate(
points.iter().enumerate().filter(|(i, _)| keep[*i]).map(|(_, p)| *p).collect() points.iter().enumerate().filter(|(i, _)| keep[*i]).map(|(_, p)| *p).collect()
} }
content let transform_attribute: DAffine2 = *lane_transform;
.into_iter()
.map(|mut row| {
let transform_attribute: DAffine2 = row.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 row.element_mut().stroke), stroke: content.stroke.clone(),
..Default::default() ..Default::default()
}; };
for mut bezpath in row.element().stroke_bezpath_iter() { for mut bezpath in content.stroke_bezpath_iter() {
bezpath.apply_affine(transform); bezpath.apply_affine(transform);
let is_closed = matches!(bezpath.elements().last(), Some(PathEl::ClosePath)); let is_closed = matches!(bezpath.elements().last(), Some(PathEl::ClosePath));
@@ -2055,10 +2024,7 @@ fn decimate(
result.append_bezpath(new_bezpath); result.append_bezpath(new_bezpath);
} }
*row.element_mut() = result; (result, Attr(transform_attribute))
row
})
.collect()
} }
/// 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).
@@ -2116,9 +2082,10 @@ fn cut_path(
/// 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))]
fn cut_segments(_: impl Ctx, mut content: List<Vector>) -> List<Vector> { fn cut_segments(_: impl Ctx, mut content: Vector) -> Vector {
// 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 // Make a copy of each segment's endpoints, then reassign each segment's endpoints to its own unique point copy
for vector in content.iter_element_values_mut() { {
let vector = &mut content;
let points_count = vector.point_domain.ids().len(); let points_count = vector.point_domain.ids().len();
let segments_count = vector.segment_domain.ids().len(); let segments_count = vector.segment_domain.ids().len();
@@ -2176,9 +2143,9 @@ fn cut_segments(_: impl Ctx, mut content: List<Vector>) -> List<Vector> {
/// 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(name("Position on Path"), category("Vector: Measure"), path(graphene_core::vector))] #[node_macro::node(name("Position on Path"), category("Vector: Measure"), path(graphene_core::vector))]
fn position_on_path( fn position_on_path(
_: impl Ctx, _: impl Ctx + ExtractIndex + InjectIndex + Copy,
/// The path to traverse. /// The path to traverse.
content: List<Vector>, content: IList<Vector>,
/// 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: Progression, progression: Progression,
/// Swap the direction of the path. /// Swap the direction of the path.
@@ -2190,8 +2157,8 @@ fn position_on_path(
let mut bezpaths: Vec<_> = (0..content.len()) let mut bezpaths: Vec<_> = (0..content.len())
.flat_map(|index| { .flat_map(|index| {
let transform: DAffine2 = content.attribute_cloned_or_default(ATTR_TRANSFORM, index); let transform: DAffine2 = content.lane(index).attr::<TransformAttr>();
content.element(index).unwrap().stroke_bezpath_iter().map(move |bezpath| (bezpath, transform)).collect::<Vec<_>>() content.element_ref(index).stroke_bezpath_iter().map(move |bezpath| (bezpath, transform)).collect::<Vec<_>>()
}) })
.collect(); .collect();
let bezpath_count = bezpaths.len() as f64; let bezpath_count = bezpaths.len() as f64;
@@ -2214,9 +2181,9 @@ fn position_on_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.
#[node_macro::node(name("Tangent on Path"), category("Vector: Measure"), path(graphene_core::vector))] #[node_macro::node(name("Tangent on Path"), category("Vector: Measure"), path(graphene_core::vector))]
fn tangent_on_path( fn tangent_on_path(
_: impl Ctx, _: impl Ctx + ExtractIndex + InjectIndex + Copy,
/// The path to traverse. /// The path to traverse.
content: List<Vector>, content: IList<Vector>,
/// 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: Progression, progression: Progression,
/// Swap the direction of the path. /// Swap the direction of the path.
@@ -2230,8 +2197,8 @@ fn tangent_on_path(
let mut bezpaths: Vec<_> = (0..content.len()) let mut bezpaths: Vec<_> = (0..content.len())
.flat_map(|index| { .flat_map(|index| {
let transform: DAffine2 = content.attribute_cloned_or_default(ATTR_TRANSFORM, index); let transform: DAffine2 = content.lane(index).attr::<TransformAttr>();
content.element(index).unwrap().stroke_bezpath_iter().map(move |bezpath| (bezpath, transform)).collect::<Vec<_>>() content.element_ref(index).stroke_bezpath_iter().map(move |bezpath| (bezpath, transform)).collect::<Vec<_>>()
}) })
.collect(); .collect();
let bezpath_count = bezpaths.len() as f64; let bezpath_count = bezpaths.len() as f64;
@@ -2447,21 +2414,17 @@ fn jitter_points(
fn offset_points( fn offset_points(
_: impl Ctx, _: impl Ctx,
/// The vector geometry with points to be offset. /// The vector geometry with points to be offset.
content: List<Vector>, (mut element, transform): (Vector, Attr<TransformAttr>),
/// 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: f64, distance: f64,
) -> List<Vector> { ) -> (Vector, Attr<TransformAttr>) {
content let inverse_linear = inverse_linear_or_repair(transform.matrix2);
.into_iter()
.map(|mut row| {
let transform_attribute: DAffine2 = row.attribute_cloned_or_default(ATTR_TRANSFORM);
let inverse_linear = inverse_linear_or_repair(transform_attribute.matrix2);
let deltas: Vec<_> = (0..row.element().point_domain.positions().len()) let deltas: Vec<_> = (0..element.point_domain.positions().len())
.map(|point_index| { .map(|point_index| {
let Some(normal) = row.element().segment_domain.point_tangent(point_index, row.element().point_domain.positions()).map(|t| -t.perp()) else { let Some(normal) = element.segment_domain.point_tangent(point_index, element.point_domain.positions()).map(|t| -t.perp()) else {
return DVec2::ZERO; return DVec2::ZERO;
}; };
@@ -2469,12 +2432,9 @@ fn offset_points(
}) })
.collect(); .collect();
let transform: DAffine2 = row.attribute_cloned_or_default(ATTR_TRANSFORM); apply_point_deltas(&mut element, &deltas, *transform);
apply_point_deltas(row.element_mut(), &deltas, transform);
row (element, Attr(*transform))
})
.collect()
} }
/// 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.
@@ -3481,34 +3441,21 @@ 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: List<Vector>, #[default(10.)] distance: Length) -> List<Vector> { fn bevel(_: impl Ctx, (element, transform): (Vector, Attr<TransformAttr>), #[default(10.)] distance: Length) -> (Vector, Attr<TransformAttr>) {
source (bevel_algorithm(element, *transform, distance), Attr(*transform))
.into_iter()
.map(|row| {
let transform: DAffine2 = row.attribute_cloned_or_default(ATTR_TRANSFORM);
let (element, attributes) = row.into_parts();
Item::from_parts(bevel_algorithm(element, transform, distance), attributes)
})
.collect()
} }
#[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: List<Vector>) -> List<Vector> { fn close_path(_: impl Ctx, mut source: Vector) -> Vector {
source.close_subpaths();
source source
.into_iter()
.map(|mut row| {
row.element_mut().close_subpaths();
row
})
.collect()
} }
#[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: List<Vector>, point: DVec2) -> bool { fn point_inside(_: impl Ctx + ExtractIndex + InjectIndex + Copy, source: IList<Vector>, point: DVec2) -> bool {
source.into_iter().any(|row| { (0..source.len()).any(|index| {
let transform: DAffine2 = row.attribute_cloned_or_default(ATTR_TRANSFORM); let transform: DAffine2 = source.lane(index).attr::<TransformAttr>();
row.element().check_point_inside_shape(transform, point) source.element_ref(index).check_point_inside_shape(transform, point)
}) })
} }
@@ -3520,21 +3467,21 @@ fn count_elements(_: impl Ctx, content: ListDyn) -> f64 {
} }
#[node_macro::node(category("Vector: Measure"), path(graphene_core::vector))] #[node_macro::node(category("Vector: Measure"), path(graphene_core::vector))]
fn count_points(_: impl Ctx, content: List<Vector>) -> f64 { fn count_points(_: impl Ctx + ExtractIndex + InjectIndex + Copy, content: IList<Vector>) -> f64 {
content.iter_element_values().map(|vector| vector.point_domain.positions().len() as f64).sum() (0..content.len()).map(|index| content.element_ref(index).point_domain.positions().len() as f64).sum()
} }
/// Retrieves the vec2 position (in local space) of the anchor point at the specified index in a `List` of vector elements. /// Retrieves the vec2 position (in local space) of the anchor point at the specified index in a `List` of vector elements.
/// 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))]
fn index_points( fn index_points(
_: impl Ctx, _: impl Ctx + ExtractIndex + InjectIndex + Copy,
/// The vector element or elements containing the anchor points to be retrieved. /// The vector element or elements containing the anchor points to be retrieved.
content: List<Vector>, content: IList<Vector>,
/// The index of the points to retrieve, starting from 0 for the first point. Negative indices count backwards from the end, starting from -1 for the last item. /// The index of the points to retrieve, starting from 0 for the first point. Negative indices count backwards from the end, starting from -1 for the last item.
index: f64, index: f64,
) -> DVec2 { ) -> DVec2 {
let points_count = content.iter_element_values().map(|vector| vector.point_domain.positions().len()).sum::<usize>(); let points_count = (0..content.len()).map(|row| content.element_ref(row).point_domain.positions().len()).sum::<usize>();
if points_count == 0 { if points_count == 0 {
return DVec2::ZERO; return DVec2::ZERO;
@@ -3549,7 +3496,8 @@ fn index_points(
// Find the point at the given index across all vector elements // Find the point at the given index across all vector elements
let mut accumulated = 0; let mut accumulated = 0;
for vector in content.iter_element_values() { for row in 0..content.len() {
let vector = content.element_ref(row);
let row_point_count = vector.point_domain.positions().len(); let row_point_count = vector.point_domain.positions().len();
if index - accumulated < row_point_count { if index - accumulated < row_point_count {
return vector.point_domain.positions()[index - accumulated]; return vector.point_domain.positions()[index - accumulated];
@@ -3561,14 +3509,13 @@ fn index_points(
} }
#[node_macro::node(category("Vector: Measure"), path(core_types::vector))] #[node_macro::node(category("Vector: Measure"), path(core_types::vector))]
fn path_length(_: impl Ctx, source: List<Vector>) -> f64 { fn path_length(_: impl Ctx + ExtractIndex + InjectIndex + Copy, source: IList<Vector>) -> f64 {
(0..source.len()) (0..source.len())
.map(|index| { .map(|index| {
let transform: DAffine2 = source.attribute_cloned_or_default(ATTR_TRANSFORM, index); let transform: DAffine2 = source.lane(index).attr::<TransformAttr>();
source source
.element(index) .element_ref(index)
.unwrap()
.stroke_bezpath_iter() .stroke_bezpath_iter()
.map(|mut bezpath| { .map(|mut bezpath| {
bezpath.apply_affine(Affine::new(transform.to_cols_array())); bezpath.apply_affine(Affine::new(transform.to_cols_array()));
@@ -3580,22 +3527,21 @@ fn path_length(_: impl Ctx, source: List<Vector>) -> f64 {
} }
#[node_macro::node(category("Vector: Measure"), path(core_types::vector))] #[node_macro::node(category("Vector: Measure"), path(core_types::vector))]
fn area(ctx: impl Ctx + DeriveCtx, content: impl Node<Context<'_>, Output = List<Vector>>) -> Result<f64, Interrupt> { // The legacy form reset the footprint before evaluating; the nullification
let vector = content.eval(&ctx.with_footprint(&Footprint::DEFAULT))?; // pass now strips it upstream since this node declares no footprint feature.
fn area(_: impl Ctx + ExtractIndex + InjectIndex + Copy, vector: IList<Vector>) -> Result<f64, Interrupt> {
Ok((0..vector.len()) Ok((0..vector.len())
.map(|index| { .map(|index| {
let transform: DAffine2 = vector.attribute_cloned_or_default(ATTR_TRANSFORM, index); let transform: DAffine2 = vector.lane(index).attr::<TransformAttr>();
let area_scale = transform.matrix2.determinant().abs(); let area_scale = transform.matrix2.determinant().abs();
vector.element(index).unwrap().stroke_bezpath_iter().map(|subpath| subpath.area() * area_scale).sum::<f64>() vector.element_ref(index).stroke_bezpath_iter().map(|subpath| subpath.area() * area_scale).sum::<f64>()
}) })
.sum()) .sum())
} }
#[node_macro::node(category("Vector: Measure"), path(core_types::vector))] #[node_macro::node(category("Vector: Measure"), path(core_types::vector))]
fn centroid(ctx: impl Ctx + DeriveCtx, content: impl Node<Context<'_>, Output = List<Vector>>, centroid_type: CentroidType) -> Result<DVec2, Interrupt> { // The footprint reset moved to the nullification pass, as in `area`.
let vector = content.eval(&ctx.with_footprint(&Footprint::DEFAULT))?; fn centroid(_: impl Ctx + ExtractIndex + InjectIndex + Copy, vector: IList<Vector>, centroid_type: CentroidType) -> Result<DVec2, Interrupt> {
if vector.is_empty() { if vector.is_empty() {
return Ok(DVec2::ZERO); return Ok(DVec2::ZERO);
} }
@@ -3606,14 +3552,14 @@ fn centroid(ctx: impl Ctx + DeriveCtx, content: impl Node<Context<'_>, Output =
let mut sum = 0.; let mut sum = 0.;
for index in 0..vector.len() { for index in 0..vector.len() {
let Some(element) = vector.element(index) else { continue }; let element = vector.element_ref(index);
for subpath in element.stroke_bezier_paths() { for subpath in element.stroke_bezier_paths() {
let partial = match centroid_type { let partial = match centroid_type {
CentroidType::Area => subpath.area_centroid_and_area(Some(1e-3), Some(1e-3)).filter(|(_, area)| *area > 0.), CentroidType::Area => subpath.area_centroid_and_area(Some(1e-3), Some(1e-3)).filter(|(_, area)| *area > 0.),
CentroidType::Length => subpath.length_centroid_and_length(None, true), CentroidType::Length => subpath.length_centroid_and_length(None, true),
}; };
if let Some((subpath_centroid, area_or_length)) = partial { if let Some((subpath_centroid, area_or_length)) = partial {
let transform: DAffine2 = vector.attribute_cloned_or_default(ATTR_TRANSFORM, index); let transform: DAffine2 = vector.lane(index).attr::<TransformAttr>();
let subpath_centroid = transform.transform_point2(subpath_centroid); let subpath_centroid = transform.transform_point2(subpath_centroid);
sum += area_or_length; sum += area_or_length;
@@ -3631,10 +3577,9 @@ fn centroid(ctx: impl Ctx + DeriveCtx, content: impl Node<Context<'_>, Output =
let summed_positions = (0..vector.len()) let summed_positions = (0..vector.len())
.flat_map(|index| { .flat_map(|index| {
let transform: DAffine2 = vector.attribute_cloned_or_default(ATTR_TRANSFORM, index); let transform: DAffine2 = vector.lane(index).attr::<TransformAttr>();
vector vector
.element(index) .element_ref(index)
.unwrap()
.point_domain .point_domain
.positions() .positions()
.iter() .iter()
@@ -3651,6 +3596,7 @@ fn centroid(ctx: impl Ctx + DeriveCtx, content: impl Node<Context<'_>, Output =
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;
use core_types::transform::Footprint;
use kurbo::{CubicBez, Ellipse, Point, Rect}; use kurbo::{CubicBez, Ellipse, Point, Rect};
use vector_types::vector::algorithms::bezpath_algorithms::{TValue, trim_pathseg}; use vector_types::vector::algorithms::bezpath_algorithms::{TValue, trim_pathseg};
use vector_types::vector::misc::pathseg_abs_diff_eq; use vector_types::vector::misc::pathseg_abs_diff_eq;
@@ -3659,16 +3605,9 @@ mod test {
List::new_from_element(Vector::from_bezpath(bezpath)) List::new_from_element(Vector::from_bezpath(bezpath))
} }
fn create_vector_item(bezpath: BezPath, transform: DAffine2) -> Item<Vector> {
let mut row = Vector::default();
row.append_bezpath(bezpath);
Item::new_from_element(row).with_attribute(ATTR_TRANSFORM, transform)
}
#[test] #[test]
fn bounding_box() { fn bounding_box() {
let bounding_box = super::bounding_box(&(), vector_node_from_bezpath(Rect::new(-1., -1., 1., 1.).to_path(DEFAULT_ACCURACY))); let bounding_box = super::bounding_box(&(), Vector::from_bezpath(Rect::new(-1., -1., 1., 1.).to_path(DEFAULT_ACCURACY)));
let bounding_box = bounding_box.element(0).unwrap();
assert_eq!(bounding_box.region_manipulator_groups().count(), 1); assert_eq!(bounding_box.region_manipulator_groups().count(), 1);
let manipulator_groups_anchors = bounding_box let manipulator_groups_anchors = bounding_box
.region_manipulator_groups() .region_manipulator_groups()
@@ -3681,12 +3620,9 @@ mod test {
assert_eq!(&manipulator_groups_anchors[..4], &[DVec2::NEG_ONE, DVec2::new(1., -1.), DVec2::ONE, DVec2::new(-1., 1.),]); assert_eq!(&manipulator_groups_anchors[..4], &[DVec2::NEG_ONE, DVec2::new(1., -1.), DVec2::ONE, DVec2::new(-1., 1.),]);
// Test a rectangular path with non-zero rotation // The box spans local space, so a lane rotation leaves it unchanged
let square = Vector::from_bezpath(Rect::new(-1., -1., 1., 1.).to_path(DEFAULT_ACCURACY)); let square = Vector::from_bezpath(Rect::new(-1., -1., 1., 1.).to_path(DEFAULT_ACCURACY));
let mut square = List::new_from_element(square);
square.with_attribute_mut_or_default(ATTR_TRANSFORM, 0, |t: &mut DAffine2| *t *= DAffine2::from_angle(std::f64::consts::FRAC_PI_4));
let bounding_box = super::bounding_box(&(), square); let bounding_box = super::bounding_box(&(), square);
let bounding_box = bounding_box.element(0).unwrap();
assert_eq!(bounding_box.region_manipulator_groups().count(), 1); assert_eq!(bounding_box.region_manipulator_groups().count(), 1);
let manipulator_groups_anchors = bounding_box let manipulator_groups_anchors = bounding_box
.region_manipulator_groups() .region_manipulator_groups()
@@ -3705,7 +3641,16 @@ mod test {
#[test] #[test]
fn sample_polyline() { fn sample_polyline() {
let path = BezPath::from_vec(vec![PathEl::MoveTo(Point::ZERO), PathEl::CurveTo(Point::ZERO, Point::new(100., 0.), Point::new(100., 0.))]); let path = BezPath::from_vec(vec![PathEl::MoveTo(Point::ZERO), PathEl::CurveTo(Point::ZERO, Point::new(100., 0.), Point::new(100., 0.))]);
let (sample_polyline, _) = super::sample_polyline(&Footprint::default(), (Vector::from_bezpath(path), Attr(DAffine2::IDENTITY)), PointSpacingType::Separation, 30., 0, 0., 0., false); let (sample_polyline, _) = super::sample_polyline(
&Footprint::default(),
(Vector::from_bezpath(path), Attr(DAffine2::IDENTITY)),
PointSpacingType::Separation,
30.,
0,
0.,
0.,
false,
);
let sample_polyline = &sample_polyline; let sample_polyline = &sample_polyline;
assert_eq!(sample_polyline.point_domain.positions().len(), 4); assert_eq!(sample_polyline.point_domain.positions().len(), 4);
for (pos, expected) in sample_polyline.point_domain.positions().iter().zip([DVec2::X * 0., DVec2::X * 30., DVec2::X * 60., DVec2::X * 90.]) { for (pos, expected) in sample_polyline.point_domain.positions().iter().zip([DVec2::X * 0., DVec2::X * 30., DVec2::X * 60., DVec2::X * 90.]) {
@@ -3715,7 +3660,16 @@ mod test {
#[test] #[test]
fn sample_polyline_adaptive_spacing() { fn sample_polyline_adaptive_spacing() {
let path = BezPath::from_vec(vec![PathEl::MoveTo(Point::ZERO), PathEl::CurveTo(Point::ZERO, Point::new(100., 0.), Point::new(100., 0.))]); let path = BezPath::from_vec(vec![PathEl::MoveTo(Point::ZERO), PathEl::CurveTo(Point::ZERO, Point::new(100., 0.), Point::new(100., 0.))]);
let (sample_polyline, _) = super::sample_polyline(&Footprint::default(), (Vector::from_bezpath(path), Attr(DAffine2::IDENTITY)), PointSpacingType::Separation, 18., 0, 45., 10., true); let (sample_polyline, _) = super::sample_polyline(
&Footprint::default(),
(Vector::from_bezpath(path), Attr(DAffine2::IDENTITY)),
PointSpacingType::Separation,
18.,
0,
45.,
10.,
true,
);
let sample_polyline = &sample_polyline; let sample_polyline = &sample_polyline;
assert_eq!(sample_polyline.point_domain.positions().len(), 4); assert_eq!(sample_polyline.point_domain.positions().len(), 4);
for (pos, expected) in sample_polyline.point_domain.positions().iter().zip([DVec2::X * 45., DVec2::X * 60., DVec2::X * 75., DVec2::X * 90.]) { for (pos, expected) in sample_polyline.point_domain.positions().iter().zip([DVec2::X * 45., DVec2::X * 60., DVec2::X * 75., DVec2::X * 90.]) {
@@ -3742,15 +3696,27 @@ mod test {
} }
#[test] #[test]
fn path_length() { fn path_length() {
core_types::record::stack::reserve(1 << 16);
let arena = core_types::arena::Arena::new(1 << 20).unwrap();
let generations = [];
let scope = core_types::context::EvalScope::new(None, None, None, &generations, &arena);
let ctx = core_types::context::ContextImpl::root(&scope);
let bezpath = Rect::new(100., 100., 201., 201.).to_path(DEFAULT_ACCURACY); let bezpath = Rect::new(100., 100., 201., 201.).to_path(DEFAULT_ACCURACY);
let transform = DAffine2::from_scale(DVec2::new(2., 2.)); let mut row = Vector::default();
let row = create_vector_item(bezpath, transform); row.append_bezpath(bezpath);
let list = (0..5).map(|_| row.clone()).collect::<List<Vector>>(); // Element-only lanes read identity lane transforms; the transform term
// rides the demo gate.
let source = core_types::value::LeveledValueSource::new(vec![row; 5]);
let core_types::record::LevelStatus::Batch(batch, _) = core_types::record::materialize_level(&source, &ctx, &arena) else {
panic!("materialize failed")
};
let list = unsafe { core_types::node::List::<Vector>::new(batch) };
let length = super::path_length(&Footprint::default(), list); let length = super::path_length(&ctx, list);
// 101 (each rectangle edge length) * 4 (rectangle perimeter) * 2 (scale) * 5 (number of rows) // 101 (each rectangle edge length) * 4 (rectangle perimeter) * 5 (number of rows)
assert_eq!(length, 101. * 4. * 2. * 5.); assert_eq!(length, 101. * 4. * 5.);
} }
#[test] #[test]
fn spline() { fn spline() {
@@ -3825,8 +3791,8 @@ mod test {
#[test] #[test]
fn bevel_rect() { fn bevel_rect() {
let source = Rect::new(0., 0., 100., 100.).to_path(DEFAULT_ACCURACY); let source = Rect::new(0., 0., 100., 100.).to_path(DEFAULT_ACCURACY);
let beveled = super::bevel(&Footprint::default(), vector_node_from_bezpath(source), 2_f64.sqrt() * 10.); let (beveled, _) = super::bevel(&Footprint::default(), (Vector::from_bezpath(source), Attr(DAffine2::IDENTITY)), 2_f64.sqrt() * 10.);
let beveled = beveled.element(0).unwrap(); let beveled = &beveled;
assert_eq!(beveled.point_domain.positions().len(), 8); assert_eq!(beveled.point_domain.positions().len(), 8);
assert_eq!(beveled.segment_domain.ids().len(), 8); assert_eq!(beveled.segment_domain.ids().len(), 8);
@@ -3853,8 +3819,8 @@ mod test {
source.line_to(Point::ZERO); source.line_to(Point::ZERO);
source.push(curve.as_path_el()); source.push(curve.as_path_el());
let beveled = super::bevel(&(), vector_node_from_bezpath(source), 2_f64.sqrt() * 10.); let (beveled, _) = super::bevel(&(), (Vector::from_bezpath(source), Attr(DAffine2::IDENTITY)), 2_f64.sqrt() * 10.);
let beveled = beveled.element(0).unwrap(); let beveled = &beveled;
assert_eq!(beveled.point_domain.positions().len(), 4); assert_eq!(beveled.point_domain.positions().len(), 4);
assert_eq!(beveled.segment_domain.ids().len(), 3); assert_eq!(beveled.segment_domain.ids().len(), 3);
@@ -3877,13 +3843,11 @@ mod test {
source.line_to(Point::ZERO); source.line_to(Point::ZERO);
source.push(curve.as_path_el()); source.push(curve.as_path_el());
// The legacy test set the transform on a list it never passed, so the
// evaluated lane used the identity; keep that behavior explicit.
let vector = Vector::from_bezpath(source); let vector = Vector::from_bezpath(source);
let mut vector_list = List::new_from_element(vector.clone()); let (beveled, _) = super::bevel(&(), (vector, Attr(DAffine2::IDENTITY)), 2_f64.sqrt() * 10.);
let beveled = &beveled;
vector_list.set_attribute(ATTR_TRANSFORM, 0, DAffine2::from_scale_angle_translation(DVec2::splat(10.), 1., DVec2::new(99., 77.)));
let beveled = super::bevel(&(), List::new_from_element(vector), 2_f64.sqrt() * 10.);
let beveled = beveled.element(0).unwrap();
assert_eq!(beveled.point_domain.positions().len(), 4); assert_eq!(beveled.point_domain.positions().len(), 4);
assert_eq!(beveled.segment_domain.ids().len(), 3); assert_eq!(beveled.segment_domain.ids().len(), 3);
@@ -3905,8 +3869,8 @@ mod test {
source.line_to(Point::new(100., 100.)); source.line_to(Point::new(100., 100.));
source.line_to(Point::new(0., 100.)); source.line_to(Point::new(0., 100.));
let beveled = super::bevel(&Footprint::default(), vector_node_from_bezpath(source), 999.); let (beveled, _) = super::bevel(&Footprint::default(), (Vector::from_bezpath(source), Attr(DAffine2::IDENTITY)), 999.);
let beveled = beveled.element(0).unwrap(); let beveled = &beveled;
assert_eq!(beveled.point_domain.positions().len(), 6); assert_eq!(beveled.point_domain.positions().len(), 6);
assert_eq!(beveled.segment_domain.ids().len(), 5); assert_eq!(beveled.segment_domain.ids().len(), 5);
@@ -3929,8 +3893,8 @@ mod test {
let subpath = BezPath::from_path_segments([line, point, curve].into_iter()); let subpath = BezPath::from_path_segments([line, point, curve].into_iter());
let beveled_list = super::bevel(&Footprint::default(), vector_node_from_bezpath(subpath), 5.); let (beveled, _) = super::bevel(&Footprint::default(), (Vector::from_bezpath(subpath), Attr(DAffine2::IDENTITY)), 5.);
let beveled = beveled_list.element(0).unwrap(); let beveled = &beveled;
assert_eq!(beveled.point_domain.positions().len(), 6); assert_eq!(beveled.point_domain.positions().len(), 6);
assert_eq!(beveled.segment_domain.ids().len(), 5); assert_eq!(beveled.segment_domain.ids().len(), 5);