Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions frontend/src/icons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,7 @@ import NodeNumbersToVec2 from "/../branding/assets/16x16-nodes/node-numbers-to-v
import NodeOffsetPath from "/../branding/assets/16x16-nodes/node-offset-path.svg";
import NodeOffsetPoints from "/../branding/assets/16x16-nodes/node-offset-points.svg";
import NodeOpacity from "/../branding/assets/16x16-nodes/node-opacity.svg";
import NodeOpenEnds from "/../branding/assets/16x16-nodes/node-open-ends.svg";
import NodeOriginsToPolyline from "/../branding/assets/16x16-nodes/node-origins-to-polyline.svg";
import NodePackStrips from "/../branding/assets/16x16-nodes/node-pack-strips.svg";
import NodePassthrough from "/../branding/assets/16x16-nodes/node-passthrough.svg";
Expand Down Expand Up @@ -552,6 +553,7 @@ export const ICON_16X16_NODES = {
NodeOffsetPath: { svg: NodeOffsetPath, size: 16 },
NodeOffsetPoints: { svg: NodeOffsetPoints, size: 16 },
NodeOpacity: { svg: NodeOpacity, size: 16 },
NodeOpenEnds: { svg: NodeOpenEnds, size: 16 },
NodeOriginsToPolyline: { svg: NodeOriginsToPolyline, size: 16 },
NodePackStrips: { svg: NodePackStrips, size: 16 },
NodePassthrough: { svg: NodePassthrough, size: 16 },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -430,9 +430,10 @@ impl SegmentGraph {

fn construct_face(&self, adjacency: &[Vec<FaceSide>], first: FaceSide, faces: &mut Faces, seen: &mut FaceSideSet) -> Option<()> {
faces.start_new_face();
// A tree's one orbit walks every side, so it takes all `2 * len` iterations to come back around
let max_iterations = self.len() * 2;
let mut side = first;
for _iteration in 1..max_iterations {
for _iteration in 0..max_iterations {
if seen.contains(side) {
return None;
}
Expand Down Expand Up @@ -1535,4 +1536,14 @@ mod tests {

vector.construct_faces(&paths);
}

#[test]
fn a_branching_tree_has_one_orbit_along_both_sides_of_every_segment() {
let points = [DVec2::new(0., 0.), DVec2::new(0., -10.), DVec2::new(-5., -15.), DVec2::new(5., -15.)];
let (vector, paths) = build_vector(&points, &[(0, 1), (1, 2), (1, 3)]);

let orbits = SegmentGraph::new(&vector, &paths).face_orbits();
assert_eq!(orbits.face_count(), 1);
assert_eq!(orbits.face_sides(0).len(), 6);
}
}
61 changes: 59 additions & 2 deletions node-graph/nodes/vector/src/vector_nodes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1527,6 +1527,8 @@ fn points_to_vector(
}

/// Lists the positions of the anchor points in the vector content, as placed by its transform. Wiring this into the **Translation** of a **Transform** node places a copy of its content at each point.
///
/// Each point keeps its attributes, so **Read Attribute** can read them alongside the positions.
#[node_macro::node(category("Vector: Point"), icon("NodeVectorToPoints"), name("Vector to Points"), path(core_types::vector))]
fn vector_to_points(
_: impl Ctx,
Expand All @@ -1535,9 +1537,36 @@ fn vector_to_points(
) -> List<DVec2> {
let transform: DAffine2 = content.attribute_cloned_or_default(ATTR_TRANSFORM);
let paths = shared_paths_from_item_attributes(&content);
let positions = content.element().anchor_positions(&paths).into_iter().map(|position| transform.transform_point2(position)).collect();
let vector = content.element();

vector.anchor_indices(&paths).into_iter().filter_map(|index| placed_point(vector, index, transform)).collect()
}

/// Lists the positions of the open ends in the vector content, where a path stops without meeting another, like the tips of a tree's branches.
///
/// Each point keeps its attributes, so **Read Attribute** can read them alongside the positions.
#[node_macro::node(category("Vector: Point"), icon("NodeOpenEnds"), path(core_types::vector))]
fn open_ends(
_: impl Ctx,
/// The vector content with paths to find the open ends of.
content: Item<Vector>,
) -> List<DVec2> {
let transform: DAffine2 = content.attribute_cloned_or_default(ATTR_TRANSFORM);
let paths = shared_paths_from_item_attributes(&content);
let vector = content.element();

// An open end is the anchor of exactly one segment, unlike a point along a path or a junction
let connectivity = vector.connectivity(&paths);
(0..vector.point_count())
.filter(|&index| connectivity.spokes(index as u32) == 1)
.filter_map(|index| placed_point(vector, index, transform))
.collect()
}

List::new_from_elements(positions)
/// The pool point at `index` with its attributes, placed by the transform.
fn placed_point(vector: &Vector, index: usize, transform: DAffine2) -> Option<Item<DVec2>> {
let (position, attributes) = vector.0.clone_item(index)?.into_parts();
Some(Item::from_parts(transform.transform_point2(position), attributes))
}

/// Creates a polyline from a series of vector points, replacing any existing segments that may already exist.
Expand Down Expand Up @@ -4267,6 +4296,34 @@ mod test {
assert_eq!(positions, [DVec2::new(-15., -13.), DVec2::new(25., -13.), DVec2::new(25., 27.), DVec2::new(-15., 27.)]);
}

#[test]
fn vector_to_points_and_open_ends_keep_point_attributes() {
use core_types::list::{ATTR_DEPTH, ATTR_HEADING};

// A trunk with a branch to the left, placed 100 px to the right
let tree = crate::turtle::turtle_path("F[+F]F", 10., 90., "FG", "f").with_attribute(ATTR_TRANSFORM, DAffine2::from_translation(DVec2::new(100., 0.)));

let points = super::vector_to_points((), tree.clone());
assert_eq!(points.iter_element_values().count(), 4);
assert_eq!(
points.iter_attribute_values::<i64>(ATTR_DEPTH).map(|depths| depths.copied().collect::<Vec<_>>()),
Some(vec![0, 0, 1, 0])
);

// The root and both tips, but not the junction where the branch leaves the trunk
let ends = super::open_ends((), tree);
let positions: Vec<DVec2> = ends.iter_element_values().copied().collect();
assert_eq!(positions, [DVec2::new(100., 0.), DVec2::new(90., -10.), DVec2::new(100., -20.)]);
assert_eq!(
ends.iter_attribute_values::<f64>(ATTR_HEADING).map(|headings| headings.copied().collect::<Vec<_>>()),
Some(vec![-90., 180., -90.])
);

// A closed path has no open ends
let square = crate::turtle::turtle_path("F+F+F+F", 10., 90., "FG", "f");
assert_eq!(super::open_ends((), square).len(), 0);
}

fn convex_hull_of(bezpath: BezPath, transform: DAffine2, curvature: f64) -> Item<Vector> {
let hull = super::convex_hull((), create_vector_item(bezpath, transform), item(curvature));
assert_eq!(hull.attribute_cloned_or_default::<DAffine2>(ATTR_TRANSFORM), transform, "the hull should keep its item's transform");
Expand Down
Loading