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::list::List;
use core_types::uuid::NodeId;
use core_types::{ATTR_TRANSFORM, Ctx, ExtractIndex, InjectIndex};
use core_types::{Ctx, ExtractIndex, InjectIndex};
use glam::DAffine2;
use graphic_types::Vector;
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.
#[node_macro::node(category("Vector"))]
fn apply_transform(_ctx: impl Ctx, mut vector: List<Vector>) -> List<Vector> {
let (elements, transforms) = vector.element_and_attribute_slices_mut::<DAffine2>(ATTR_TRANSFORM);
for (element, transform) in elements.iter_mut().zip(transforms.iter_mut()) {
for (_, point) in element.point_domain.positions_mut() {
fn apply_transform(_ctx: impl Ctx, (mut vector, transform): (Vector, Attr<TransformAttr>)) -> (Vector, Attr<TransformAttr>) {
let transform: DAffine2 = *transform;
for (_, point) in vector.point_domain.positions_mut() {
*point = transform.transform_point2(*point);
}
element.segment_domain.transform(*transform);
vector.segment_domain.transform(transform);
*transform = DAffine2::IDENTITY;
}
vector
(vector, Attr(DAffine2::IDENTITY))
}
+208 -244
View File
@@ -1,24 +1,24 @@
use core::cmp::Ordering;
use core::f64::consts::{PI, TAU};
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::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::extent::{ExtentIn, LevelIn, ListIn, ValueIn};
use core_types::gpoll::{Extent, GPoll};
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 graphic_types::markers::{EditorMergedLayers, Fill, Stroke as StrokeAttr};
use core_types::attribute::Transform as TransformAttr;
use glam::{DAffine2, DMat2, DVec2};
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::markers::{EditorMergedLayers, Fill, Stroke as StrokeAttr};
use graphic_types::raster_types::{CPU, GPU, Raster};
use graphic_types::{ATTR_EDITOR_MERGED_LAYERS, ATTR_FILL, ATTR_STROKE, Graphic, IntoGraphicList};
use kurbo::simplify::{SimplifyOptions, simplify_bezpath};
@@ -27,7 +27,6 @@ use rand::{Rng, SeedableRng};
use std::collections::hash_map::DefaultHasher;
use vector_types::gradient::{build_transform_with_y_preservation, initial_gradient_transform_for_bounding_box};
use vector_types::subpath::{BezierHandles, ManipulatorGroup};
use vector_types::{ATTR_GRADIENT_TYPE, ATTR_SPREAD_METHOD};
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::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::{FillId, PointId, RegionId, SegmentDomain, SegmentId, StrokeId, VectorExt};
use vector_types::{ATTR_GRADIENT_TYPE, ATTR_SPREAD_METHOD};
use vector_types::{GradientSpreadMethod, GradientType};
/// 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());
}
let element = content.element_ref(lane).clone();
let park_existing = |paint: Option<&List<Graphic>>| -> Result<Option<&'e List<Graphic>>, Interrupt> {
paint.map(|paint| park_paint(ctx.arena(), paint.clone())).transpose()
};
let park_existing = |paint: Option<&List<Graphic>>| -> Result<Option<&'e List<Graphic>>, Interrupt> { paint.map(|paint| park_paint(ctx.arena(), paint.clone())).transpose() };
let existing_fill = park_existing(content.lane(lane).attr::<Fill>())?;
let existing_stroke = park_existing(content.lane(lane).attr::<StrokeAttr>())?;
let carried = carried_lane_attrs(ctx.arena(), content.lane(lane))?;
@@ -161,8 +159,7 @@ fn assign_colors_extent(
fn assign_colors_graphic<'e>(
ctx: impl Ctx + ExtractArena<'e> + ExtractIndex + InjectIndex + Copy,
content: IList<Graphic>,
#[default(true)]
fill: bool,
#[default(true)] fill: bool,
stroke: bool,
gradient: IList<GradientStops>,
reverse: bool,
@@ -316,8 +313,7 @@ fn fill<'e>(
fn fill_graphic_leveled<'e>(
ctx: impl Ctx + ExtractArena<'e> + ExtractIndex + InjectIndex + Copy,
(element, _content_fill): (Graphic, Attr<'e, Fill>),
#[default(Color::BLACK)]
fill: IList<Graphic>,
#[default(Color::BLACK)] fill: IList<Graphic>,
_backup_color: IList<Color>,
_backup_gradient: IList<GradientStops>,
_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>(
ctx: impl Ctx + ExtractArena<'e> + ExtractIndex + InjectIndex + Copy,
(element, content_transform): (Graphic, Attr<TransformAttr>),
#[default(Color::BLACK)]
paint: IList<Graphic>,
#[default(Color::BLACK)] paint: IList<Graphic>,
#[unit(" px")]
#[default(2.)]
weight: f64,
align: StrokeAlign,
cap: StrokeCap,
join: StrokeJoin,
#[default(4.)]
miter_limit: f64,
#[default(4.)] miter_limit: f64,
paint_order: PaintOrder,
dash_lengths: IList<f64>,
#[unit(" px")]
dash_offset: f64,
#[unit(" px")] dash_offset: f64,
) -> 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 stroke = Stroke {
@@ -570,7 +563,7 @@ fn copy_to_points_extent(
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
fn round_corners(
_: impl Ctx,
source: List<Vector>,
(source, transform): (Vector, Attr<TransformAttr>),
#[hard(0..)]
#[default(10.)]
radius: PixelLength,
@@ -583,13 +576,9 @@ fn round_corners(
#[hard(0..180)]
#[default(5.)]
min_angle_threshold: Angle,
) -> List<Vector> {
(0..source.len())
.map(|index| {
let source_transform: DAffine2 = source.attribute_cloned_or_default(ATTR_TRANSFORM, index);
) -> (Vector, Attr<TransformAttr>) {
let source_transform: DAffine2 = *transform;
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
let roundness = 1. - roundness;
@@ -674,37 +663,23 @@ fn round_corners(
result.append_bezpath(rounded_subpath);
}
Item::from_parts(result, attributes)
})
.collect()
(result, Attr(source_transform))
}
#[node_macro::node(name("Merge by Distance"), category("Vector: Modifier"), path(core_types::vector))]
pub fn merge_by_distance(
_: impl Ctx,
content: List<Vector>,
(mut content, transform): (Vector, Attr<TransformAttr>),
#[default(0.1)]
#[hard(0.0001..)]
distance: PixelLength,
algorithm: MergeByDistanceAlgorithm,
) -> List<Vector> {
) -> (Vector, Attr<TransformAttr>) {
match algorithm {
MergeByDistanceAlgorithm::Spatial => content
.into_iter()
.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(),
MergeByDistanceAlgorithm::Spatial => content.merge_by_distance_spatial(*transform, distance),
MergeByDistanceAlgorithm::Topological => content.merge_by_distance_topological(distance),
}
(content, Attr(*transform))
}
pub mod extrude_algorithms {
@@ -905,23 +880,19 @@ pub mod extrude_algorithms {
}
#[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> {
for vector in source.iter_element_values_mut() {
extrude_algorithms::extrude(vector, direction, joining_algorithm);
}
fn extrude(_: impl Ctx, mut source: Vector, direction: DVec2, joining_algorithm: ExtrudeJoiningAlgorithm) -> Vector {
extrude_algorithms::extrude(&mut source, direction, joining_algorithm);
source
}
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
fn box_warp(_: impl Ctx, content: List<Vector>, #[expose] rectangle: List<Vector>) -> List<Vector> {
let Some(target) = rectangle.element(0).cloned() else { return content };
let target_transform: DAffine2 = rectangle.attribute_cloned_or_default(ATTR_TRANSFORM, 0);
content
.into_iter()
.map(|mut row| {
let transform: DAffine2 = row.attribute_cloned_or_default(ATTR_TRANSFORM);
let vector = std::mem::take(row.element_mut());
fn box_warp(_: impl Ctx + ExtractIndex + InjectIndex + Copy, (vector, transform): (Vector, Attr<TransformAttr>), #[expose] rectangle: IList<Vector>) -> (Vector, Attr<TransformAttr>) {
if rectangle.is_empty() {
return (vector, Attr(*transform));
}
let target = rectangle.element_ref(0);
let target_transform: DAffine2 = rectangle.lane(0).attr::<TransformAttr>();
let transform: DAffine2 = *transform;
// Get the bounding box of the source vector geometry
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);
// Add this to the `List` and reset the transform since we've applied it directly to the points
*row.element_mut() = result;
row.set_attribute(ATTR_TRANSFORM, DAffine2::IDENTITY);
row
})
.collect()
// Reset the transform since we've applied it directly to the points
(result, Attr(DAffine2::IDENTITY))
}
// 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))]
fn auto_tangents(
_: 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).
#[default(0.5)]
#[range]
@@ -1130,12 +1097,8 @@ fn auto_tangents(
/// If active, existing non-zero handles won't be affected.
#[default(true)]
preserve_existing: bool,
) -> List<Vector> {
(0..source.len())
.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();
) -> (Vector, Attr<TransformAttr>) {
let transform: DAffine2 = *lane_transform;
let mut result = Vector {
stroke: source.stroke.clone(),
@@ -1267,18 +1230,11 @@ fn auto_tangents(
}
}
Item::from_parts(result, attributes)
})
.collect()
(result, Attr(transform))
}
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
fn bounding_box(_: impl Ctx, content: List<Vector>) -> List<Vector> {
content
.into_iter()
.map(|mut row| {
let vector = std::mem::take(row.element_mut());
fn bounding_box(_: impl Ctx, vector: Vector) -> Vector {
let mut result = vector
.bounding_box_rect()
.map(|bbox| {
@@ -1291,16 +1247,13 @@ fn bounding_box(_: impl Ctx, content: List<Vector>) -> List<Vector> {
result.stroke = vector.stroke.clone();
result.set_stroke_transform(DAffine2::IDENTITY);
*row.element_mut() = result;
row
})
.collect()
result
}
#[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())
.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)])
.map(|[top_left, bottom_right]| bottom_right - top_left)
.unwrap_or_default()
@@ -1308,18 +1261,17 @@ fn dimensions(_: impl Ctx, content: List<Vector>) -> DVec2 {
/// Type-asserts a value to be vector data.
#[node_macro::node(category("Vector"), name("As Vector"), path(core_types::vector))]
fn as_vector(_: impl Ctx, value: List<Vector>) -> List<Vector> {
fn as_vector(_: impl Ctx, value: Vector) -> Vector {
value
}
/// 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))]
fn points_to_polyline(_: impl Ctx, mut points: List<Vector>, #[default(true)] closed: bool) -> List<Vector> {
for vector in points.iter_element_values_mut() {
fn points_to_polyline(_: impl Ctx, mut points: Vector, #[default(true)] closed: bool) -> Vector {
let mut segment_domain = SegmentDomain::new();
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 {
(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 {
segment_domain.push(next_id.next_id(), points_count - 1, 0, BezierHandles::Linear, StrokeId::generate());
vector
points
.region_domain
.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
}
#[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> {
content
.into_iter()
.map(|mut row| {
let transform_attribute: DAffine2 = row.attribute_cloned_or_default(ATTR_TRANSFORM);
fn offset_path(_: impl Ctx, (vector, lane_transform): (Vector, Attr<TransformAttr>), distance: f64, join: StrokeJoin, #[default(4.)] miter_limit: f64) -> (Vector, Attr<TransformAttr>) {
let transform_attribute: DAffine2 = *lane_transform;
let transform = Affine::new(transform_attribute.to_cols_array());
let vector = std::mem::take(row.element_mut());
let bezpaths = vector.stroke_bezpath_iter();
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);
}
*row.element_mut() = result;
row
})
.collect()
(result, Attr(transform_attribute))
}
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.
fn solidify_stroke_extent(content: ListIn<'_, Graphic>, level: LevelIn) -> GPoll<Extent> {
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::Free => 0,
})),
})
}),
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> {
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::Free => 0,
})),
})
}),
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>(
arena: &'e core_types::arena::Arena,
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 mut output = Vector::default();
@@ -1787,7 +1745,17 @@ fn flatten_path_core<'e>(
pub fn flatten_path<'e>(
ctx: impl Ctx + ExtractArena<'e> + ExtractIndex + InjectIndex + Copy,
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.
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");
@@ -1800,7 +1768,17 @@ pub fn flatten_path<'e>(
pub fn flatten_path_vector<'e>(
ctx: impl Ctx + ExtractArena<'e> + ExtractIndex + InjectIndex + Copy,
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.
let item = unsafe { core_types::record::GroupItem::from_resident(content.batch()) };
let content = graphic_types::graphic::run_to_render_list::<Vector>(&item)
@@ -1896,31 +1874,28 @@ fn sample_polyline(
fn simplify(
_: impl Ctx,
/// 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.
#[default(5.)]
#[unit(" px")]
tolerance: Length,
) -> List<Vector> {
) -> (Vector, Attr<TransformAttr>) {
if tolerance <= 0. {
return content;
return (content, Attr(*lane_transform));
}
let options = SimplifyOptions::default();
content
.into_iter()
.map(|mut row| {
let transform_attribute: DAffine2 = row.attribute_cloned_or_default(ATTR_TRANSFORM);
let transform_attribute: DAffine2 = *lane_transform;
let transform = Affine::new(transform_attribute.to_cols_array());
let inverse_transform = transform.inverse();
let mut result = Vector {
stroke: std::mem::take(&mut row.element_mut().stroke),
stroke: content.stroke.clone(),
..Default::default()
};
for mut bezpath in row.element().stroke_bezpath_iter() {
for mut bezpath in content.stroke_bezpath_iter() {
bezpath.apply_affine(transform);
let mut simplified = simplify_bezpath(bezpath, tolerance, &options);
@@ -1929,10 +1904,7 @@ fn simplify(
result.append_bezpath(simplified);
}
*row.element_mut() = result;
row
})
.collect()
(result, Attr(transform_attribute))
}
/// 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(
_: impl Ctx,
/// 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.
#[default(5.)]
#[unit(" px")]
tolerance: Length,
) -> List<Vector> {
) -> (Vector, Attr<TransformAttr>) {
// Tolerance of 0 means no simplification is possible, so return immediately
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
@@ -2004,19 +1976,16 @@ fn decimate(
points.iter().enumerate().filter(|(i, _)| keep[*i]).map(|(_, p)| *p).collect()
}
content
.into_iter()
.map(|mut row| {
let transform_attribute: DAffine2 = row.attribute_cloned_or_default(ATTR_TRANSFORM);
let transform_attribute: DAffine2 = *lane_transform;
let transform = Affine::new(transform_attribute.to_cols_array());
let inverse_transform = transform.inverse();
let mut result = Vector {
stroke: std::mem::take(&mut row.element_mut().stroke),
stroke: content.stroke.clone(),
..Default::default()
};
for mut bezpath in row.element().stroke_bezpath_iter() {
for mut bezpath in content.stroke_bezpath_iter() {
bezpath.apply_affine(transform);
let is_closed = matches!(bezpath.elements().last(), Some(PathEl::ClosePath));
@@ -2055,10 +2024,7 @@ fn decimate(
result.append_bezpath(new_bezpath);
}
*row.element_mut() = result;
row
})
.collect()
(result, Attr(transform_attribute))
}
/// 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.
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
fn cut_segments(_: impl Ctx, mut content: List<Vector>) -> List<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
for vector in content.iter_element_values_mut() {
fn cut_segments(_: impl Ctx, mut content: Vector) -> Vector {
// Make a copy of each segment's endpoints, then reassign each segment's endpoints to its own unique point copy
{
let vector = &mut content;
let points_count = vector.point_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.
#[node_macro::node(name("Position on Path"), category("Vector: Measure"), path(graphene_core::vector))]
fn position_on_path(
_: impl Ctx,
_: impl Ctx + ExtractIndex + InjectIndex + Copy,
/// 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.
progression: Progression,
/// Swap the direction of the path.
@@ -2190,8 +2157,8 @@ fn position_on_path(
let mut bezpaths: Vec<_> = (0..content.len())
.flat_map(|index| {
let transform: DAffine2 = content.attribute_cloned_or_default(ATTR_TRANSFORM, index);
content.element(index).unwrap().stroke_bezpath_iter().map(move |bezpath| (bezpath, transform)).collect::<Vec<_>>()
let transform: DAffine2 = content.lane(index).attr::<TransformAttr>();
content.element_ref(index).stroke_bezpath_iter().map(move |bezpath| (bezpath, transform)).collect::<Vec<_>>()
})
.collect();
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.
#[node_macro::node(name("Tangent on Path"), category("Vector: Measure"), path(graphene_core::vector))]
fn tangent_on_path(
_: impl Ctx,
_: impl Ctx + ExtractIndex + InjectIndex + Copy,
/// 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.
progression: Progression,
/// Swap the direction of the path.
@@ -2230,8 +2197,8 @@ fn tangent_on_path(
let mut bezpaths: Vec<_> = (0..content.len())
.flat_map(|index| {
let transform: DAffine2 = content.attribute_cloned_or_default(ATTR_TRANSFORM, index);
content.element(index).unwrap().stroke_bezpath_iter().map(move |bezpath| (bezpath, transform)).collect::<Vec<_>>()
let transform: DAffine2 = content.lane(index).attr::<TransformAttr>();
content.element_ref(index).stroke_bezpath_iter().map(move |bezpath| (bezpath, transform)).collect::<Vec<_>>()
})
.collect();
let bezpath_count = bezpaths.len() as f64;
@@ -2447,21 +2414,17 @@ fn jitter_points(
fn offset_points(
_: impl Ctx,
/// 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.
#[default(10.)]
#[unit(" px")]
distance: f64,
) -> List<Vector> {
content
.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);
) -> (Vector, Attr<TransformAttr>) {
let inverse_linear = inverse_linear_or_repair(transform.matrix2);
let deltas: Vec<_> = (0..row.element().point_domain.positions().len())
let deltas: Vec<_> = (0..element.point_domain.positions().len())
.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;
};
@@ -2469,12 +2432,9 @@ fn offset_points(
})
.collect();
let transform: DAffine2 = row.attribute_cloned_or_default(ATTR_TRANSFORM);
apply_point_deltas(row.element_mut(), &deltas, transform);
apply_point_deltas(&mut element, &deltas, *transform);
row
})
.collect()
(element, Attr(*transform))
}
/// 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))]
fn bevel(_: impl Ctx, source: List<Vector>, #[default(10.)] distance: Length) -> List<Vector> {
source
.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()
fn bevel(_: impl Ctx, (element, transform): (Vector, Attr<TransformAttr>), #[default(10.)] distance: Length) -> (Vector, Attr<TransformAttr>) {
(bevel_algorithm(element, *transform, distance), Attr(*transform))
}
#[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
.into_iter()
.map(|mut row| {
row.element_mut().close_subpaths();
row
})
.collect()
}
#[node_macro::node(category("Vector: Measure"), path(core_types::vector))]
fn point_inside(_: impl Ctx, source: List<Vector>, point: DVec2) -> bool {
source.into_iter().any(|row| {
let transform: DAffine2 = row.attribute_cloned_or_default(ATTR_TRANSFORM);
row.element().check_point_inside_shape(transform, point)
fn point_inside(_: impl Ctx + ExtractIndex + InjectIndex + Copy, source: IList<Vector>, point: DVec2) -> bool {
(0..source.len()).any(|index| {
let transform: DAffine2 = source.lane(index).attr::<TransformAttr>();
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))]
fn count_points(_: impl Ctx, content: List<Vector>) -> f64 {
content.iter_element_values().map(|vector| vector.point_domain.positions().len() as f64).sum()
fn count_points(_: impl Ctx + ExtractIndex + InjectIndex + Copy, content: IList<Vector>) -> f64 {
(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.
/// If no value exists at that index, the position (0, 0) is returned.
#[node_macro::node(category("Vector: Measure"), path(graphene_core::vector))]
fn index_points(
_: impl Ctx,
_: impl Ctx + ExtractIndex + InjectIndex + Copy,
/// 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.
index: f64,
) -> 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 {
return DVec2::ZERO;
@@ -3549,7 +3496,8 @@ fn index_points(
// Find the point at the given index across all vector elements
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();
if index - accumulated < row_point_count {
return vector.point_domain.positions()[index - accumulated];
@@ -3561,14 +3509,13 @@ fn index_points(
}
#[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())
.map(|index| {
let transform: DAffine2 = source.attribute_cloned_or_default(ATTR_TRANSFORM, index);
let transform: DAffine2 = source.lane(index).attr::<TransformAttr>();
source
.element(index)
.unwrap()
.element_ref(index)
.stroke_bezpath_iter()
.map(|mut bezpath| {
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))]
fn area(ctx: impl Ctx + DeriveCtx, content: impl Node<Context<'_>, Output = List<Vector>>) -> Result<f64, Interrupt> {
let vector = content.eval(&ctx.with_footprint(&Footprint::DEFAULT))?;
// The legacy form reset the footprint before evaluating; the nullification
// 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())
.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();
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())
}
#[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> {
let vector = content.eval(&ctx.with_footprint(&Footprint::DEFAULT))?;
// The footprint reset moved to the nullification pass, as in `area`.
fn centroid(_: impl Ctx + ExtractIndex + InjectIndex + Copy, vector: IList<Vector>, centroid_type: CentroidType) -> Result<DVec2, Interrupt> {
if vector.is_empty() {
return Ok(DVec2::ZERO);
}
@@ -3606,14 +3552,14 @@ fn centroid(ctx: impl Ctx + DeriveCtx, content: impl Node<Context<'_>, Output =
let mut sum = 0.;
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() {
let partial = match centroid_type {
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),
};
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);
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())
.flat_map(|index| {
let transform: DAffine2 = vector.attribute_cloned_or_default(ATTR_TRANSFORM, index);
let transform: DAffine2 = vector.lane(index).attr::<TransformAttr>();
vector
.element(index)
.unwrap()
.element_ref(index)
.point_domain
.positions()
.iter()
@@ -3651,6 +3596,7 @@ fn centroid(ctx: impl Ctx + DeriveCtx, content: impl Node<Context<'_>, Output =
#[cfg(test)]
mod test {
use super::*;
use core_types::transform::Footprint;
use kurbo::{CubicBez, Ellipse, Point, Rect};
use vector_types::vector::algorithms::bezpath_algorithms::{TValue, trim_pathseg};
use vector_types::vector::misc::pathseg_abs_diff_eq;
@@ -3659,16 +3605,9 @@ mod test {
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]
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 = bounding_box.element(0).unwrap();
let bounding_box = super::bounding_box(&(), Vector::from_bezpath(Rect::new(-1., -1., 1., 1.).to_path(DEFAULT_ACCURACY)));
assert_eq!(bounding_box.region_manipulator_groups().count(), 1);
let manipulator_groups_anchors = bounding_box
.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.),]);
// 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 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 = bounding_box.element(0).unwrap();
assert_eq!(bounding_box.region_manipulator_groups().count(), 1);
let manipulator_groups_anchors = bounding_box
.region_manipulator_groups()
@@ -3705,7 +3641,16 @@ mod test {
#[test]
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 (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;
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.]) {
@@ -3715,7 +3660,16 @@ mod test {
#[test]
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 (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;
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.]) {
@@ -3742,15 +3696,27 @@ mod test {
}
#[test]
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 transform = DAffine2::from_scale(DVec2::new(2., 2.));
let row = create_vector_item(bezpath, transform);
let list = (0..5).map(|_| row.clone()).collect::<List<Vector>>();
let mut row = Vector::default();
row.append_bezpath(bezpath);
// 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)
assert_eq!(length, 101. * 4. * 2. * 5.);
// 101 (each rectangle edge length) * 4 (rectangle perimeter) * 5 (number of rows)
assert_eq!(length, 101. * 4. * 5.);
}
#[test]
fn spline() {
@@ -3825,8 +3791,8 @@ mod test {
#[test]
fn bevel_rect() {
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 = beveled.element(0).unwrap();
let (beveled, _) = super::bevel(&Footprint::default(), (Vector::from_bezpath(source), Attr(DAffine2::IDENTITY)), 2_f64.sqrt() * 10.);
let beveled = &beveled;
assert_eq!(beveled.point_domain.positions().len(), 8);
assert_eq!(beveled.segment_domain.ids().len(), 8);
@@ -3853,8 +3819,8 @@ mod test {
source.line_to(Point::ZERO);
source.push(curve.as_path_el());
let beveled = super::bevel(&(), vector_node_from_bezpath(source), 2_f64.sqrt() * 10.);
let beveled = beveled.element(0).unwrap();
let (beveled, _) = super::bevel(&(), (Vector::from_bezpath(source), Attr(DAffine2::IDENTITY)), 2_f64.sqrt() * 10.);
let beveled = &beveled;
assert_eq!(beveled.point_domain.positions().len(), 4);
assert_eq!(beveled.segment_domain.ids().len(), 3);
@@ -3877,13 +3843,11 @@ mod test {
source.line_to(Point::ZERO);
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 mut vector_list = List::new_from_element(vector.clone());
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();
let (beveled, _) = super::bevel(&(), (vector, Attr(DAffine2::IDENTITY)), 2_f64.sqrt() * 10.);
let beveled = &beveled;
assert_eq!(beveled.point_domain.positions().len(), 4);
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(0., 100.));
let beveled = super::bevel(&Footprint::default(), vector_node_from_bezpath(source), 999.);
let beveled = beveled.element(0).unwrap();
let (beveled, _) = super::bevel(&Footprint::default(), (Vector::from_bezpath(source), Attr(DAffine2::IDENTITY)), 999.);
let beveled = &beveled;
assert_eq!(beveled.point_domain.positions().len(), 6);
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 beveled_list = super::bevel(&Footprint::default(), vector_node_from_bezpath(subpath), 5.);
let beveled = beveled_list.element(0).unwrap();
let (beveled, _) = super::bevel(&Footprint::default(), (Vector::from_bezpath(subpath), Attr(DAffine2::IDENTITY)), 5.);
let beveled = &beveled;
assert_eq!(beveled.point_domain.positions().len(), 6);
assert_eq!(beveled.segment_domain.ids().len(), 5);