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