mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-23 12:08:12 +08:00
WIP
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
[package]
|
||||
name = "graphene-vector-nodes"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
description = "graphene vector nodes"
|
||||
authors = ["Graphite Authors <contact@graphite.rs>"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
[features]
|
||||
default = ["serde"]
|
||||
serde = [
|
||||
"dep:serde",
|
||||
"bezier-rs/serde",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
# Local dependencies
|
||||
dyn-any = { workspace = true }
|
||||
bezier-rs = { workspace = true }
|
||||
graphene-core = { workspace = true }
|
||||
graphene-vector = { workspace = true }
|
||||
node-macro = { workspace = true }
|
||||
|
||||
# Workspace dependencies
|
||||
kurbo = { workspace = true }
|
||||
glam = { workspace = true }
|
||||
specta = { workspace = true }
|
||||
log = { workspace = true }
|
||||
rustc-hash = { workspace = true }
|
||||
petgraph = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
|
||||
# Optional workspace dependencies
|
||||
serde = { workspace = true, optional = true, features = ["derive"] }
|
||||
|
||||
[dev-dependencies]
|
||||
# Workspace dependencies
|
||||
tokio = { workspace = true }
|
||||
@@ -0,0 +1,316 @@
|
||||
use super::poisson_disk::poisson_disk_sample;
|
||||
use crate::misc::{PointSpacingType, dvec2_to_point};
|
||||
use glam::DVec2;
|
||||
use kurbo::{BezPath, DEFAULT_ACCURACY, Line, ParamCurve, ParamCurveDeriv, PathEl, PathSeg, Point, Rect, Shape};
|
||||
|
||||
/// Splits the [`BezPath`] at `t` value which lie in the range of [0, 1].
|
||||
/// Returns [`None`] if the given [`BezPath`] has no segments or `t` is within f64::EPSILON of 0 or 1.
|
||||
pub fn split_bezpath(bezpath: &BezPath, t: f64, euclidian: bool) -> Option<(BezPath, BezPath)> {
|
||||
if t <= f64::EPSILON || (1. - t) <= f64::EPSILON || bezpath.segments().count() == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Get the segment which lies at the split.
|
||||
let (segment_index, t) = t_value_to_parametric(bezpath, t, euclidian, None);
|
||||
let segment = bezpath.get_seg(segment_index + 1).unwrap();
|
||||
|
||||
// Divide the segment.
|
||||
let first_segment = segment.subsegment(0.0..t);
|
||||
let second_segment = segment.subsegment(t..1.);
|
||||
|
||||
let mut first_bezpath = BezPath::new();
|
||||
let mut second_bezpath = BezPath::new();
|
||||
|
||||
// Append the segments up to the subdividing segment from original bezpath to first bezpath.
|
||||
for segment in bezpath.segments().take(segment_index) {
|
||||
if first_bezpath.elements().is_empty() {
|
||||
first_bezpath.move_to(segment.start());
|
||||
}
|
||||
first_bezpath.push(segment.as_path_el());
|
||||
}
|
||||
|
||||
// Append the first segment of the subdivided segment.
|
||||
if first_bezpath.elements().is_empty() {
|
||||
first_bezpath.move_to(first_segment.start());
|
||||
}
|
||||
first_bezpath.push(first_segment.as_path_el());
|
||||
|
||||
// Append the second segment of the subdivided segment in the second bezpath.
|
||||
if second_bezpath.elements().is_empty() {
|
||||
second_bezpath.move_to(second_segment.start());
|
||||
}
|
||||
second_bezpath.push(second_segment.as_path_el());
|
||||
|
||||
// Append the segments after the subdividing segment from original bezpath to second bezpath.
|
||||
for segment in bezpath.segments().skip(segment_index + 1) {
|
||||
if second_bezpath.elements().is_empty() {
|
||||
second_bezpath.move_to(segment.start());
|
||||
}
|
||||
second_bezpath.push(segment.as_path_el());
|
||||
}
|
||||
|
||||
Some((first_bezpath, second_bezpath))
|
||||
}
|
||||
|
||||
pub fn position_on_bezpath(bezpath: &BezPath, t: f64, euclidian: bool, segments_length: Option<&[f64]>) -> Point {
|
||||
let (segment_index, t) = t_value_to_parametric(bezpath, t, euclidian, segments_length);
|
||||
bezpath.get_seg(segment_index + 1).unwrap().eval(t)
|
||||
}
|
||||
|
||||
pub fn tangent_on_bezpath(bezpath: &BezPath, t: f64, euclidian: bool, segments_length: Option<&[f64]>) -> Point {
|
||||
let (segment_index, t) = t_value_to_parametric(bezpath, t, euclidian, segments_length);
|
||||
let segment = bezpath.get_seg(segment_index + 1).unwrap();
|
||||
match segment {
|
||||
PathSeg::Line(line) => line.deriv().eval(t),
|
||||
PathSeg::Quad(quad_bez) => quad_bez.deriv().eval(t),
|
||||
PathSeg::Cubic(cubic_bez) => cubic_bez.deriv().eval(t),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sample_polyline_on_bezpath(
|
||||
bezpath: BezPath,
|
||||
point_spacing_type: PointSpacingType,
|
||||
amount: f64,
|
||||
start_offset: f64,
|
||||
stop_offset: f64,
|
||||
adaptive_spacing: bool,
|
||||
segments_length: &[f64],
|
||||
) -> Option<BezPath> {
|
||||
let mut sample_bezpath = BezPath::new();
|
||||
|
||||
let was_closed = matches!(bezpath.elements().last(), Some(PathEl::ClosePath));
|
||||
|
||||
// Calculate the total length of the collected segments.
|
||||
let total_length: f64 = segments_length.iter().sum();
|
||||
|
||||
// Adjust the usable length by subtracting start and stop offsets.
|
||||
let mut used_length = total_length - start_offset - stop_offset;
|
||||
|
||||
// Sanity check that the usable length is positive.
|
||||
if used_length <= 0. {
|
||||
return None;
|
||||
}
|
||||
|
||||
const SAFETY_MAX_COUNT: f64 = 10_000. - 1.;
|
||||
|
||||
// Determine the number of points to generate along the path.
|
||||
let sample_count = match point_spacing_type {
|
||||
PointSpacingType::Separation => {
|
||||
let spacing = amount.min(used_length - f64::EPSILON);
|
||||
|
||||
if adaptive_spacing {
|
||||
// Calculate point count to evenly distribute points while covering the entire path.
|
||||
// With adaptive spacing, we widen or narrow the points as necessary to ensure the last point is always at the end of the path.
|
||||
(used_length / spacing).round().min(SAFETY_MAX_COUNT)
|
||||
} else {
|
||||
// Calculate point count based on exact spacing, which may not cover the entire path.
|
||||
// Without adaptive spacing, we just evenly space the points at the exact specified spacing, usually falling short before the end of the path.
|
||||
let count = (used_length / spacing + f64::EPSILON).floor().min(SAFETY_MAX_COUNT);
|
||||
if count != SAFETY_MAX_COUNT {
|
||||
used_length -= used_length % spacing;
|
||||
}
|
||||
count
|
||||
}
|
||||
}
|
||||
PointSpacingType::Quantity => (amount - 1.).floor().clamp(1., SAFETY_MAX_COUNT),
|
||||
};
|
||||
|
||||
// Skip if there are no points to generate.
|
||||
if sample_count < 1. {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Decide how many loop-iterations: if closed, skip the last duplicate point
|
||||
let sample_count_usize = sample_count as usize;
|
||||
let max_i = if was_closed { sample_count_usize } else { sample_count_usize + 1 };
|
||||
|
||||
// Generate points along the path based on calculated intervals.
|
||||
let mut length_up_to_previous_segment = 0.;
|
||||
let mut next_segment_index = 0;
|
||||
|
||||
for count in 0..max_i {
|
||||
let fraction = count as f64 / sample_count;
|
||||
let length_up_to_next_sample_point = fraction * used_length + start_offset;
|
||||
let mut next_length = length_up_to_next_sample_point - length_up_to_previous_segment;
|
||||
let mut next_segment_length = segments_length[next_segment_index];
|
||||
|
||||
// Keep moving to the next segment while the length up to the next sample point is greater than the length up to the current segment.
|
||||
while next_length > next_segment_length {
|
||||
if next_segment_index == segments_length.len() - 1 {
|
||||
break;
|
||||
}
|
||||
length_up_to_previous_segment += next_segment_length;
|
||||
next_length = length_up_to_next_sample_point - length_up_to_previous_segment;
|
||||
next_segment_index += 1;
|
||||
next_segment_length = segments_length[next_segment_index];
|
||||
}
|
||||
|
||||
let t = (next_length / next_segment_length).clamp(0., 1.);
|
||||
|
||||
let segment = bezpath.get_seg(next_segment_index + 1).unwrap();
|
||||
let t = eval_pathseg_euclidean(segment, t, DEFAULT_ACCURACY);
|
||||
let point = segment.eval(t);
|
||||
|
||||
if sample_bezpath.elements().is_empty() {
|
||||
sample_bezpath.move_to(point)
|
||||
} else {
|
||||
sample_bezpath.line_to(point)
|
||||
}
|
||||
}
|
||||
|
||||
if was_closed {
|
||||
sample_bezpath.close_path();
|
||||
}
|
||||
|
||||
Some(sample_bezpath)
|
||||
}
|
||||
|
||||
pub fn t_value_to_parametric(bezpath: &BezPath, t: f64, euclidian: bool, segments_length: Option<&[f64]>) -> (usize, f64) {
|
||||
if euclidian {
|
||||
let (segment_index, t) = bezpath_t_value_to_parametric(bezpath, BezPathTValue::GlobalEuclidean(t), segments_length);
|
||||
let segment = bezpath.get_seg(segment_index + 1).unwrap();
|
||||
return (segment_index, eval_pathseg_euclidean(segment, t, DEFAULT_ACCURACY));
|
||||
}
|
||||
bezpath_t_value_to_parametric(bezpath, BezPathTValue::GlobalParametric(t), segments_length)
|
||||
}
|
||||
|
||||
/// Finds the t value of point on the given path segment i.e fractional distance along the segment's total length.
|
||||
/// It uses a binary search to find the value `t` such that the ratio `length_up_to_t / total_length` approximates the input `distance`.
|
||||
pub fn eval_pathseg_euclidean(path_segment: PathSeg, distance: f64, accuracy: f64) -> f64 {
|
||||
let mut low_t = 0.;
|
||||
let mut mid_t = 0.5;
|
||||
let mut high_t = 1.;
|
||||
|
||||
let total_length = path_segment.perimeter(accuracy);
|
||||
|
||||
if !total_length.is_finite() || total_length <= f64::EPSILON {
|
||||
return 0.;
|
||||
}
|
||||
|
||||
let distance = distance.clamp(0., 1.);
|
||||
|
||||
while high_t - low_t > accuracy {
|
||||
let current_length = path_segment.subsegment(0.0..mid_t).perimeter(accuracy);
|
||||
let current_distance = current_length / total_length;
|
||||
|
||||
if current_distance > distance {
|
||||
high_t = mid_t;
|
||||
} else {
|
||||
low_t = mid_t;
|
||||
}
|
||||
mid_t = (high_t + low_t) / 2.;
|
||||
}
|
||||
|
||||
mid_t
|
||||
}
|
||||
|
||||
/// Converts from a bezpath (composed of multiple segments) to a point along a certain segment represented.
|
||||
/// The returned tuple represents the segment index and the `t` value along that segment.
|
||||
/// Both the input global `t` value and the output `t` value are in euclidean space, meaning there is a constant rate of change along the arc length.
|
||||
fn global_euclidean_to_local_euclidean(bezpath: &BezPath, global_t: f64, lengths: &[f64], total_length: f64) -> (usize, f64) {
|
||||
let mut accumulator = 0.;
|
||||
for (index, length) in lengths.iter().enumerate() {
|
||||
let length_ratio = length / total_length;
|
||||
if (index == 0 || accumulator <= global_t) && global_t <= accumulator + length_ratio {
|
||||
return (index, ((global_t - accumulator) / length_ratio).clamp(0., 1.));
|
||||
}
|
||||
accumulator += length_ratio;
|
||||
}
|
||||
(bezpath.segments().count() - 1, 1.)
|
||||
}
|
||||
|
||||
enum BezPathTValue {
|
||||
GlobalEuclidean(f64),
|
||||
GlobalParametric(f64),
|
||||
}
|
||||
|
||||
/// Convert a [BezPathTValue] to a parametric `(segment_index, t)` tuple.
|
||||
/// - Asserts that `t` values contained within the `SubpathTValue` argument lie in the range [0, 1].
|
||||
fn bezpath_t_value_to_parametric(bezpath: &BezPath, t: BezPathTValue, precomputed_segments_length: Option<&[f64]>) -> (usize, f64) {
|
||||
let segment_count = bezpath.segments().count();
|
||||
assert!(segment_count >= 1);
|
||||
|
||||
match t {
|
||||
BezPathTValue::GlobalEuclidean(t) => {
|
||||
let computed_segments_length;
|
||||
|
||||
let segments_length = if let Some(segments_length) = precomputed_segments_length {
|
||||
segments_length
|
||||
} else {
|
||||
computed_segments_length = bezpath.segments().map(|segment| segment.perimeter(DEFAULT_ACCURACY)).collect::<Vec<f64>>();
|
||||
computed_segments_length.as_slice()
|
||||
};
|
||||
|
||||
let total_length = segments_length.iter().sum();
|
||||
|
||||
global_euclidean_to_local_euclidean(bezpath, t, segments_length, total_length)
|
||||
}
|
||||
BezPathTValue::GlobalParametric(global_t) => {
|
||||
assert!((0.0..=1.).contains(&global_t));
|
||||
|
||||
if global_t == 1. {
|
||||
return (segment_count - 1, 1.);
|
||||
}
|
||||
|
||||
let scaled_t = global_t * segment_count as f64;
|
||||
let segment_index = scaled_t.floor() as usize;
|
||||
let t = scaled_t - segment_index as f64;
|
||||
|
||||
(segment_index, t)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Randomly places points across the filled surface of this subpath (which is assumed to be closed).
|
||||
/// The `separation_disk_diameter` determines the minimum distance between all points from one another.
|
||||
/// Conceptually, this works by "throwing a dart" at the subpath's bounding box and keeping the dart only if:
|
||||
/// - It's inside the shape
|
||||
/// - It's not closer than `separation_disk_diameter` to any other point from a previous accepted dart throw
|
||||
///
|
||||
/// This repeats until accepted darts fill all possible areas between one another.
|
||||
///
|
||||
/// While the conceptual process described above asymptotically slows down and is never guaranteed to produce a maximal set in finite time,
|
||||
/// this is implemented with an algorithm that produces a maximal set in O(n) time. The slowest part is actually checking if points are inside the subpath shape.
|
||||
pub fn poisson_disk_points(bezpath_index: usize, bezpaths: &[(BezPath, Rect)], separation_disk_diameter: f64, rng: impl FnMut() -> f64) -> Vec<DVec2> {
|
||||
let (this_bezpath, this_bbox) = bezpaths[bezpath_index].clone();
|
||||
|
||||
if this_bezpath.elements().is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let point_in_shape_checker = |point: DVec2| {
|
||||
// Check against all paths the point is contained in to compute the correct winding number
|
||||
let mut number = 0;
|
||||
|
||||
for (i, (shape, bbox)) in bezpaths.iter().enumerate() {
|
||||
if bbox.x0 > point.x || bbox.y0 > point.y || bbox.x1 < point.x || bbox.y1 < point.y {
|
||||
continue;
|
||||
}
|
||||
|
||||
let winding = shape.winding(dvec2_to_point(point));
|
||||
if winding == 0 && i == bezpath_index {
|
||||
return false;
|
||||
}
|
||||
number += winding;
|
||||
}
|
||||
|
||||
// Non-zero fill rule
|
||||
number != 0
|
||||
};
|
||||
|
||||
let line_intersect_shape_checker = |p0: (f64, f64), p1: (f64, f64)| {
|
||||
for segment in this_bezpath.segments() {
|
||||
if !segment.intersect_line(Line::new(p0, p1)).is_empty() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
};
|
||||
|
||||
let offset = DVec2::new(this_bbox.x0, this_bbox.y0);
|
||||
let width = this_bbox.width();
|
||||
let height = this_bbox.height();
|
||||
|
||||
poisson_disk_sample(offset, width, height, separation_disk_diameter, point_in_shape_checker, line_intersect_shape_checker, rng)
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
use glam::{DAffine2, DVec2};
|
||||
use graphene_vector::{PointDomain, PointId, SegmentDomain, VectorData, VectorDataIndex};
|
||||
use petgraph::prelude::UnGraphMap;
|
||||
use rustc_hash::FxHashSet;
|
||||
|
||||
pub trait MergeByDistanceExt {
|
||||
/// Collapse all points with edges shorter than the specified distance
|
||||
fn merge_by_distance_topological(&mut self, distance: f64);
|
||||
fn merge_by_distance_spatial(&mut self, transform: DAffine2, distance: f64);
|
||||
}
|
||||
|
||||
impl MergeByDistanceExt for VectorData {
|
||||
fn merge_by_distance_topological(&mut self, distance: f64) {
|
||||
// Treat self as an undirected graph
|
||||
let indices = VectorDataIndex::build_from(self);
|
||||
|
||||
// TODO: We lose information on the winding order by using an undirected graph. Switch to a directed graph and fix the algorithm to handle that.
|
||||
// Graph containing only short edges, referencing the data graph
|
||||
let mut short_edges = UnGraphMap::new();
|
||||
|
||||
for segment_id in self.segment_ids().iter().copied() {
|
||||
let length = indices.segment_chord_length(segment_id);
|
||||
if length < distance {
|
||||
let [start, end] = indices.segment_ends(segment_id);
|
||||
let start = indices.point_graph.node_weight(start).unwrap().id;
|
||||
let end = indices.point_graph.node_weight(end).unwrap().id;
|
||||
|
||||
short_edges.add_node(start);
|
||||
short_edges.add_node(end);
|
||||
short_edges.add_edge(start, end, segment_id);
|
||||
}
|
||||
}
|
||||
|
||||
// Group connected segments to collapse them into a single point
|
||||
// TODO: there are a few possible algorithms for this - perhaps test empirically to find fastest
|
||||
let collapse: Vec<FxHashSet<PointId>> = petgraph::algo::tarjan_scc(&short_edges).into_iter().map(|connected| connected.into_iter().collect()).collect();
|
||||
let average_position = collapse
|
||||
.iter()
|
||||
.map(|collapse_set| {
|
||||
let sum: DVec2 = collapse_set.iter().map(|&id| indices.point_position(id, self)).sum();
|
||||
sum / collapse_set.len() as f64
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Collect points and segments to delete at the end to avoid invalidating indices
|
||||
let mut points_to_delete = FxHashSet::default();
|
||||
let mut segments_to_delete = FxHashSet::default();
|
||||
for (mut collapse_set, average_pos) in collapse.into_iter().zip(average_position.into_iter()) {
|
||||
// Remove any segments where both endpoints are in the collapse set
|
||||
segments_to_delete.extend(self.segment_domain.iter().filter_map(|(id, start_offset, end_offset, _)| {
|
||||
let start = self.point_domain.ids()[start_offset];
|
||||
let end = self.point_domain.ids()[end_offset];
|
||||
if collapse_set.contains(&start) && collapse_set.contains(&end) { Some(id) } else { None }
|
||||
}));
|
||||
|
||||
// Delete all points but the first, set its position to the average, and update segments
|
||||
let first_id = collapse_set.iter().copied().next().unwrap();
|
||||
collapse_set.remove(&first_id);
|
||||
let first_offset = indices.point_to_offset[&first_id];
|
||||
|
||||
// Look for segments with endpoints in `collapse_set` and replace them with the point we are collapsing to
|
||||
for (_, start_offset, end_offset, handles) in self.segment_domain.iter_mut() {
|
||||
let start_id = self.point_domain.ids()[*start_offset];
|
||||
let end_id = self.point_domain.ids()[*end_offset];
|
||||
|
||||
// Update Bezier handles for moved points
|
||||
if start_id == first_id {
|
||||
let point_position = self.point_domain.position[*start_offset];
|
||||
handles.move_start(average_pos - point_position);
|
||||
}
|
||||
if end_id == first_id {
|
||||
let point_position = self.point_domain.position[*end_offset];
|
||||
handles.move_end(average_pos - point_position);
|
||||
}
|
||||
|
||||
// Replace removed points with the collapsed point
|
||||
if collapse_set.contains(&start_id) {
|
||||
let point_position = self.point_domain.position[*start_offset];
|
||||
*start_offset = first_offset;
|
||||
handles.move_start(average_pos - point_position);
|
||||
}
|
||||
if collapse_set.contains(&end_id) {
|
||||
let point_position = self.point_domain.position[*end_offset];
|
||||
*end_offset = first_offset;
|
||||
handles.move_end(average_pos - point_position);
|
||||
}
|
||||
}
|
||||
|
||||
// Update the position of the collapsed point
|
||||
self.point_domain.position[first_offset] = average_pos;
|
||||
|
||||
points_to_delete.extend(collapse_set)
|
||||
}
|
||||
|
||||
// Remove faces whose start or end segments are removed
|
||||
// TODO: Adjust faces and only delete if all (or all but one) segments are removed
|
||||
self.region_domain
|
||||
.retain_with_region(|_, segment_range| segments_to_delete.contains(segment_range.start()) || segments_to_delete.contains(segment_range.end()));
|
||||
self.segment_domain.retain(|id| !segments_to_delete.contains(id), usize::MAX);
|
||||
self.point_domain.retain(&mut self.segment_domain, |id| !points_to_delete.contains(id));
|
||||
}
|
||||
|
||||
fn merge_by_distance_spatial(&mut self, transform: DAffine2, distance: f64) {
|
||||
let point_count = self.point_domain.positions().len();
|
||||
|
||||
// Find min x and y for grid cell normalization
|
||||
let mut min_x = f64::MAX;
|
||||
let mut min_y = f64::MAX;
|
||||
|
||||
// Calculate mins without collecting all positions
|
||||
for &pos in self.point_domain.positions() {
|
||||
let transformed_pos = transform.transform_point2(pos);
|
||||
min_x = min_x.min(transformed_pos.x);
|
||||
min_y = min_y.min(transformed_pos.y);
|
||||
}
|
||||
|
||||
// Create a spatial grid with cell size of 'distance'
|
||||
use std::collections::HashMap;
|
||||
let mut grid: HashMap<(i32, i32), Vec<usize>> = HashMap::new();
|
||||
|
||||
// Add points to grid cells without collecting all positions first
|
||||
for i in 0..point_count {
|
||||
let pos = transform.transform_point2(self.point_domain.positions()[i]);
|
||||
let grid_x = ((pos.x - min_x) / distance).floor() as i32;
|
||||
let grid_y = ((pos.y - min_y) / distance).floor() as i32;
|
||||
|
||||
grid.entry((grid_x, grid_y)).or_default().push(i);
|
||||
}
|
||||
|
||||
// Create point index mapping for merged points
|
||||
let mut point_index_map = vec![None; point_count];
|
||||
let mut merged_positions = Vec::new();
|
||||
let mut merged_indices = Vec::new();
|
||||
|
||||
// Process each point
|
||||
for i in 0..point_count {
|
||||
// Skip points that have already been processed
|
||||
if point_index_map[i].is_some() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let pos_i = transform.transform_point2(self.point_domain.positions()[i]);
|
||||
let grid_x = ((pos_i.x - min_x) / distance).floor() as i32;
|
||||
let grid_y = ((pos_i.y - min_y) / distance).floor() as i32;
|
||||
|
||||
let mut group = vec![i];
|
||||
|
||||
// Check only neighboring cells (3x3 grid around current cell)
|
||||
for dx in -1..=1 {
|
||||
for dy in -1..=1 {
|
||||
let neighbor_cell = (grid_x + dx, grid_y + dy);
|
||||
|
||||
if let Some(indices) = grid.get(&neighbor_cell) {
|
||||
for &j in indices {
|
||||
if j > i && point_index_map[j].is_none() {
|
||||
let pos_j = transform.transform_point2(self.point_domain.positions()[j]);
|
||||
if pos_i.distance(pos_j) <= distance {
|
||||
group.push(j);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create merged point - calculate positions as needed
|
||||
let merged_position = group
|
||||
.iter()
|
||||
.map(|&idx| transform.transform_point2(self.point_domain.positions()[idx]))
|
||||
.fold(DVec2::ZERO, |sum, pos| sum + pos)
|
||||
/ group.len() as f64;
|
||||
|
||||
let merged_position = transform.inverse().transform_point2(merged_position);
|
||||
let merged_index = merged_positions.len();
|
||||
|
||||
merged_positions.push(merged_position);
|
||||
merged_indices.push(self.point_domain.ids()[group[0]]);
|
||||
|
||||
// Update mapping for all points in the group
|
||||
for &idx in &group {
|
||||
point_index_map[idx] = Some(merged_index);
|
||||
}
|
||||
}
|
||||
|
||||
// Create new point domain with merged points
|
||||
let mut new_point_domain = PointDomain::new();
|
||||
for (idx, pos) in merged_indices.into_iter().zip(merged_positions) {
|
||||
new_point_domain.push(idx, pos);
|
||||
}
|
||||
|
||||
// Update segment domain
|
||||
let mut new_segment_domain = SegmentDomain::new();
|
||||
for segment_idx in 0..self.segment_domain.ids().len() {
|
||||
let id = self.segment_domain.ids()[segment_idx];
|
||||
let start = self.segment_domain.start_point()[segment_idx];
|
||||
let end = self.segment_domain.end_point()[segment_idx];
|
||||
let handles = self.segment_domain.handles()[segment_idx];
|
||||
let stroke = self.segment_domain.stroke()[segment_idx];
|
||||
|
||||
// Get new indices for start and end points
|
||||
let new_start = point_index_map[start].unwrap();
|
||||
let new_end = point_index_map[end].unwrap();
|
||||
|
||||
// Skip segments where start and end points were merged
|
||||
if new_start != new_end {
|
||||
new_segment_domain.push(id, new_start, new_end, handles, stroke);
|
||||
}
|
||||
}
|
||||
|
||||
// Create new vector data
|
||||
self.point_domain = new_point_domain;
|
||||
self.segment_domain = new_segment_domain;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
pub mod bezpath_algorithms;
|
||||
pub mod merge_by_distance;
|
||||
pub mod offset_subpath;
|
||||
pub mod poisson_disk;
|
||||
pub mod spline;
|
||||
@@ -0,0 +1,173 @@
|
||||
use bezier_rs::{Bezier, BezierHandles, Join, Subpath, TValue};
|
||||
use graphene_vector::PointId;
|
||||
|
||||
/// Value to control smoothness and mathematical accuracy to offset a cubic Bezier.
|
||||
const CUBIC_REGULARIZATION_ACCURACY: f64 = 0.5;
|
||||
/// Accuracy of fitting offset curve to Bezier paths.
|
||||
const CUBIC_TO_BEZPATH_ACCURACY: f64 = 1e-3;
|
||||
/// Constant used to determine if `f64`s are equivalent.
|
||||
pub const MAX_ABSOLUTE_DIFFERENCE: f64 = 1e-3;
|
||||
|
||||
fn segment_to_bezier(seg: kurbo::PathSeg) -> Bezier {
|
||||
match seg {
|
||||
kurbo::PathSeg::Line(line) => Bezier::from_linear_coordinates(line.p0.x, line.p0.y, line.p1.x, line.p1.y),
|
||||
kurbo::PathSeg::Quad(quad_bez) => Bezier::from_quadratic_coordinates(quad_bez.p0.x, quad_bez.p0.y, quad_bez.p1.x, quad_bez.p1.y, quad_bez.p1.x, quad_bez.p1.y),
|
||||
kurbo::PathSeg::Cubic(cubic_bez) => Bezier::from_cubic_coordinates(
|
||||
cubic_bez.p0.x,
|
||||
cubic_bez.p0.y,
|
||||
cubic_bez.p1.x,
|
||||
cubic_bez.p1.y,
|
||||
cubic_bez.p2.x,
|
||||
cubic_bez.p2.y,
|
||||
cubic_bez.p3.x,
|
||||
cubic_bez.p3.y,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Replace the implementation to use only Kurbo API.
|
||||
/// Reduces the segments of the subpath into simple subcurves, then offset each subcurve a set `distance` away.
|
||||
/// The intersections of segments of the subpath are joined using the method specified by the `join` argument.
|
||||
pub fn offset_subpath(subpath: &Subpath<PointId>, distance: f64, join: Join) -> Subpath<PointId> {
|
||||
// An offset at a distance 0 from the curve is simply the same curve.
|
||||
// An offset of a single point is not defined.
|
||||
if distance == 0. || subpath.len() <= 1 || subpath.len_segments() < 1 {
|
||||
return subpath.clone();
|
||||
}
|
||||
|
||||
let mut subpaths = subpath
|
||||
.iter()
|
||||
.filter(|bezier| !bezier.is_point())
|
||||
.map(|bezier| bezier.to_cubic())
|
||||
.map(|cubic| {
|
||||
let Bezier { start, end, handles } = cubic;
|
||||
let BezierHandles::Cubic { handle_start, handle_end } = handles else { unreachable!()};
|
||||
|
||||
let cubic_bez = kurbo::CubicBez::new((start.x, start.y), (handle_start.x, handle_start.y), (handle_end.x, handle_end.y), (end.x, end.y));
|
||||
let cubic_offset = kurbo::offset::CubicOffset::new_regularized(cubic_bez, distance, CUBIC_REGULARIZATION_ACCURACY);
|
||||
let offset_bezpath = kurbo::fit_to_bezpath(&cubic_offset, CUBIC_TO_BEZPATH_ACCURACY);
|
||||
|
||||
let beziers = offset_bezpath.segments().fold(Vec::new(), |mut acc, seg| {
|
||||
acc.push(segment_to_bezier(seg));
|
||||
acc
|
||||
});
|
||||
|
||||
Subpath::from_beziers(&beziers, false)
|
||||
})
|
||||
.filter(|subpath| subpath.len() >= 2) // In some cases the reduced and scaled bézier is marked by is_point (so the subpath is empty).
|
||||
.collect::<Vec<Subpath<PointId>>>();
|
||||
|
||||
let mut drop_common_point = vec![true; subpath.len()];
|
||||
|
||||
// Clip or join consecutive Subpaths
|
||||
for i in 0..subpaths.len() - 1 {
|
||||
let j = i + 1;
|
||||
let subpath1 = &subpaths[i];
|
||||
let subpath2 = &subpaths[j];
|
||||
|
||||
let last_segment = subpath1.get_segment(subpath1.len_segments() - 1).unwrap();
|
||||
let first_segment = subpath2.get_segment(0).unwrap();
|
||||
|
||||
// If the anchors are approximately equal, there is no need to clip / join the segments
|
||||
if last_segment.end().abs_diff_eq(first_segment.start(), MAX_ABSOLUTE_DIFFERENCE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Calculate the angle formed between two consecutive Subpaths
|
||||
let out_tangent = subpath.get_segment(i).unwrap().tangent(TValue::Parametric(1.));
|
||||
let in_tangent = subpath.get_segment(j).unwrap().tangent(TValue::Parametric(0.));
|
||||
let angle = out_tangent.angle_to(in_tangent);
|
||||
|
||||
// The angle is concave. The Subpath overlap and must be clipped
|
||||
let mut apply_join = true;
|
||||
if (angle > 0. && distance > 0.) || (angle < 0. && distance < 0.) {
|
||||
// If the distance is large enough, there may still be no intersections. Also, if the angle is close enough to zero,
|
||||
// subpath intersections may find no intersections. In this case, the points are likely close enough that we can approximate
|
||||
// the points as being on top of one another.
|
||||
if let Some((clipped_subpath1, clipped_subpath2)) = Subpath::clip_simple_subpaths(subpath1, subpath2) {
|
||||
subpaths[i] = clipped_subpath1;
|
||||
subpaths[j] = clipped_subpath2;
|
||||
apply_join = false;
|
||||
}
|
||||
}
|
||||
// The angle is convex. The Subpath must be joined using the specified join type
|
||||
if apply_join {
|
||||
drop_common_point[j] = false;
|
||||
match join {
|
||||
Join::Bevel => {}
|
||||
Join::Miter(miter_limit) => {
|
||||
let miter_manipulator_group = subpaths[i].miter_line_join(&subpaths[j], miter_limit);
|
||||
if let Some(miter_manipulator_group) = miter_manipulator_group {
|
||||
subpaths[i].manipulator_groups_mut().push(miter_manipulator_group);
|
||||
}
|
||||
}
|
||||
Join::Round => {
|
||||
let (out_handle, round_point, in_handle) = subpaths[i].round_line_join(&subpaths[j], subpath.manipulator_groups()[j].anchor);
|
||||
let last_index = subpaths[i].manipulator_groups().len() - 1;
|
||||
subpaths[i].manipulator_groups_mut()[last_index].out_handle = Some(out_handle);
|
||||
subpaths[i].manipulator_groups_mut().push(round_point);
|
||||
subpaths[j].manipulator_groups_mut()[0].in_handle = Some(in_handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clip any overlap in the last segment
|
||||
if subpath.closed {
|
||||
let out_tangent = subpath.get_segment(subpath.len_segments() - 1).unwrap().tangent(TValue::Parametric(1.));
|
||||
let in_tangent = subpath.get_segment(0).unwrap().tangent(TValue::Parametric(0.));
|
||||
let angle = out_tangent.angle_to(in_tangent);
|
||||
|
||||
let mut apply_join = true;
|
||||
if (angle > 0. && distance > 0.) || (angle < 0. && distance < 0.) {
|
||||
if let Some((clipped_subpath1, clipped_subpath2)) = Subpath::clip_simple_subpaths(&subpaths[subpaths.len() - 1], &subpaths[0]) {
|
||||
// Merge the clipped subpaths
|
||||
let last_index = subpaths.len() - 1;
|
||||
subpaths[last_index] = clipped_subpath1;
|
||||
subpaths[0] = clipped_subpath2;
|
||||
apply_join = false;
|
||||
}
|
||||
}
|
||||
if apply_join {
|
||||
drop_common_point[0] = false;
|
||||
match join {
|
||||
Join::Bevel => {}
|
||||
Join::Miter(miter_limit) => {
|
||||
let last_subpath_index = subpaths.len() - 1;
|
||||
let miter_manipulator_group = subpaths[last_subpath_index].miter_line_join(&subpaths[0], miter_limit);
|
||||
if let Some(miter_manipulator_group) = miter_manipulator_group {
|
||||
subpaths[last_subpath_index].manipulator_groups_mut().push(miter_manipulator_group);
|
||||
}
|
||||
}
|
||||
Join::Round => {
|
||||
let last_subpath_index = subpaths.len() - 1;
|
||||
let (out_handle, round_point, in_handle) = subpaths[last_subpath_index].round_line_join(&subpaths[0], subpath.manipulator_groups()[0].anchor);
|
||||
let last_index = subpaths[last_subpath_index].manipulator_groups().len() - 1;
|
||||
subpaths[last_subpath_index].manipulator_groups_mut()[last_index].out_handle = Some(out_handle);
|
||||
subpaths[last_subpath_index].manipulator_groups_mut().push(round_point);
|
||||
subpaths[0].manipulator_groups_mut()[0].in_handle = Some(in_handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Merge the subpaths. Drop points which overlap with one another.
|
||||
let mut manipulator_groups = subpaths[0].manipulator_groups().to_vec();
|
||||
for i in 1..subpaths.len() {
|
||||
if drop_common_point[i] {
|
||||
let last_group = manipulator_groups.pop().unwrap();
|
||||
let mut manipulators_copy = subpaths[i].manipulator_groups().to_vec();
|
||||
manipulators_copy[0].in_handle = last_group.in_handle;
|
||||
|
||||
manipulator_groups.append(&mut manipulators_copy);
|
||||
} else {
|
||||
manipulator_groups.append(&mut subpaths[i].manipulator_groups().to_vec());
|
||||
}
|
||||
}
|
||||
if subpath.closed && drop_common_point[0] {
|
||||
let last_group = manipulator_groups.pop().unwrap();
|
||||
manipulator_groups[0].in_handle = last_group.in_handle;
|
||||
}
|
||||
|
||||
Subpath::new(manipulator_groups, subpath.closed)
|
||||
}
|
||||
@@ -0,0 +1,422 @@
|
||||
use glam::DVec2;
|
||||
use std::collections::HashMap;
|
||||
use std::f64;
|
||||
|
||||
const DEEPEST_SUBDIVISION_LEVEL_BEFORE_DISCARDING: usize = 8;
|
||||
|
||||
/// Fast (O(n) with respect to time and memory) algorithm for generating a maximal set of points using Poisson-disk sampling.
|
||||
/// Based on the paper:
|
||||
/// "Poisson Disk Point Sets by Hierarchical Dart Throwing"
|
||||
/// <https://scholarsarchive.byu.edu/facpub/237/>
|
||||
pub fn poisson_disk_sample(
|
||||
offset: DVec2,
|
||||
width: f64,
|
||||
height: f64,
|
||||
diameter: f64,
|
||||
point_in_shape_checker: impl Fn(DVec2) -> bool,
|
||||
line_intersect_shape_checker: impl Fn((f64, f64), (f64, f64)) -> bool,
|
||||
rng: impl FnMut() -> f64,
|
||||
) -> Vec<DVec2> {
|
||||
let mut rng = rng;
|
||||
let diameter_squared = diameter.powi(2);
|
||||
|
||||
// Initialize a place to store the generated points within a spatial acceleration structure
|
||||
let mut points_grid = AccelerationGrid::new(width, height, diameter);
|
||||
|
||||
// Pick a grid size for the base-level domain that's as large as possible, while also:
|
||||
// - Dividing into an integer number of cells across the dartboard domain, to avoid wastefully throwing darts beyond the width and height of the dartboard domain
|
||||
// - Being fully covered by the radius around a dart thrown anywhere in its area, where the worst-case is a corner which has a distance of sqrt(2) to the opposite corner
|
||||
let greater_dimension = width.max(height);
|
||||
let base_level_grid_size = greater_dimension / (greater_dimension * f64::consts::SQRT_2 / (diameter / 2.)).ceil();
|
||||
|
||||
// Initialize the problem by including all base-level squares in the active list since they're all part of the yet-to-be-targetted dartboard domain
|
||||
let base_level = ActiveListLevel::new_filled(base_level_grid_size, offset, width, height, &point_in_shape_checker, &line_intersect_shape_checker);
|
||||
// In the future, if necessary, this could be turned into a fixed-length array with worst-case length `f64::MANTISSA_DIGITS`
|
||||
let mut active_list_levels = vec![base_level];
|
||||
|
||||
// Loop until all active squares have been processed, meaning all of the dartboard domain has been checked
|
||||
while active_list_levels.iter().any(|active_list| active_list.not_empty()) {
|
||||
// Randomly pick a square in the dartboard domain, with probability proportional to its area
|
||||
let (active_square_level, active_square_index_in_level) = target_active_square(&active_list_levels, &mut rng);
|
||||
|
||||
// The level contains the list of all active squares at this target square's subdivision depth
|
||||
let level = &mut active_list_levels[active_square_level];
|
||||
|
||||
// Take the targetted active square out of the list and get its size
|
||||
let active_square = level.take_square(active_square_index_in_level);
|
||||
let active_square_size = level.square_size();
|
||||
|
||||
// Skip this target square if it's within range of any current points, since more nearby points could have been added after this square was included in the active list
|
||||
if !square_not_covered_by_poisson_points(active_square.top_left_corner(), active_square_size / 2., diameter_squared, &points_grid) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Throw a dart by picking a random point within this target square
|
||||
let point = {
|
||||
let active_top_left_corner = active_square.top_left_corner();
|
||||
let x = active_top_left_corner.x + rng() * active_square_size;
|
||||
let y = active_top_left_corner.y + rng() * active_square_size;
|
||||
(x, y).into()
|
||||
};
|
||||
|
||||
// If the dart hit a valid spot, save that point (we're now permanently done with this target square's region)
|
||||
if point_not_covered_by_poisson_points(point, diameter_squared, &points_grid) {
|
||||
// Silently reject the point if it lies outside the shape
|
||||
if active_square.fully_in_shape() || point_in_shape_checker(point + offset) {
|
||||
points_grid.insert(point);
|
||||
}
|
||||
}
|
||||
// Otherwise, subdivide this target square and add valid sub-squares back to the active list for later targetting
|
||||
else {
|
||||
// Discard any targetable domain smaller than this limited number of subdivision levels since it's too small to matter
|
||||
let next_level_deeper_level = active_square_level + 1;
|
||||
if next_level_deeper_level > DEEPEST_SUBDIVISION_LEVEL_BEFORE_DISCARDING {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If necessary for the following step, add another layer of depth to store squares at the next subdivision level
|
||||
if active_list_levels.len() <= next_level_deeper_level {
|
||||
active_list_levels.push(ActiveListLevel::new(active_square_size / 2.))
|
||||
}
|
||||
|
||||
// Get the list of active squares at the level of depth beneath this target square's level
|
||||
let next_level_deeper = &mut active_list_levels[next_level_deeper_level];
|
||||
|
||||
// Subdivide this target square into four sub-squares; running out of numerical precision will make this terminate at very small scales
|
||||
let subdivided_size = active_square_size / 2.;
|
||||
let active_top_left_corner = active_square.top_left_corner();
|
||||
let subdivided = [
|
||||
active_top_left_corner + DVec2::new(0., 0.),
|
||||
active_top_left_corner + DVec2::new(subdivided_size, 0.),
|
||||
active_top_left_corner + DVec2::new(0., subdivided_size),
|
||||
active_top_left_corner + DVec2::new(subdivided_size, subdivided_size),
|
||||
];
|
||||
|
||||
// Add the sub-squares which aren't within the radius of a nearby point to the sub-level's active list
|
||||
let half_subdivided_size = subdivided_size / 2.;
|
||||
let new_sub_squares = subdivided.into_iter().filter_map(|sub_square| {
|
||||
// Any sub-squares within the radius of a nearby point are filtered out
|
||||
if !square_not_covered_by_poisson_points(sub_square, half_subdivided_size, diameter_squared, &points_grid) {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Fully inside the shape
|
||||
if active_square.fully_in_shape() {
|
||||
Some(ActiveSquare::new(sub_square, true))
|
||||
}
|
||||
// Intersecting the shape's border
|
||||
else {
|
||||
// The sub-square is fully inside the shape if its top-left corner is inside and its edges don't intersect the shape border
|
||||
let point_with_offset = sub_square + offset;
|
||||
let square_edges_intersect_shape = {
|
||||
let min = point_with_offset;
|
||||
let max = min + DVec2::splat(subdivided_size);
|
||||
|
||||
// Top edge line
|
||||
line_intersect_shape_checker((min.x, min.y), (max.x, min.y)) ||
|
||||
// Right edge line
|
||||
line_intersect_shape_checker((max.x, min.y), (max.x, max.y)) ||
|
||||
// Bottom edge line
|
||||
line_intersect_shape_checker((max.x, max.y), (min.x, max.y)) ||
|
||||
// Left edge line
|
||||
line_intersect_shape_checker((min.x, max.y), (min.x, min.y))
|
||||
};
|
||||
let sub_square_fully_inside_shape = !square_edges_intersect_shape && point_in_shape_checker(point_with_offset) && point_in_shape_checker(point_with_offset + subdivided_size);
|
||||
|
||||
Some(ActiveSquare::new(sub_square, sub_square_fully_inside_shape))
|
||||
}
|
||||
});
|
||||
next_level_deeper.add_squares(new_sub_squares);
|
||||
}
|
||||
}
|
||||
|
||||
points_grid.final_points(offset)
|
||||
}
|
||||
|
||||
/// Randomly pick a square in the dartboard domain, with probability proportional to its area.
|
||||
/// Returns a tuple with the subdivision level depth and the square index at that depth.
|
||||
fn target_active_square(active_list_levels: &[ActiveListLevel], rng: &mut impl FnMut() -> f64) -> (usize, usize) {
|
||||
let active_squares_total_area: f64 = active_list_levels.iter().map(|active_list| active_list.total_area()).sum();
|
||||
let mut index_into_area = rng() * active_squares_total_area;
|
||||
|
||||
for (level, active_list_level) in active_list_levels.iter().enumerate() {
|
||||
let subtracted = index_into_area - active_list_level.total_area();
|
||||
if subtracted > 0. {
|
||||
index_into_area = subtracted;
|
||||
continue;
|
||||
}
|
||||
|
||||
let active_square_index_in_level = (index_into_area / active_list_levels[level].square_area()).floor() as usize;
|
||||
return (level, active_square_index_in_level);
|
||||
}
|
||||
|
||||
panic!("index_into_area couldn't be be mapped to a square in any level of the active lists");
|
||||
}
|
||||
|
||||
fn point_not_covered_by_poisson_points(point: DVec2, diameter_squared: f64, points_grid: &AccelerationGrid) -> bool {
|
||||
points_grid.nearby_points(point).all(|nearby_point| {
|
||||
let x_separation = nearby_point.x - point.x;
|
||||
let y_separation = nearby_point.y - point.y;
|
||||
|
||||
x_separation.powi(2) + y_separation.powi(2) > diameter_squared
|
||||
})
|
||||
}
|
||||
|
||||
fn square_not_covered_by_poisson_points(point: DVec2, half_square_size: f64, diameter_squared: f64, points_grid: &AccelerationGrid) -> bool {
|
||||
let square_center_x = point.x + half_square_size;
|
||||
let square_center_y = point.y + half_square_size;
|
||||
|
||||
points_grid.nearby_points(point).all(|nearby_point| {
|
||||
let x_distance = (square_center_x - nearby_point.x).abs() + half_square_size;
|
||||
let y_distance = (square_center_y - nearby_point.y).abs() + half_square_size;
|
||||
|
||||
x_distance.powi(2) + y_distance.powi(2) > diameter_squared
|
||||
})
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn cartesian_product<A, B>(a: A, b: B) -> impl Iterator<Item = (A::Item, B::Item)>
|
||||
where
|
||||
A: Iterator + Clone,
|
||||
B: Iterator + Clone,
|
||||
A::Item: Clone,
|
||||
B::Item: Clone,
|
||||
{
|
||||
a.flat_map(move |i| (b.clone().map(move |j| (i.clone(), j))))
|
||||
}
|
||||
|
||||
/// A square (represented by its top left corner position and width/height of `square_size`) that is currently a candidate for targetting by the dart throwing process.
|
||||
/// The positive sign bit encodes if the square is contained entirely within the masking shape, or negative if it's outside or intersects the shape path.
|
||||
pub struct ActiveSquare(DVec2);
|
||||
|
||||
impl ActiveSquare {
|
||||
pub fn new(top_left_corner: DVec2, fully_in_shape: bool) -> Self {
|
||||
Self(if fully_in_shape { top_left_corner } else { -top_left_corner })
|
||||
}
|
||||
|
||||
pub fn top_left_corner(&self) -> DVec2 {
|
||||
self.0.abs()
|
||||
}
|
||||
|
||||
pub fn fully_in_shape(&self) -> bool {
|
||||
self.0.x.is_sign_positive()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ActiveListLevel {
|
||||
/// List of all subdivided squares of the same size that are currently candidates for targetting by the dart throwing process
|
||||
active_squares: Vec<ActiveSquare>,
|
||||
/// Width and height of the squares in this level of subdivision
|
||||
square_size: f64,
|
||||
/// Current sum of the area in all active squares in this subdivision level
|
||||
total_area: f64,
|
||||
}
|
||||
|
||||
impl ActiveListLevel {
|
||||
#[inline(always)]
|
||||
pub fn new(square_size: f64) -> Self {
|
||||
Self {
|
||||
active_squares: Vec::new(),
|
||||
square_size,
|
||||
total_area: 0.,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_filled(
|
||||
square_size: f64,
|
||||
offset: DVec2,
|
||||
width: f64,
|
||||
height: f64,
|
||||
point_in_shape_checker: impl Fn(DVec2) -> bool,
|
||||
line_intersect_shape_checker: impl Fn((f64, f64), (f64, f64)) -> bool,
|
||||
) -> Self {
|
||||
// These should divide evenly but rounding is to protect against small numerical imprecision errors
|
||||
let x_squares = (width / square_size).round() as usize;
|
||||
let y_squares = (height / square_size).round() as usize;
|
||||
|
||||
// Hashes based on the grid cell coordinates and direction of the line: (x, y, is_vertical)
|
||||
let mut line_intersection_cache: HashMap<(usize, usize, bool), bool> = HashMap::new();
|
||||
|
||||
// Populate each square with its top-left corner coordinate
|
||||
let active_squares: Vec<_> = cartesian_product(0..x_squares, 0..y_squares)
|
||||
.filter_map(|(x, y)| {
|
||||
let corner = DVec2::new(x as f64 * square_size, y as f64 * square_size);
|
||||
let corner_with_offset = corner + offset;
|
||||
|
||||
// Lazily check (and cache) if the square's edges intersect the shape, which is an expensive operation
|
||||
let mut square_edges_intersect_shape_value = None;
|
||||
let mut square_edges_intersect_shape = || {
|
||||
square_edges_intersect_shape_value.unwrap_or_else(|| {
|
||||
let square_edges_intersect_shape = {
|
||||
let min = corner_with_offset;
|
||||
let max = min + DVec2::splat(square_size);
|
||||
|
||||
// Top edge line
|
||||
*line_intersection_cache.entry((x, y, false)).or_insert_with(|| line_intersect_shape_checker((min.x, min.y), (max.x, min.y))) ||
|
||||
// Right edge line
|
||||
*line_intersection_cache.entry((x + 1, y, true)).or_insert_with(|| line_intersect_shape_checker((max.x, min.y), (max.x, max.y))) ||
|
||||
// Bottom edge line
|
||||
*line_intersection_cache.entry((x, y + 1, false)).or_insert_with(|| line_intersect_shape_checker((max.x, max.y), (min.x, max.y))) ||
|
||||
// Left edge line
|
||||
*line_intersection_cache.entry((x, y, true)).or_insert_with(|| line_intersect_shape_checker((min.x, max.y), (min.x, min.y)))
|
||||
};
|
||||
square_edges_intersect_shape_value = Some(square_edges_intersect_shape);
|
||||
square_edges_intersect_shape
|
||||
})
|
||||
};
|
||||
|
||||
// Check if this cell's top-left corner is inside the shape
|
||||
let point_in_shape = point_in_shape_checker(corner_with_offset);
|
||||
|
||||
// Determine if the square is inside the shape
|
||||
let square_not_outside_shape = point_in_shape || square_edges_intersect_shape();
|
||||
if square_not_outside_shape {
|
||||
// Check if this cell's bottom-right corner is inside the shape
|
||||
let opposite_corner_with_offset = DVec2::new((x + 1) as f64 * square_size, (y + 1) as f64 * square_size) + offset;
|
||||
let opposite_corner_in_shape = point_in_shape_checker(opposite_corner_with_offset);
|
||||
|
||||
let square_in_shape = opposite_corner_in_shape && !square_edges_intersect_shape();
|
||||
Some(ActiveSquare::new(corner, square_in_shape))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Sum every square's area to get the total
|
||||
let total_area = square_size.powi(2) * active_squares.len() as f64;
|
||||
|
||||
Self {
|
||||
active_squares,
|
||||
square_size,
|
||||
total_area,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
#[inline(always)]
|
||||
pub fn take_square(&mut self, active_square_index: usize) -> ActiveSquare {
|
||||
let targetted_square = self.active_squares.swap_remove(active_square_index);
|
||||
self.total_area = self.square_size.powi(2) * self.active_squares.len() as f64;
|
||||
targetted_square
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn add_squares(&mut self, new_squares: impl Iterator<Item = ActiveSquare>) {
|
||||
for new_square in new_squares {
|
||||
self.active_squares.push(new_square);
|
||||
}
|
||||
self.total_area = self.square_size.powi(2) * self.active_squares.len() as f64;
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn square_size(&self) -> f64 {
|
||||
self.square_size
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn square_area(&self) -> f64 {
|
||||
self.square_size.powi(2)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn total_area(&self) -> f64 {
|
||||
self.total_area
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn not_empty(&self) -> bool {
|
||||
!self.active_squares.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct PointsList {
|
||||
// The worst-case number of points in a 3x3 grid is 16 (one at each intersection of the four gridlines per axis)
|
||||
storage_slots: [DVec2; 16],
|
||||
length: usize,
|
||||
}
|
||||
|
||||
impl PointsList {
|
||||
#[inline(always)]
|
||||
pub fn push(&mut self, point: DVec2) {
|
||||
self.storage_slots[self.length] = point;
|
||||
self.length += 1;
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn list_cell_and_neighbors(&self) -> impl Iterator<Item = DVec2> {
|
||||
// The negative bit is used to store whether a point belongs to a neighboring cell
|
||||
self.storage_slots.into_iter().take(self.length).map(|point| (point.x.abs(), point.y.abs()).into())
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn list_cell(&self) -> impl Iterator<Item = DVec2> {
|
||||
// The negative bit is used to store whether a point belongs to a neighboring cell
|
||||
self.storage_slots
|
||||
.into_iter()
|
||||
.take(self.length)
|
||||
.filter(|point| point.x.is_sign_positive() && point.y.is_sign_positive())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AccelerationGrid {
|
||||
size: f64,
|
||||
dimension_x: usize,
|
||||
dimension_y: usize,
|
||||
cells: Vec<PointsList>,
|
||||
}
|
||||
|
||||
impl AccelerationGrid {
|
||||
#[inline(always)]
|
||||
pub fn new(width: f64, height: f64, size: f64) -> Self {
|
||||
let dimension_x = (width / size).ceil() as usize + 1;
|
||||
let dimension_y = (height / size).ceil() as usize + 1;
|
||||
|
||||
Self {
|
||||
size,
|
||||
dimension_x,
|
||||
dimension_y,
|
||||
cells: vec![PointsList::default(); dimension_x * dimension_y],
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn insert(&mut self, point: DVec2) {
|
||||
let x = (point.x / self.size).floor() as usize;
|
||||
let y = (point.y / self.size).floor() as usize;
|
||||
|
||||
// Insert this point at this cell and the surrounding cells in a 3x3 patch
|
||||
for (x_offset, y_offset) in cartesian_product((-1)..=1, (-1)..=1) {
|
||||
// Avoid going negative
|
||||
let (x, y) = (x as isize + x_offset, y as isize + y_offset);
|
||||
if x < 0 || y < 0 {
|
||||
continue;
|
||||
}
|
||||
// Avoid going beyond the width or height
|
||||
let (x, y) = (x as usize, y as usize);
|
||||
if x > self.dimension_x - 1 || y > self.dimension_y - 1 {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get the cell corresponding to the (x, y) index
|
||||
let cell = &mut self.cells[y * self.dimension_x + x];
|
||||
|
||||
// Store the given point in this grid cell, and use the negative bit to indicate if this belongs to a neighboring cell
|
||||
cell.push(if x_offset == 0 && y_offset == 0 { point } else { -point });
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn nearby_points(&self, point: DVec2) -> impl Iterator<Item = DVec2> {
|
||||
let x = (point.x / self.size).floor() as usize;
|
||||
let y = (point.y / self.size).floor() as usize;
|
||||
|
||||
self.cells[y * self.dimension_x + x].list_cell_and_neighbors()
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn final_points(&self, offset: DVec2) -> Vec<DVec2> {
|
||||
self.cells.iter().flat_map(|cell| cell.list_cell()).map(|point| point + offset).collect()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
use glam::DVec2;
|
||||
|
||||
/// Solve for the first handle of an open spline. (The opposite handle can be found by mirroring the result about the anchor.)
|
||||
pub fn solve_spline_first_handle_open(points: &[DVec2]) -> Vec<DVec2> {
|
||||
let len_points = points.len();
|
||||
if len_points == 0 {
|
||||
return Vec::new();
|
||||
}
|
||||
if len_points == 1 {
|
||||
return vec![points[0]];
|
||||
}
|
||||
|
||||
// Matrix coefficients a, b and c (see https://mathworld.wolfram.com/CubicSpline.html).
|
||||
// Because the `a` coefficients are all 1, they need not be stored.
|
||||
// This algorithm does a variation of the above algorithm.
|
||||
// Instead of using the traditional cubic (a + bt + ct^2 + dt^3), we use the bezier cubic.
|
||||
|
||||
let mut b = vec![DVec2::new(4., 4.); len_points];
|
||||
b[0] = DVec2::new(2., 2.);
|
||||
b[len_points - 1] = DVec2::new(2., 2.);
|
||||
|
||||
let mut c = vec![DVec2::new(1., 1.); len_points];
|
||||
|
||||
// 'd' is the the second point in a cubic bezier, which is what we solve for
|
||||
let mut d = vec![DVec2::ZERO; len_points];
|
||||
|
||||
d[0] = DVec2::new(2. * points[1].x + points[0].x, 2. * points[1].y + points[0].y);
|
||||
d[len_points - 1] = DVec2::new(3. * points[len_points - 1].x, 3. * points[len_points - 1].y);
|
||||
for idx in 1..(len_points - 1) {
|
||||
d[idx] = DVec2::new(4. * points[idx].x + 2. * points[idx + 1].x, 4. * points[idx].y + 2. * points[idx + 1].y);
|
||||
}
|
||||
|
||||
// Solve with Thomas algorithm (see https://en.wikipedia.org/wiki/Tridiagonal_matrix_algorithm)
|
||||
// Now we do row operations to eliminate `a` coefficients.
|
||||
c[0] /= -b[0];
|
||||
d[0] /= -b[0];
|
||||
#[allow(clippy::assign_op_pattern)]
|
||||
for i in 1..len_points {
|
||||
b[i] += c[i - 1];
|
||||
// For some reason this `+=` version makes the borrow checker mad:
|
||||
// d[i] += d[i-1]
|
||||
d[i] = d[i] + d[i - 1];
|
||||
c[i] /= -b[i];
|
||||
d[i] /= -b[i];
|
||||
}
|
||||
|
||||
// At this point b[i] == -a[i + 1] and a[i] == 0.
|
||||
// Now we do row operations to eliminate 'c' coefficients and solve.
|
||||
d[len_points - 1] *= -1.;
|
||||
#[allow(clippy::assign_op_pattern)]
|
||||
for i in (0..len_points - 1).rev() {
|
||||
d[i] = d[i] - (c[i] * d[i + 1]);
|
||||
d[i] *= -1.; // d[i] /= b[i]
|
||||
}
|
||||
|
||||
d
|
||||
}
|
||||
|
||||
/// Solve for the first handle of a closed spline. (The opposite handle can be found by mirroring the result about the anchor.)
|
||||
/// If called with fewer than 3 points, this function will return an empty result.
|
||||
pub fn solve_spline_first_handle_closed(points: &[DVec2]) -> Vec<DVec2> {
|
||||
let len_points = points.len();
|
||||
if len_points < 3 {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
// Matrix coefficients `a`, `b` and `c` (see https://mathworld.wolfram.com/CubicSpline.html).
|
||||
// We don't really need to allocate them but it keeps the maths understandable.
|
||||
let a = vec![DVec2::ONE; len_points];
|
||||
let b = vec![DVec2::splat(4.); len_points];
|
||||
let c = vec![DVec2::ONE; len_points];
|
||||
|
||||
let mut cmod = vec![DVec2::ZERO; len_points];
|
||||
let mut u = vec![DVec2::ZERO; len_points];
|
||||
|
||||
// `x` is initially the output of the matrix multiplication, but is converted to the second value.
|
||||
let mut x = vec![DVec2::ZERO; len_points];
|
||||
|
||||
for (i, point) in x.iter_mut().enumerate() {
|
||||
let previous_i = i.checked_sub(1).unwrap_or(len_points - 1);
|
||||
let next_i = (i + 1) % len_points;
|
||||
*point = 3. * (points[next_i] - points[previous_i]);
|
||||
}
|
||||
|
||||
// Solve using https://en.wikipedia.org/wiki/Tridiagonal_matrix_algorithm#Variants (the variant using periodic boundary conditions).
|
||||
// This code below is based on the reference C language implementation provided in that section of the article.
|
||||
let alpha = a[0];
|
||||
let beta = c[len_points - 1];
|
||||
|
||||
// Arbitrary, but chosen such that division by zero is avoided.
|
||||
let gamma = -b[0];
|
||||
|
||||
cmod[0] = alpha / (b[0] - gamma);
|
||||
u[0] = gamma / (b[0] - gamma);
|
||||
x[0] /= b[0] - gamma;
|
||||
|
||||
// Handle from from `1` to `len_points - 2` (inclusive).
|
||||
for ix in 1..=(len_points - 2) {
|
||||
let m = 1.0 / (b[ix] - a[ix] * cmod[ix - 1]);
|
||||
cmod[ix] = c[ix] * m;
|
||||
u[ix] = (0.0 - a[ix] * u[ix - 1]) * m;
|
||||
x[ix] = (x[ix] - a[ix] * x[ix - 1]) * m;
|
||||
}
|
||||
|
||||
// Handle `len_points - 1`.
|
||||
let m = 1.0 / (b[len_points - 1] - alpha * beta / gamma - beta * cmod[len_points - 2]);
|
||||
u[len_points - 1] = (alpha - a[len_points - 1] * u[len_points - 2]) * m;
|
||||
x[len_points - 1] = (x[len_points - 1] - a[len_points - 1] * x[len_points - 2]) * m;
|
||||
|
||||
// Loop from `len_points - 2` to `0` (inclusive).
|
||||
for ix in (0..=(len_points - 2)).rev() {
|
||||
u[ix] = u[ix] - cmod[ix] * u[ix + 1];
|
||||
x[ix] = x[ix] - cmod[ix] * x[ix + 1];
|
||||
}
|
||||
|
||||
let fact = (x[0] + x[len_points - 1] * beta / gamma) / (1.0 + u[0] + u[len_points - 1] * beta / gamma);
|
||||
|
||||
for ix in 0..(len_points) {
|
||||
x[ix] -= fact * u[ix];
|
||||
}
|
||||
|
||||
let mut real = vec![DVec2::ZERO; len_points];
|
||||
for i in 0..len_points {
|
||||
let previous = i.checked_sub(1).unwrap_or(len_points - 1);
|
||||
let next = (i + 1) % len_points;
|
||||
real[i] = x[previous] * a[next] + x[i] * b[i] + x[next] * c[i];
|
||||
}
|
||||
|
||||
// The matrix is now solved.
|
||||
|
||||
// Since we have computed the derivative, work back to find the start handle.
|
||||
for i in 0..len_points {
|
||||
x[i] = (x[i] / 3.) + points[i];
|
||||
}
|
||||
|
||||
x
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn closed_spline() {
|
||||
use graphene_vector::{dvec2_to_point, point_to_dvec2};
|
||||
use kurbo::{BezPath, ParamCurve, ParamCurveDeriv};
|
||||
|
||||
// These points are just chosen arbitrary
|
||||
let points = [DVec2::new(0., 0.), DVec2::new(0., 0.), DVec2::new(6., 5.), DVec2::new(7., 9.), DVec2::new(2., 3.)];
|
||||
|
||||
// List of first handle or second point in a cubic bezier curve.
|
||||
let first_handles = solve_spline_first_handle_closed(&points);
|
||||
|
||||
// Construct the Subpath
|
||||
let mut bezpath = BezPath::new();
|
||||
bezpath.move_to(dvec2_to_point(points[0]));
|
||||
|
||||
for i in 0..first_handles.len() {
|
||||
let next_i = i + 1;
|
||||
let next_i = if next_i == first_handles.len() { 0 } else { next_i };
|
||||
|
||||
// First handle or second point of a cubic Bezier curve.
|
||||
let p1 = dvec2_to_point(first_handles[i]);
|
||||
// Second handle or third point of a cubic Bezier curve.
|
||||
let p2 = dvec2_to_point(2. * points[next_i] - first_handles[next_i]);
|
||||
// Endpoint or fourth point of a cubic Bezier curve.
|
||||
let p3 = dvec2_to_point(points[next_i]);
|
||||
|
||||
bezpath.curve_to(p1, p2, p3);
|
||||
}
|
||||
|
||||
// For each pair of bézier curves, ensure that the second derivative is continuous
|
||||
for (bézier_a, bézier_b) in bezpath.segments().zip(bezpath.segments().skip(1).chain(bezpath.segments().take(1))) {
|
||||
let derivative2_end_a = point_to_dvec2(bézier_a.to_cubic().deriv().eval(1.));
|
||||
let derivative2_start_b = point_to_dvec2(bézier_b.to_cubic().deriv().eval(0.));
|
||||
|
||||
assert!(
|
||||
derivative2_end_a.abs_diff_eq(derivative2_start_b, 1e-10),
|
||||
"second derivative at the end of a {derivative2_end_a} is equal to the second derivative at the start of b {derivative2_start_b}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
use super::misc::{ArcType, AsU64, GridType};
|
||||
use bezier_rs::Subpath;
|
||||
use glam::DVec2;
|
||||
use graphene_core::context::Ctx;
|
||||
use graphene_core::registry::types::{Angle, PixelSize};
|
||||
use graphene_vector::{HandleId, PointId, SegmentId, StrokeId, VectorData, VectorDataTable};
|
||||
|
||||
trait CornerRadius {
|
||||
fn generate(self, size: DVec2, clamped: bool) -> VectorDataTable;
|
||||
}
|
||||
impl CornerRadius for f64 {
|
||||
fn generate(self, size: DVec2, clamped: bool) -> VectorDataTable {
|
||||
let clamped_radius = if clamped { self.clamp(0., size.x.min(size.y).max(0.) / 2.) } else { self };
|
||||
VectorDataTable::new(VectorData::from_subpath(Subpath::new_rounded_rect(size / -2., size / 2., [clamped_radius; 4])))
|
||||
}
|
||||
}
|
||||
impl CornerRadius for [f64; 4] {
|
||||
fn generate(self, size: DVec2, clamped: bool) -> VectorDataTable {
|
||||
let clamped_radius = if clamped {
|
||||
// Algorithm follows the CSS spec: <https://drafts.csswg.org/css-backgrounds/#corner-overlap>
|
||||
|
||||
let mut scale_factor: f64 = 1.;
|
||||
for i in 0..4 {
|
||||
let side_length = if i % 2 == 0 { size.x } else { size.y };
|
||||
let adjacent_corner_radius_sum = self[i] + self[(i + 1) % 4];
|
||||
if side_length < adjacent_corner_radius_sum {
|
||||
scale_factor = scale_factor.min(side_length / adjacent_corner_radius_sum);
|
||||
}
|
||||
}
|
||||
self.map(|x| x * scale_factor)
|
||||
} else {
|
||||
self
|
||||
};
|
||||
VectorDataTable::new(VectorData::from_subpath(Subpath::new_rounded_rect(size / -2., size / 2., clamped_radius)))
|
||||
}
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Vector: Shape"))]
|
||||
fn circle(_: impl Ctx, _primary: (), #[default(50.)] radius: f64) -> VectorDataTable {
|
||||
let radius = radius.abs();
|
||||
VectorDataTable::new(VectorData::from_subpath(Subpath::new_ellipse(DVec2::splat(-radius), DVec2::splat(radius))))
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Vector: Shape"))]
|
||||
fn arc(
|
||||
_: impl Ctx,
|
||||
_primary: (),
|
||||
#[default(50.)] radius: f64,
|
||||
start_angle: Angle,
|
||||
#[default(270.)]
|
||||
#[range((0., 360.))]
|
||||
sweep_angle: Angle,
|
||||
arc_type: ArcType,
|
||||
) -> VectorDataTable {
|
||||
VectorDataTable::new(VectorData::from_subpath(Subpath::new_arc(
|
||||
radius,
|
||||
start_angle / 360. * std::f64::consts::TAU,
|
||||
sweep_angle / 360. * std::f64::consts::TAU,
|
||||
match arc_type {
|
||||
ArcType::Open => bezier_rs::ArcType::Open,
|
||||
ArcType::Closed => bezier_rs::ArcType::Closed,
|
||||
ArcType::PieSlice => bezier_rs::ArcType::PieSlice,
|
||||
},
|
||||
)))
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Vector: Shape"))]
|
||||
fn ellipse(_: impl Ctx, _primary: (), #[default(50)] radius_x: f64, #[default(25)] radius_y: f64) -> VectorDataTable {
|
||||
let radius = DVec2::new(radius_x, radius_y);
|
||||
let corner1 = -radius;
|
||||
let corner2 = radius;
|
||||
|
||||
let mut ellipse = VectorData::from_subpath(Subpath::new_ellipse(corner1, corner2));
|
||||
|
||||
let len = ellipse.segment_domain.ids().len();
|
||||
for i in 0..len {
|
||||
ellipse
|
||||
.colinear_manipulators
|
||||
.push([HandleId::end(ellipse.segment_domain.ids()[i]), HandleId::primary(ellipse.segment_domain.ids()[(i + 1) % len])]);
|
||||
}
|
||||
|
||||
VectorDataTable::new(ellipse)
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Vector: Shape"), properties("rectangle_properties"))]
|
||||
fn rectangle<T: CornerRadius>(
|
||||
_: impl Ctx,
|
||||
_primary: (),
|
||||
#[default(100)] width: f64,
|
||||
#[default(100)] height: f64,
|
||||
_individual_corner_radii: bool, // TODO: Move this to the bottom once we have a migration capability
|
||||
#[implementations(f64, [f64; 4])] corner_radius: T,
|
||||
#[default(true)] clamped: bool,
|
||||
) -> VectorDataTable {
|
||||
corner_radius.generate(DVec2::new(width, height), clamped)
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Vector: Shape"))]
|
||||
fn regular_polygon<T: AsU64>(
|
||||
_: impl Ctx,
|
||||
_primary: (),
|
||||
#[default(6)]
|
||||
#[hard_min(3.)]
|
||||
#[implementations(u32, u64, f64)]
|
||||
sides: T,
|
||||
#[default(50)] radius: f64,
|
||||
) -> VectorDataTable {
|
||||
let points = sides.as_u64();
|
||||
let radius: f64 = radius * 2.;
|
||||
VectorDataTable::new(VectorData::from_subpath(Subpath::new_regular_polygon(DVec2::splat(-radius), points, radius)))
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Vector: Shape"))]
|
||||
fn star<T: AsU64>(
|
||||
_: impl Ctx,
|
||||
_primary: (),
|
||||
#[default(5)]
|
||||
#[hard_min(2.)]
|
||||
#[implementations(u32, u64, f64)]
|
||||
sides: T,
|
||||
#[default(50)] radius_1: f64,
|
||||
#[default(25)] radius_2: f64,
|
||||
) -> VectorDataTable {
|
||||
let points = sides.as_u64();
|
||||
let diameter: f64 = radius_1 * 2.;
|
||||
let inner_diameter = radius_2 * 2.;
|
||||
|
||||
VectorDataTable::new(VectorData::from_subpath(Subpath::new_star_polygon(DVec2::splat(-diameter), points, diameter, inner_diameter)))
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Vector: Shape"))]
|
||||
fn line(_: impl Ctx, _primary: (), #[default((0., -50.))] start: PixelSize, #[default((0., 50.))] end: PixelSize) -> VectorDataTable {
|
||||
VectorDataTable::new(VectorData::from_subpath(Subpath::new_line(start, end)))
|
||||
}
|
||||
|
||||
trait GridSpacing {
|
||||
fn as_dvec2(&self) -> DVec2;
|
||||
}
|
||||
impl GridSpacing for f64 {
|
||||
fn as_dvec2(&self) -> DVec2 {
|
||||
DVec2::splat(*self)
|
||||
}
|
||||
}
|
||||
impl GridSpacing for DVec2 {
|
||||
fn as_dvec2(&self) -> DVec2 {
|
||||
*self
|
||||
}
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Vector: Shape"), properties("grid_properties"))]
|
||||
fn grid<T: GridSpacing>(
|
||||
_: impl Ctx,
|
||||
_primary: (),
|
||||
grid_type: GridType,
|
||||
#[hard_min(0.)]
|
||||
#[default(10)]
|
||||
#[implementations(f64, DVec2)]
|
||||
spacing: T,
|
||||
#[default(30., 30.)] angles: DVec2,
|
||||
#[default(10)] columns: u32,
|
||||
#[default(10)] rows: u32,
|
||||
) -> VectorDataTable {
|
||||
let (x_spacing, y_spacing) = spacing.as_dvec2().into();
|
||||
let (angle_a, angle_b) = angles.into();
|
||||
|
||||
let mut vector_data = VectorData::default();
|
||||
let mut segment_id = SegmentId::ZERO;
|
||||
let mut point_id = PointId::ZERO;
|
||||
|
||||
match grid_type {
|
||||
GridType::Rectangular => {
|
||||
// Create rectangular grid points and connect them with line segments
|
||||
for y in 0..rows {
|
||||
for x in 0..columns {
|
||||
// Add current point to the grid
|
||||
let current_index = vector_data.point_domain.ids().len();
|
||||
vector_data.point_domain.push(point_id.next_id(), DVec2::new(x_spacing * x as f64, y_spacing * y as f64));
|
||||
|
||||
// Helper function to connect points with line segments
|
||||
let mut push_segment = |to_index: Option<usize>| {
|
||||
if let Some(other_index) = to_index {
|
||||
vector_data
|
||||
.segment_domain
|
||||
.push(segment_id.next_id(), other_index, current_index, bezier_rs::BezierHandles::Linear, StrokeId::ZERO);
|
||||
}
|
||||
};
|
||||
|
||||
// Connect to the point to the left (horizontal connection)
|
||||
push_segment((x > 0).then(|| current_index - 1));
|
||||
|
||||
// Connect to the point above (vertical connection)
|
||||
push_segment(current_index.checked_sub(columns as usize));
|
||||
}
|
||||
}
|
||||
}
|
||||
GridType::Isometric => {
|
||||
// Calculate isometric grid spacing based on angles
|
||||
let tan_a = angle_a.to_radians().tan();
|
||||
let tan_b = angle_b.to_radians().tan();
|
||||
let spacing = DVec2::new(y_spacing / (tan_a + tan_b), y_spacing);
|
||||
|
||||
// Create isometric grid points and connect them with line segments
|
||||
for y in 0..rows {
|
||||
for x in 0..columns {
|
||||
// Add current point to the grid with offset for odd columns
|
||||
let current_index = vector_data.point_domain.ids().len();
|
||||
|
||||
let a_angles_eaten = x.div_ceil(2) as f64;
|
||||
let b_angles_eaten = (x / 2) as f64;
|
||||
|
||||
let offset_y_fraction = b_angles_eaten * tan_b - a_angles_eaten * tan_a;
|
||||
|
||||
let position = DVec2::new(spacing.x * x as f64, spacing.y * y as f64 + offset_y_fraction * spacing.x);
|
||||
vector_data.point_domain.push(point_id.next_id(), position);
|
||||
|
||||
// Helper function to connect points with line segments
|
||||
let mut push_segment = |to_index: Option<usize>| {
|
||||
if let Some(other_index) = to_index {
|
||||
vector_data
|
||||
.segment_domain
|
||||
.push(segment_id.next_id(), other_index, current_index, bezier_rs::BezierHandles::Linear, StrokeId::ZERO);
|
||||
}
|
||||
};
|
||||
|
||||
// Connect to the point to the left
|
||||
push_segment((x > 0).then(|| current_index - 1));
|
||||
|
||||
// Connect to the point directly above
|
||||
push_segment(current_index.checked_sub(columns as usize));
|
||||
|
||||
// Additional diagonal connections for odd columns (creates hexagonal pattern)
|
||||
if x % 2 == 1 {
|
||||
// Connect to the point diagonally up-right (if not at right edge)
|
||||
push_segment(current_index.checked_sub(columns as usize - 1).filter(|_| x + 1 < columns));
|
||||
|
||||
// Connect to the point diagonally up-left
|
||||
push_segment(current_index.checked_sub(columns as usize + 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
VectorDataTable::new(vector_data)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn isometric_grid_test() {
|
||||
// Doesn't crash with weird angles
|
||||
grid((), (), GridType::Isometric, 0., (0., 0.).into(), 5, 5);
|
||||
grid((), (), GridType::Isometric, 90., (90., 90.).into(), 5, 5);
|
||||
|
||||
// Works properly
|
||||
let grid = grid((), (), GridType::Isometric, 10., (30., 30.).into(), 5, 5);
|
||||
assert_eq!(grid.instance_ref_iter().next().unwrap().instance.point_domain.ids().len(), 5 * 5);
|
||||
assert_eq!(grid.instance_ref_iter().next().unwrap().instance.segment_bezier_iter().count(), 4 * 5 + 4 * 9);
|
||||
for (_, bezier, _, _) in grid.instance_ref_iter().next().unwrap().instance.segment_bezier_iter() {
|
||||
assert_eq!(bezier.handles, bezier_rs::BezierHandles::Linear);
|
||||
assert!(
|
||||
((bezier.start - bezier.end).length() - 10.).abs() < 1e-5,
|
||||
"Length of {} should be 10",
|
||||
(bezier.start - bezier.end).length()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skew_isometric_grid_test() {
|
||||
let grid = grid((), (), GridType::Isometric, 10., (40., 30.).into(), 5, 5);
|
||||
assert_eq!(grid.instance_ref_iter().next().unwrap().instance.point_domain.ids().len(), 5 * 5);
|
||||
assert_eq!(grid.instance_ref_iter().next().unwrap().instance.segment_bezier_iter().count(), 4 * 5 + 4 * 9);
|
||||
for (_, bezier, _, _) in grid.instance_ref_iter().next().unwrap().instance.segment_bezier_iter() {
|
||||
assert_eq!(bezier.handles, bezier_rs::BezierHandles::Linear);
|
||||
let vector = bezier.start - bezier.end;
|
||||
let angle = (vector.angle_to(DVec2::X).to_degrees() + 180.) % 180.;
|
||||
assert!([90., 150., 40.].into_iter().any(|target| (target - angle).abs() < 1e-10), "unexpected angle of {}", angle)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
pub mod algorithms;
|
||||
pub mod generator_nodes;
|
||||
pub mod misc;
|
||||
pub mod modification;
|
||||
pub mod vector_nodes;
|
||||
@@ -0,0 +1,98 @@
|
||||
use dyn_any::DynAny;
|
||||
use glam::DVec2;
|
||||
use kurbo::Point;
|
||||
|
||||
/// Represents different ways of calculating the centroid.
|
||||
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)]
|
||||
#[widget(Radio)]
|
||||
pub enum CentroidType {
|
||||
/// The center of mass for the area of a solid shape's interior, as if made out of an infinitely flat material.
|
||||
#[default]
|
||||
Area,
|
||||
/// The center of mass for the arc length of a curved shape's perimeter, as if made out of an infinitely thin wire.
|
||||
Length,
|
||||
}
|
||||
|
||||
pub trait AsU64 {
|
||||
fn as_u64(&self) -> u64;
|
||||
}
|
||||
impl AsU64 for u32 {
|
||||
fn as_u64(&self) -> u64 {
|
||||
*self as u64
|
||||
}
|
||||
}
|
||||
impl AsU64 for u64 {
|
||||
fn as_u64(&self) -> u64 {
|
||||
*self
|
||||
}
|
||||
}
|
||||
impl AsU64 for f64 {
|
||||
fn as_u64(&self) -> u64 {
|
||||
*self as u64
|
||||
}
|
||||
}
|
||||
|
||||
pub trait AsI64 {
|
||||
fn as_i64(&self) -> i64;
|
||||
}
|
||||
impl AsI64 for u32 {
|
||||
fn as_i64(&self) -> i64 {
|
||||
*self as i64
|
||||
}
|
||||
}
|
||||
impl AsI64 for u64 {
|
||||
fn as_i64(&self) -> i64 {
|
||||
*self as i64
|
||||
}
|
||||
}
|
||||
impl AsI64 for f64 {
|
||||
fn as_i64(&self) -> i64 {
|
||||
*self as i64
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)]
|
||||
#[widget(Radio)]
|
||||
pub enum GridType {
|
||||
#[default]
|
||||
Rectangular,
|
||||
Isometric,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)]
|
||||
#[widget(Radio)]
|
||||
pub enum ArcType {
|
||||
#[default]
|
||||
Open,
|
||||
Closed,
|
||||
PieSlice,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)]
|
||||
#[widget(Radio)]
|
||||
pub enum MergeByDistanceAlgorithm {
|
||||
#[default]
|
||||
Spatial,
|
||||
Topological,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)]
|
||||
#[widget(Radio)]
|
||||
pub enum PointSpacingType {
|
||||
#[default]
|
||||
/// The desired spacing distance between points.
|
||||
Separation,
|
||||
/// The exact number of points to span the path.
|
||||
Quantity,
|
||||
}
|
||||
|
||||
pub fn point_to_dvec2(point: Point) -> DVec2 {
|
||||
DVec2 { x: point.x, y: point.y }
|
||||
}
|
||||
|
||||
pub fn dvec2_to_point(value: DVec2) -> Point {
|
||||
Point { x: value.x, y: value.y }
|
||||
}
|
||||
@@ -0,0 +1,728 @@
|
||||
use crate::misc::point_to_dvec2;
|
||||
use bezier_rs::BezierHandles;
|
||||
use dyn_any::DynAny;
|
||||
use glam::DVec2;
|
||||
use graphene_core::context::Ctx;
|
||||
use graphene_core::instances::Instance;
|
||||
use graphene_core::uuid::generate_uuid;
|
||||
use graphene_vector::{FillId, HandleId, HandleType, PointDomain, PointId, RegionDomain, RegionId, SegmentDomain, SegmentId, StrokeId, VectorData, VectorDataTable};
|
||||
use kurbo::{BezPath, PathEl, Point};
|
||||
use log::warn;
|
||||
use serde::de::{SeqAccess, Visitor};
|
||||
use serde::ser::SerializeSeq;
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::fmt;
|
||||
use std::hash::BuildHasher;
|
||||
use std::hash::Hash;
|
||||
|
||||
/// Represents a procedural change to the [`PointDomain`] in [`VectorData`].
|
||||
#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct PointModification {
|
||||
add: Vec<PointId>,
|
||||
remove: HashSet<PointId>,
|
||||
#[serde(serialize_with = "serialize_hashmap", deserialize_with = "deserialize_hashmap")]
|
||||
delta: HashMap<PointId, DVec2>,
|
||||
}
|
||||
|
||||
impl Hash for PointModification {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
generate_uuid().hash(state)
|
||||
}
|
||||
}
|
||||
|
||||
impl PointModification {
|
||||
/// Apply this modification to the specified [`PointDomain`].
|
||||
pub fn apply(&self, point_domain: &mut PointDomain, segment_domain: &mut SegmentDomain) {
|
||||
point_domain.retain(segment_domain, |id| !self.remove.contains(id));
|
||||
|
||||
for (index, (id, position)) in point_domain.positions_mut().enumerate() {
|
||||
let Some(&delta) = self.delta.get(&id) else { continue };
|
||||
if !delta.is_finite() {
|
||||
warn!("Invalid delta when applying a point modification");
|
||||
continue;
|
||||
}
|
||||
|
||||
*position += delta;
|
||||
|
||||
for (_, handles, start, end) in segment_domain.handles_mut() {
|
||||
if start == index {
|
||||
handles.move_start(delta);
|
||||
}
|
||||
if end == index {
|
||||
handles.move_end(delta);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for &add_id in &self.add {
|
||||
let Some(&position) = self.delta.get(&add_id) else { continue };
|
||||
if !position.is_finite() {
|
||||
warn!("Invalid position when applying a point modification");
|
||||
continue;
|
||||
}
|
||||
|
||||
point_domain.push(add_id, position);
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new modification that will convert an empty [`VectorData`] into the target [`VectorData`].
|
||||
pub fn create_from_vector(vector_data: &VectorData) -> Self {
|
||||
Self {
|
||||
add: vector_data.point_domain.ids().to_vec(),
|
||||
remove: HashSet::new(),
|
||||
delta: vector_data.point_domain.ids().iter().copied().zip(vector_data.point_domain.positions().iter().cloned()).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn push(&mut self, id: PointId, position: DVec2) {
|
||||
self.add.push(id);
|
||||
self.delta.insert(id, position);
|
||||
}
|
||||
|
||||
fn remove(&mut self, id: PointId) {
|
||||
self.remove.insert(id);
|
||||
self.add.retain(|&add| add != id);
|
||||
self.delta.remove(&id);
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a procedural change to the [`SegmentDomain`] in [`VectorData`].
|
||||
#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct SegmentModification {
|
||||
add: Vec<SegmentId>,
|
||||
remove: HashSet<SegmentId>,
|
||||
#[serde(serialize_with = "serialize_hashmap", deserialize_with = "deserialize_hashmap")]
|
||||
start_point: HashMap<SegmentId, PointId>,
|
||||
#[serde(serialize_with = "serialize_hashmap", deserialize_with = "deserialize_hashmap")]
|
||||
end_point: HashMap<SegmentId, PointId>,
|
||||
#[serde(serialize_with = "serialize_hashmap", deserialize_with = "deserialize_hashmap")]
|
||||
handle_primary: HashMap<SegmentId, Option<DVec2>>,
|
||||
#[serde(serialize_with = "serialize_hashmap", deserialize_with = "deserialize_hashmap")]
|
||||
handle_end: HashMap<SegmentId, Option<DVec2>>,
|
||||
#[serde(serialize_with = "serialize_hashmap", deserialize_with = "deserialize_hashmap")]
|
||||
stroke: HashMap<SegmentId, StrokeId>,
|
||||
}
|
||||
|
||||
impl SegmentModification {
|
||||
/// Apply this modification to the specified [`SegmentDomain`].
|
||||
pub fn apply(&self, segment_domain: &mut SegmentDomain, point_domain: &PointDomain) {
|
||||
segment_domain.retain(|id| !self.remove.contains(id), point_domain.ids().len());
|
||||
|
||||
for (id, point) in segment_domain.start_point_mut() {
|
||||
let Some(&new) = self.start_point.get(&id) else { continue };
|
||||
let Some(index) = point_domain.resolve_id(new) else {
|
||||
warn!("Invalid start ID when applying a segment modification");
|
||||
continue;
|
||||
};
|
||||
|
||||
*point = index;
|
||||
}
|
||||
|
||||
for (id, point) in segment_domain.end_point_mut() {
|
||||
let Some(&new) = self.end_point.get(&id) else { continue };
|
||||
let Some(index) = point_domain.resolve_id(new) else {
|
||||
warn!("Invalid end ID when applying a segment modification");
|
||||
continue;
|
||||
};
|
||||
|
||||
*point = index;
|
||||
}
|
||||
|
||||
for (id, handles, start, end) in segment_domain.handles_mut() {
|
||||
let Some(&start) = point_domain.positions().get(start) else { continue };
|
||||
let Some(&end) = point_domain.positions().get(end) else { continue };
|
||||
|
||||
// Compute the actual start and end position based on the offset from the anchor
|
||||
let start = self.handle_primary.get(&id).copied().map(|handle| handle.map(|handle| handle + start));
|
||||
let end = self.handle_end.get(&id).copied().map(|handle| handle.map(|handle| handle + end));
|
||||
|
||||
if !start.unwrap_or_default().is_none_or(|start| start.is_finite()) || !end.unwrap_or_default().is_none_or(|end| end.is_finite()) {
|
||||
warn!("Invalid handles when applying a segment modification");
|
||||
continue;
|
||||
}
|
||||
|
||||
match (start, end) {
|
||||
// The new handles are fully specified by the modification
|
||||
(Some(Some(handle_start)), Some(Some(handle_end))) => *handles = BezierHandles::Cubic { handle_start, handle_end },
|
||||
(Some(Some(handle)), Some(None)) | (Some(None), Some(Some(handle))) => *handles = BezierHandles::Quadratic { handle },
|
||||
(Some(None), Some(None)) => *handles = BezierHandles::Linear,
|
||||
// Remove the end handle
|
||||
(None, Some(None)) => {
|
||||
if let BezierHandles::Cubic { handle_start, .. } = *handles {
|
||||
*handles = BezierHandles::Quadratic { handle: handle_start }
|
||||
}
|
||||
}
|
||||
// Change the end handle
|
||||
(None, Some(Some(handle_end))) => match *handles {
|
||||
BezierHandles::Linear => *handles = BezierHandles::Quadratic { handle: handle_end },
|
||||
BezierHandles::Quadratic { handle: handle_start } => *handles = BezierHandles::Cubic { handle_start, handle_end },
|
||||
BezierHandles::Cubic { handle_start, .. } => *handles = BezierHandles::Cubic { handle_start, handle_end },
|
||||
},
|
||||
// Remove the start handle
|
||||
(Some(None), None) => *handles = BezierHandles::Linear,
|
||||
// Change the start handle
|
||||
(Some(Some(handle_start)), None) => match *handles {
|
||||
BezierHandles::Linear => *handles = BezierHandles::Quadratic { handle: handle_start },
|
||||
BezierHandles::Quadratic { .. } => *handles = BezierHandles::Quadratic { handle: handle_start },
|
||||
BezierHandles::Cubic { handle_end, .. } => *handles = BezierHandles::Cubic { handle_start, handle_end },
|
||||
},
|
||||
// No change
|
||||
(None, None) => {}
|
||||
};
|
||||
}
|
||||
|
||||
for (id, stroke) in segment_domain.stroke_mut() {
|
||||
let Some(&new) = self.stroke.get(&id) else { continue };
|
||||
*stroke = new;
|
||||
}
|
||||
|
||||
for &add_id in &self.add {
|
||||
let Some(&start) = self.start_point.get(&add_id) else { continue };
|
||||
let Some(&end) = self.end_point.get(&add_id) else { continue };
|
||||
let Some(&handle_start) = self.handle_primary.get(&add_id) else { continue };
|
||||
let Some(&handle_end) = self.handle_end.get(&add_id) else { continue };
|
||||
let Some(&stroke) = self.stroke.get(&add_id) else { continue };
|
||||
|
||||
let Some(start_index) = point_domain.resolve_id(start) else {
|
||||
warn!("invalid start id: {:#?}", start);
|
||||
continue;
|
||||
};
|
||||
let Some(end_index) = point_domain.resolve_id(end) else {
|
||||
warn!("invalid end id: {:#?}", end);
|
||||
continue;
|
||||
};
|
||||
|
||||
let start_position = point_domain.positions()[start_index];
|
||||
let end_position = point_domain.positions()[end_index];
|
||||
let handles = match (handle_start, handle_end) {
|
||||
(Some(handle_start), Some(handle_end)) => BezierHandles::Cubic {
|
||||
handle_start: handle_start + start_position,
|
||||
handle_end: handle_end + end_position,
|
||||
},
|
||||
(Some(handle), None) | (None, Some(handle)) => BezierHandles::Quadratic { handle: handle + start_position },
|
||||
(None, None) => BezierHandles::Linear,
|
||||
};
|
||||
|
||||
if !handles.is_finite() {
|
||||
warn!("invalid handles");
|
||||
continue;
|
||||
}
|
||||
|
||||
segment_domain.push(add_id, start_index, end_index, handles, stroke);
|
||||
}
|
||||
|
||||
assert!(
|
||||
segment_domain.start_point().iter().all(|&index| index < point_domain.ids().len()),
|
||||
"index should be in range {:#?}",
|
||||
segment_domain
|
||||
);
|
||||
assert!(
|
||||
segment_domain.end_point().iter().all(|&index| index < point_domain.ids().len()),
|
||||
"index should be in range {:#?}",
|
||||
segment_domain
|
||||
);
|
||||
}
|
||||
|
||||
/// Create a new modification that will convert an empty [`VectorData`] into the target [`VectorData`].
|
||||
pub fn create_from_vector(vector_data: &VectorData) -> Self {
|
||||
let point_id = |(&segment, &index)| (segment, vector_data.point_domain.ids()[index]);
|
||||
Self {
|
||||
add: vector_data.segment_domain.ids().to_vec(),
|
||||
remove: HashSet::new(),
|
||||
start_point: vector_data.segment_domain.ids().iter().zip(vector_data.segment_domain.start_point()).map(point_id).collect(),
|
||||
end_point: vector_data.segment_domain.ids().iter().zip(vector_data.segment_domain.end_point()).map(point_id).collect(),
|
||||
handle_primary: vector_data.segment_bezier_iter().map(|(id, b, _, _)| (id, b.handle_start().map(|handle| handle - b.start))).collect(),
|
||||
handle_end: vector_data.segment_bezier_iter().map(|(id, b, _, _)| (id, b.handle_end().map(|handle| handle - b.end))).collect(),
|
||||
stroke: vector_data.segment_domain.ids().iter().copied().zip(vector_data.segment_domain.stroke().iter().cloned()).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn push(&mut self, id: SegmentId, points: [PointId; 2], handles: [Option<DVec2>; 2], stroke: StrokeId) {
|
||||
self.remove.remove(&id);
|
||||
self.add.push(id);
|
||||
self.start_point.insert(id, points[0]);
|
||||
self.end_point.insert(id, points[1]);
|
||||
self.handle_primary.insert(id, handles[0]);
|
||||
self.handle_end.insert(id, handles[1]);
|
||||
self.stroke.insert(id, stroke);
|
||||
}
|
||||
|
||||
fn remove(&mut self, id: SegmentId) {
|
||||
self.remove.insert(id);
|
||||
self.add.retain(|&add| add != id);
|
||||
self.start_point.remove(&id);
|
||||
self.end_point.remove(&id);
|
||||
self.handle_primary.remove(&id);
|
||||
self.handle_end.remove(&id);
|
||||
self.stroke.remove(&id);
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a procedural change to the [`RegionDomain`] in [`VectorData`].
|
||||
#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct RegionModification {
|
||||
add: Vec<RegionId>,
|
||||
remove: HashSet<RegionId>,
|
||||
#[serde(serialize_with = "serialize_hashmap", deserialize_with = "deserialize_hashmap")]
|
||||
segment_range: HashMap<RegionId, std::ops::RangeInclusive<SegmentId>>,
|
||||
#[serde(serialize_with = "serialize_hashmap", deserialize_with = "deserialize_hashmap")]
|
||||
fill: HashMap<RegionId, FillId>,
|
||||
}
|
||||
|
||||
impl RegionModification {
|
||||
/// Apply this modification to the specified [`RegionDomain`].
|
||||
pub fn apply(&self, region_domain: &mut RegionDomain) {
|
||||
region_domain.retain(|id| !self.remove.contains(id));
|
||||
|
||||
for (id, segment_range) in region_domain.segment_range_mut() {
|
||||
let Some(new) = self.segment_range.get(&id) else { continue };
|
||||
*segment_range = new.clone(); // Range inclusive is not copy
|
||||
}
|
||||
|
||||
for (id, fill) in region_domain.fill_mut() {
|
||||
let Some(&new) = self.fill.get(&id) else { continue };
|
||||
*fill = new;
|
||||
}
|
||||
|
||||
for &add_id in &self.add {
|
||||
let Some(segment_range) = self.segment_range.get(&add_id) else { continue };
|
||||
let Some(&fill) = self.fill.get(&add_id) else { continue };
|
||||
region_domain.push(add_id, segment_range.clone(), fill);
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new modification that will convert an empty [`VectorData`] into the target [`VectorData`].
|
||||
pub fn create_from_vector(vector_data: &VectorData) -> Self {
|
||||
Self {
|
||||
add: vector_data.region_domain.ids().to_vec(),
|
||||
remove: HashSet::new(),
|
||||
segment_range: vector_data.region_domain.ids().iter().copied().zip(vector_data.region_domain.segment_range().iter().cloned()).collect(),
|
||||
fill: vector_data.region_domain.ids().iter().copied().zip(vector_data.region_domain.fill().iter().cloned()).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a procedural change to the [`VectorData`].
|
||||
#[derive(Clone, Debug, Default, PartialEq, DynAny, serde::Serialize, serde::Deserialize)]
|
||||
pub struct VectorModification {
|
||||
points: PointModification,
|
||||
segments: SegmentModification,
|
||||
regions: RegionModification,
|
||||
add_g1_continuous: HashSet<[HandleId; 2]>,
|
||||
remove_g1_continuous: HashSet<[HandleId; 2]>,
|
||||
}
|
||||
|
||||
/// A modification type that can be added to a [`VectorModification`].
|
||||
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub enum VectorModificationType {
|
||||
InsertSegment { id: SegmentId, points: [PointId; 2], handles: [Option<DVec2>; 2] },
|
||||
InsertPoint { id: PointId, position: DVec2 },
|
||||
|
||||
RemoveSegment { id: SegmentId },
|
||||
RemovePoint { id: PointId },
|
||||
|
||||
SetG1Continuous { handles: [HandleId; 2], enabled: bool },
|
||||
SetHandles { segment: SegmentId, handles: [Option<DVec2>; 2] },
|
||||
SetPrimaryHandle { segment: SegmentId, relative_position: DVec2 },
|
||||
SetEndHandle { segment: SegmentId, relative_position: DVec2 },
|
||||
SetStartPoint { segment: SegmentId, id: PointId },
|
||||
SetEndPoint { segment: SegmentId, id: PointId },
|
||||
|
||||
ApplyPointDelta { point: PointId, delta: DVec2 },
|
||||
ApplyPrimaryDelta { segment: SegmentId, delta: DVec2 },
|
||||
ApplyEndDelta { segment: SegmentId, delta: DVec2 },
|
||||
}
|
||||
|
||||
impl VectorModification {
|
||||
/// Apply this modification to the specified [`VectorData`].
|
||||
pub fn apply(&self, vector_data: &mut VectorData) {
|
||||
self.points.apply(&mut vector_data.point_domain, &mut vector_data.segment_domain);
|
||||
self.segments.apply(&mut vector_data.segment_domain, &vector_data.point_domain);
|
||||
self.regions.apply(&mut vector_data.region_domain);
|
||||
|
||||
let valid = |val: &[HandleId; 2]| vector_data.segment_domain.ids().contains(&val[0].segment) && vector_data.segment_domain.ids().contains(&val[1].segment);
|
||||
vector_data
|
||||
.colinear_manipulators
|
||||
.retain(|val| !self.remove_g1_continuous.contains(val) && !self.remove_g1_continuous.contains(&[val[1], val[0]]) && valid(val));
|
||||
|
||||
for handles in &self.add_g1_continuous {
|
||||
if !vector_data.colinear_manipulators.iter().any(|test| test == handles || test == &[handles[1], handles[0]]) && valid(handles) {
|
||||
vector_data.colinear_manipulators.push(*handles);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a [`VectorModificationType`] to this modification.
|
||||
pub fn modify(&mut self, vector_data_modification: &VectorModificationType) {
|
||||
match vector_data_modification {
|
||||
VectorModificationType::InsertSegment { id, points, handles } => self.segments.push(*id, *points, *handles, StrokeId::ZERO),
|
||||
VectorModificationType::InsertPoint { id, position } => self.points.push(*id, *position),
|
||||
|
||||
VectorModificationType::RemoveSegment { id } => self.segments.remove(*id),
|
||||
VectorModificationType::RemovePoint { id } => self.points.remove(*id),
|
||||
|
||||
VectorModificationType::SetG1Continuous { handles, enabled } => {
|
||||
if *enabled {
|
||||
if !self.add_g1_continuous.contains(&[handles[1], handles[0]]) {
|
||||
self.add_g1_continuous.insert(*handles);
|
||||
}
|
||||
self.remove_g1_continuous.remove(handles);
|
||||
self.remove_g1_continuous.remove(&[handles[1], handles[0]]);
|
||||
} else {
|
||||
if !self.remove_g1_continuous.contains(&[handles[1], handles[0]]) {
|
||||
self.remove_g1_continuous.insert(*handles);
|
||||
}
|
||||
self.add_g1_continuous.remove(handles);
|
||||
self.add_g1_continuous.remove(&[handles[1], handles[0]]);
|
||||
}
|
||||
}
|
||||
VectorModificationType::SetHandles { segment, handles } => {
|
||||
self.segments.handle_primary.insert(*segment, handles[0]);
|
||||
self.segments.handle_end.insert(*segment, handles[1]);
|
||||
}
|
||||
VectorModificationType::SetPrimaryHandle { segment, relative_position } => {
|
||||
self.segments.handle_primary.insert(*segment, Some(*relative_position));
|
||||
}
|
||||
VectorModificationType::SetEndHandle { segment, relative_position } => {
|
||||
self.segments.handle_end.insert(*segment, Some(*relative_position));
|
||||
}
|
||||
VectorModificationType::SetStartPoint { segment, id } => {
|
||||
self.segments.start_point.insert(*segment, *id);
|
||||
}
|
||||
VectorModificationType::SetEndPoint { segment, id } => {
|
||||
self.segments.end_point.insert(*segment, *id);
|
||||
}
|
||||
|
||||
VectorModificationType::ApplyPointDelta { point, delta } => {
|
||||
*self.points.delta.entry(*point).or_default() += *delta;
|
||||
}
|
||||
VectorModificationType::ApplyPrimaryDelta { segment, delta } => {
|
||||
let position = self.segments.handle_primary.entry(*segment).or_default();
|
||||
*position = Some(position.unwrap_or_default() + *delta);
|
||||
}
|
||||
VectorModificationType::ApplyEndDelta { segment, delta } => {
|
||||
let position = self.segments.handle_end.entry(*segment).or_default();
|
||||
*position = Some(position.unwrap_or_default() + *delta);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new modification that will convert an empty [`VectorData`] into the target [`VectorData`].
|
||||
pub fn create_from_vector(vector_data: &VectorData) -> Self {
|
||||
Self {
|
||||
points: PointModification::create_from_vector(vector_data),
|
||||
segments: SegmentModification::create_from_vector(vector_data),
|
||||
regions: RegionModification::create_from_vector(vector_data),
|
||||
add_g1_continuous: vector_data.colinear_manipulators.iter().copied().collect(),
|
||||
remove_g1_continuous: HashSet::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for VectorModification {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
generate_uuid().hash(state)
|
||||
}
|
||||
}
|
||||
|
||||
/// A node that applies a procedural modification to some [`VectorData`].
|
||||
#[node_macro::node(category(""))]
|
||||
async fn path_modify(_ctx: impl Ctx, mut vector_data: VectorDataTable, modification: Box<VectorModification>) -> VectorDataTable {
|
||||
if vector_data.is_empty() {
|
||||
vector_data.push(Instance::default());
|
||||
}
|
||||
let vector_data_instance = vector_data.get_mut(0).expect("push should give one item");
|
||||
modification.apply(vector_data_instance.instance);
|
||||
if vector_data.len() > 1 {
|
||||
warn!("The path modify ran on {} instances of vector data. Only the first can be modified.", vector_data.len());
|
||||
}
|
||||
vector_data
|
||||
}
|
||||
|
||||
// Do we want to enforce that all serialized/deserialized hashmaps are a vec of tuples?
|
||||
// TODO: Eventually remove this document upgrade code
|
||||
pub fn serialize_hashmap<K, V, S, H>(hashmap: &HashMap<K, V, H>, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
K: Serialize + Eq + Hash,
|
||||
V: Serialize,
|
||||
S: Serializer,
|
||||
H: BuildHasher,
|
||||
{
|
||||
let mut seq = serializer.serialize_seq(Some(hashmap.len()))?;
|
||||
for (key, value) in hashmap {
|
||||
seq.serialize_element(&(key, value))?;
|
||||
}
|
||||
seq.end()
|
||||
}
|
||||
|
||||
pub fn deserialize_hashmap<'de, K, V, D, H>(deserializer: D) -> Result<HashMap<K, V, H>, D::Error>
|
||||
where
|
||||
K: Deserialize<'de> + Eq + Hash,
|
||||
V: Deserialize<'de>,
|
||||
D: Deserializer<'de>,
|
||||
H: BuildHasher + Default,
|
||||
{
|
||||
struct HashMapVisitor<K, V, H> {
|
||||
#[allow(clippy::type_complexity)]
|
||||
marker: std::marker::PhantomData<fn() -> HashMap<K, V, H>>,
|
||||
}
|
||||
|
||||
impl<'de, K, V, H> Visitor<'de> for HashMapVisitor<K, V, H>
|
||||
where
|
||||
K: Deserialize<'de> + Eq + Hash,
|
||||
V: Deserialize<'de>,
|
||||
H: BuildHasher + Default,
|
||||
{
|
||||
type Value = HashMap<K, V, H>;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("a sequence of tuples")
|
||||
}
|
||||
|
||||
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
|
||||
where
|
||||
A: SeqAccess<'de>,
|
||||
{
|
||||
let mut hashmap = HashMap::default();
|
||||
while let Some((key, value)) = seq.next_element()? {
|
||||
hashmap.insert(key, value);
|
||||
}
|
||||
Ok(hashmap)
|
||||
}
|
||||
}
|
||||
|
||||
let visitor = HashMapVisitor { marker: std::marker::PhantomData };
|
||||
deserializer.deserialize_seq(visitor)
|
||||
}
|
||||
|
||||
pub struct AppendBezpath<'a> {
|
||||
first_point: Option<Point>,
|
||||
last_point: Option<Point>,
|
||||
first_point_index: Option<usize>,
|
||||
last_point_index: Option<usize>,
|
||||
first_segment_id: Option<SegmentId>,
|
||||
last_segment_id: Option<SegmentId>,
|
||||
point_id: PointId,
|
||||
segment_id: SegmentId,
|
||||
vector_data: &'a mut VectorData,
|
||||
}
|
||||
|
||||
impl<'a> AppendBezpath<'a> {
|
||||
fn new(vector_data: &'a mut VectorData) -> Self {
|
||||
Self {
|
||||
first_point: None,
|
||||
last_point: None,
|
||||
first_point_index: None,
|
||||
last_point_index: None,
|
||||
first_segment_id: None,
|
||||
last_segment_id: None,
|
||||
point_id: vector_data.point_domain.next_id(),
|
||||
segment_id: vector_data.segment_domain.next_id(),
|
||||
vector_data,
|
||||
}
|
||||
}
|
||||
|
||||
fn append_segment_and_close_path(&mut self, point: Point, handle: BezierHandles) {
|
||||
let handle = if self.first_point.unwrap() != point {
|
||||
// If the first point is not the same as the last point of the path then we append the segment
|
||||
// with given handle and point and then close the path with linear handle.
|
||||
self.append_segment(point, handle);
|
||||
BezierHandles::Linear
|
||||
} else {
|
||||
// if the endpoints are the same then we close the path with given handle.
|
||||
handle
|
||||
};
|
||||
|
||||
// Create a new segment.
|
||||
let next_segment_id = self.segment_id.next_id();
|
||||
self.vector_data
|
||||
.segment_domain
|
||||
.push(next_segment_id, self.last_point_index.unwrap(), self.first_point_index.unwrap(), handle, StrokeId::ZERO);
|
||||
|
||||
// Create a new region.
|
||||
let next_region_id = self.vector_data.region_domain.next_id();
|
||||
let first_segment_id = self.first_segment_id.unwrap_or(next_segment_id);
|
||||
let last_segment_id = next_segment_id;
|
||||
|
||||
self.vector_data.region_domain.push(next_region_id, first_segment_id..=last_segment_id, FillId::ZERO);
|
||||
}
|
||||
|
||||
fn append_segment(&mut self, end_point: Point, handle: BezierHandles) {
|
||||
// Append the point.
|
||||
let next_point_index = self.vector_data.point_domain.ids().len();
|
||||
let next_point_id = self.point_id.next_id();
|
||||
|
||||
self.vector_data.point_domain.push(next_point_id, point_to_dvec2(end_point));
|
||||
|
||||
// Append the segment.
|
||||
let next_segment_id = self.segment_id.next_id();
|
||||
self.vector_data
|
||||
.segment_domain
|
||||
.push(next_segment_id, self.last_point_index.unwrap(), next_point_index, handle, StrokeId::ZERO);
|
||||
|
||||
// Update the states.
|
||||
self.last_point = Some(end_point);
|
||||
self.last_point_index = Some(next_point_index);
|
||||
|
||||
self.first_segment_id = Some(self.first_segment_id.unwrap_or(next_segment_id));
|
||||
self.last_segment_id = Some(next_segment_id);
|
||||
}
|
||||
|
||||
fn append_first_point(&mut self, point: Point) {
|
||||
self.first_point = Some(point);
|
||||
self.last_point = Some(point);
|
||||
|
||||
// Append the first point.
|
||||
let next_point_index = self.vector_data.point_domain.ids().len();
|
||||
self.vector_data.point_domain.push(self.point_id.next_id(), point_to_dvec2(point));
|
||||
|
||||
// Update the state.
|
||||
self.first_point_index = Some(next_point_index);
|
||||
self.last_point_index = Some(next_point_index);
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.first_point = None;
|
||||
self.last_point = None;
|
||||
self.first_point_index = None;
|
||||
self.last_point_index = None;
|
||||
self.first_segment_id = None;
|
||||
self.last_segment_id = None;
|
||||
}
|
||||
|
||||
pub fn append_bezpath(vector_data: &'a mut VectorData, bezpath: BezPath) {
|
||||
let mut this = Self::new(vector_data);
|
||||
let mut elements = bezpath.elements().iter().peekable();
|
||||
|
||||
while let Some(element) = elements.next() {
|
||||
let close_path = elements.peek().is_some_and(|elm| **elm == PathEl::ClosePath);
|
||||
|
||||
match *element {
|
||||
PathEl::MoveTo(point) => this.append_first_point(point),
|
||||
PathEl::LineTo(point) => {
|
||||
let handle = BezierHandles::Linear;
|
||||
if close_path {
|
||||
this.append_segment_and_close_path(point, handle);
|
||||
} else {
|
||||
this.append_segment(point, handle);
|
||||
}
|
||||
}
|
||||
PathEl::QuadTo(point, point1) => {
|
||||
let handle = BezierHandles::Quadratic { handle: point_to_dvec2(point) };
|
||||
if close_path {
|
||||
this.append_segment_and_close_path(point1, handle);
|
||||
} else {
|
||||
this.append_segment(point1, handle);
|
||||
}
|
||||
}
|
||||
PathEl::CurveTo(point, point1, point2) => {
|
||||
let handle = BezierHandles::Cubic {
|
||||
handle_start: point_to_dvec2(point),
|
||||
handle_end: point_to_dvec2(point1),
|
||||
};
|
||||
|
||||
if close_path {
|
||||
this.append_segment_and_close_path(point2, handle);
|
||||
} else {
|
||||
this.append_segment(point2, handle);
|
||||
}
|
||||
}
|
||||
PathEl::ClosePath => {
|
||||
// Already handled using `append_segment_and_close_path()` hence we reset state and continue.
|
||||
this.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait VectorDataExt {
|
||||
/// Appends a Kurbo BezPath to the vector data.
|
||||
fn append_bezpath(&mut self, bezpath: BezPath);
|
||||
}
|
||||
|
||||
impl VectorDataExt for VectorData {
|
||||
fn append_bezpath(&mut self, bezpath: BezPath) {
|
||||
AppendBezpath::append_bezpath(self, bezpath);
|
||||
}
|
||||
}
|
||||
|
||||
pub trait HandleExt {
|
||||
/// Set the handle's position relative to the anchor which is the start anchor for the primary handle and end anchor for the end handle.
|
||||
#[must_use]
|
||||
fn set_relative_position(self, relative_position: DVec2) -> VectorModificationType;
|
||||
}
|
||||
|
||||
impl HandleExt for HandleId {
|
||||
fn set_relative_position(self, relative_position: DVec2) -> VectorModificationType {
|
||||
let Self { ty, segment } = self;
|
||||
match ty {
|
||||
HandleType::Primary => VectorModificationType::SetPrimaryHandle { segment, relative_position },
|
||||
HandleType::End => VectorModificationType::SetEndHandle { segment, relative_position },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn modify_new() {
|
||||
let vector_data = VectorData::from_subpaths(
|
||||
[bezier_rs::Subpath::new_ellipse(DVec2::ZERO, DVec2::ONE), bezier_rs::Subpath::new_rect(DVec2::NEG_ONE, DVec2::ZERO)],
|
||||
false,
|
||||
);
|
||||
|
||||
let modify = VectorModification::create_from_vector(&vector_data);
|
||||
|
||||
let mut new = VectorData::default();
|
||||
modify.apply(&mut new);
|
||||
assert_eq!(vector_data, new);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn modify_existing() {
|
||||
use bezier_rs::{Bezier, Subpath};
|
||||
let subpaths = [
|
||||
Subpath::new_ellipse(DVec2::ZERO, DVec2::ONE),
|
||||
Subpath::new_rect(DVec2::NEG_ONE, DVec2::ZERO),
|
||||
Subpath::from_beziers(
|
||||
&[
|
||||
Bezier::from_quadratic_dvec2(DVec2::new(0., 0.), DVec2::new(5., 10.), DVec2::new(10., 0.)),
|
||||
Bezier::from_quadratic_dvec2(DVec2::new(10., 0.), DVec2::new(15., 10.), DVec2::new(20., 0.)),
|
||||
],
|
||||
false,
|
||||
),
|
||||
];
|
||||
let mut vector_data = VectorData::from_subpaths(subpaths, false);
|
||||
|
||||
let mut modify_new = VectorModification::create_from_vector(&vector_data);
|
||||
let mut modify_original = VectorModification::default();
|
||||
|
||||
for modification in [&mut modify_new, &mut modify_original] {
|
||||
let point = vector_data.point_domain.ids()[0];
|
||||
modification.modify(&VectorModificationType::ApplyPointDelta { point, delta: DVec2::X * 0.5 });
|
||||
let point = vector_data.point_domain.ids()[9];
|
||||
modification.modify(&VectorModificationType::ApplyPointDelta { point, delta: DVec2::X });
|
||||
}
|
||||
|
||||
let mut new = VectorData::default();
|
||||
modify_new.apply(&mut new);
|
||||
|
||||
modify_original.apply(&mut vector_data);
|
||||
|
||||
assert_eq!(vector_data, new);
|
||||
assert_eq!(vector_data.point_domain.positions()[0], DVec2::X);
|
||||
assert_eq!(vector_data.point_domain.positions()[9], DVec2::new(11., 0.));
|
||||
assert_eq!(
|
||||
vector_data.segment_bezier_iter().nth(8).unwrap().1,
|
||||
Bezier::from_quadratic_dvec2(DVec2::new(0., 0.), DVec2::new(5., 10.), DVec2::new(11., 0.))
|
||||
);
|
||||
assert_eq!(
|
||||
vector_data.segment_bezier_iter().nth(9).unwrap().1,
|
||||
Bezier::from_quadratic_dvec2(DVec2::new(11., 0.), DVec2::new(16., 10.), DVec2::new(20., 0.))
|
||||
);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user