mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-16 23:08:05 +08:00
Initial working versions of decimate and vectorize
This commit is contained in:
@@ -40,6 +40,8 @@ lyon_geom = { workspace = true }
|
||||
log = { workspace = true }
|
||||
base64 = { workspace = true }
|
||||
poly-cool = { workspace = true }
|
||||
vtracer = { workspace = true }
|
||||
visioncortex = { workspace = true }
|
||||
|
||||
# Optional workspace dependencies
|
||||
serde = { workspace = true, optional = true }
|
||||
|
||||
62
node-graph/gcore/src/vector/algorithms/decimation.rs
Normal file
62
node-graph/gcore/src/vector/algorithms/decimation.rs
Normal file
@@ -0,0 +1,62 @@
|
||||
use glam::DVec2;
|
||||
|
||||
/// Implements the Ramer-Douglas-Peucker algorithm to find indices of points to keep.
|
||||
pub fn ramer_douglas_peucker(points: &[DVec2], epsilon: f64) -> Vec<usize> {
|
||||
if points.len() <= 2 {
|
||||
return (0..points.len()).collect();
|
||||
}
|
||||
|
||||
let mut kept_indices = Vec::new();
|
||||
rdp_recursive(points, 0, points.len() - 1, epsilon, &mut kept_indices);
|
||||
kept_indices.sort_unstable();
|
||||
kept_indices.dedup();
|
||||
kept_indices
|
||||
}
|
||||
|
||||
pub fn rdp_recursive(points: &[DVec2], start_idx: usize, end_idx: usize, epsilon: f64, kept_indices: &mut Vec<usize>) {
|
||||
if start_idx >= end_idx {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut dmax = 0.0;
|
||||
let mut index = start_idx;
|
||||
|
||||
// Find the point with maximum perpendicular distance from the line segment
|
||||
for i in (start_idx + 1)..end_idx {
|
||||
let d = perpendicular_distance(points[i], points[start_idx], points[end_idx]);
|
||||
if d > dmax {
|
||||
index = i;
|
||||
dmax = d;
|
||||
}
|
||||
}
|
||||
|
||||
// If max distance is greater than epsilon, recursively simplify
|
||||
if dmax > epsilon {
|
||||
rdp_recursive(points, start_idx, index, epsilon, kept_indices);
|
||||
rdp_recursive(points, index, end_idx, epsilon, kept_indices);
|
||||
} else {
|
||||
// Keep only the endpoints
|
||||
kept_indices.push(start_idx);
|
||||
kept_indices.push(end_idx);
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculates the perpendicular distance from a point to a line defined by two endpoints.
|
||||
pub fn perpendicular_distance(point: DVec2, line_start: DVec2, line_end: DVec2) -> f64 {
|
||||
let dx = line_end.x - line_start.x;
|
||||
let dy = line_end.y - line_start.y;
|
||||
|
||||
let line_length_squared = dx * dx + dy * dy;
|
||||
|
||||
// If the line segment is actually a point, return the distance to that point
|
||||
if line_length_squared == 0.0 {
|
||||
return point.distance(line_start);
|
||||
}
|
||||
|
||||
// Calculate perpendicular distance using the cross product formula:
|
||||
// distance = |dy·px - dx·py + x2·y1 - y2·x1| / √(dx² + dy²)
|
||||
let numerator = (dy * point.x - dx * point.y + line_end.x * line_start.y - line_end.y * line_start.x).abs();
|
||||
let denominator = line_length_squared.sqrt();
|
||||
|
||||
numerator / denominator
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
pub mod bezpath_algorithms;
|
||||
mod contants;
|
||||
pub mod decimation;
|
||||
pub mod instance;
|
||||
pub mod intersection;
|
||||
pub mod merge_by_distance;
|
||||
@@ -7,3 +8,4 @@ pub mod offset_subpath;
|
||||
pub mod poisson_disk;
|
||||
pub mod spline;
|
||||
pub mod util;
|
||||
pub mod vectorize;
|
||||
|
||||
357
node-graph/gcore/src/vector/algorithms/vectorize.rs
Normal file
357
node-graph/gcore/src/vector/algorithms/vectorize.rs
Normal file
@@ -0,0 +1,357 @@
|
||||
use crate::{
|
||||
Graphic,
|
||||
raster_types::{CPU, Raster},
|
||||
table::{Table, TableRow, TableRowRef},
|
||||
vector::Vector,
|
||||
vector::VectorExt,
|
||||
vector::style::Fill,
|
||||
};
|
||||
use glam::{DAffine2, DVec2};
|
||||
use graphene_core_shaders::color::Color;
|
||||
use kurbo::{BezPath, PathEl};
|
||||
use visioncortex::PathSimplifyMode;
|
||||
use vtracer::{ColorMode, Config, Hierarchical};
|
||||
|
||||
/// Parses SVG path data and appends it to a Vector
|
||||
pub fn parse_svg_paths_to_vector(svg_content: &str) -> Vec<TableRow<Vector>> {
|
||||
let mut rows = Vec::new();
|
||||
|
||||
for element in extract_path_elements(svg_content) {
|
||||
let attributes = parse_path_attributes(&element);
|
||||
let Some(path_data) = attribute_value(&attributes, "d") else { continue };
|
||||
let Ok(bezpath) = BezPath::from_svg(path_data) else { continue };
|
||||
|
||||
let fill = parse_fill_attribute(attribute_value(&attributes, "fill"));
|
||||
let transform = parse_transform_attribute(attribute_value(&attributes, "transform"));
|
||||
|
||||
for subpath in split_bezpath_subpaths(&bezpath) {
|
||||
if subpath.elements().is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut vector = Vector::default();
|
||||
vector.append_bezpath(subpath);
|
||||
vector.style.set_fill(fill.clone());
|
||||
|
||||
let mut table_row = TableRow::new_from_element(vector);
|
||||
table_row.transform = transform;
|
||||
rows.push(table_row);
|
||||
}
|
||||
}
|
||||
|
||||
rows
|
||||
}
|
||||
|
||||
fn split_bezpath_subpaths(bezpath: &BezPath) -> Vec<BezPath> {
|
||||
let mut subpaths = Vec::new();
|
||||
let mut current = BezPath::new();
|
||||
|
||||
for element in bezpath.elements() {
|
||||
match element {
|
||||
PathEl::MoveTo(_) => {
|
||||
if !current.is_empty() {
|
||||
subpaths.push(std::mem::take(&mut current));
|
||||
}
|
||||
current.push(*element);
|
||||
}
|
||||
PathEl::ClosePath => {
|
||||
current.close_path();
|
||||
subpaths.push(std::mem::take(&mut current));
|
||||
}
|
||||
_ => current.push(*element),
|
||||
}
|
||||
}
|
||||
|
||||
if !current.is_empty() {
|
||||
subpaths.push(current);
|
||||
}
|
||||
|
||||
subpaths
|
||||
}
|
||||
|
||||
pub fn extract_path_elements(svg_content: &str) -> Vec<String> {
|
||||
let mut elements = Vec::new();
|
||||
let mut search_start = 0;
|
||||
|
||||
while let Some(relative_start) = svg_content[search_start..].find("<path") {
|
||||
let start = search_start + relative_start;
|
||||
let mut index = start + "<path".len();
|
||||
let mut in_quotes = false;
|
||||
let mut quote_char = '\0';
|
||||
|
||||
while index < svg_content.len() {
|
||||
let ch = svg_content.as_bytes()[index] as char;
|
||||
|
||||
match ch {
|
||||
'"' | '\'' if !in_quotes => {
|
||||
in_quotes = true;
|
||||
quote_char = ch;
|
||||
}
|
||||
c if c == quote_char && in_quotes => {
|
||||
in_quotes = false;
|
||||
}
|
||||
'>' if !in_quotes => {
|
||||
elements.push(svg_content[start..=index].to_string());
|
||||
search_start = index + 1;
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
index += 1;
|
||||
}
|
||||
|
||||
if index >= svg_content.len() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
elements
|
||||
}
|
||||
|
||||
pub fn is_attribute_name_char(ch: char) -> bool {
|
||||
ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_' | ':' | '.')
|
||||
}
|
||||
|
||||
pub fn parse_path_attributes(element: &str) -> Vec<(String, String)> {
|
||||
let mut attributes = Vec::new();
|
||||
let mut body = element.trim();
|
||||
|
||||
if let Some(start) = body.find("<path") {
|
||||
body = &body[start + "<path".len()..];
|
||||
} else {
|
||||
return attributes;
|
||||
}
|
||||
|
||||
if let Some(end) = body.rfind('>') {
|
||||
body = &body[..end];
|
||||
}
|
||||
|
||||
body = body.trim();
|
||||
if let Some(stripped) = body.strip_suffix('/') {
|
||||
body = stripped.trim_end();
|
||||
}
|
||||
|
||||
let bytes = body.as_bytes();
|
||||
let mut index = 0;
|
||||
|
||||
while index < bytes.len() {
|
||||
while index < bytes.len() && bytes[index].is_ascii_whitespace() {
|
||||
index += 1;
|
||||
}
|
||||
|
||||
if index >= bytes.len() || bytes[index] == b'/' {
|
||||
break;
|
||||
}
|
||||
|
||||
let name_start = index;
|
||||
while index < bytes.len() && is_attribute_name_char(body.as_bytes()[index] as char) {
|
||||
index += 1;
|
||||
}
|
||||
|
||||
if name_start == index {
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
let name = body[name_start..index].trim();
|
||||
|
||||
while index < bytes.len() && bytes[index].is_ascii_whitespace() {
|
||||
index += 1;
|
||||
}
|
||||
|
||||
if index >= bytes.len() || bytes[index] != b'=' {
|
||||
while index < bytes.len() && !bytes[index].is_ascii_whitespace() && bytes[index] != b'/' {
|
||||
index += 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
index += 1;
|
||||
while index < bytes.len() && bytes[index].is_ascii_whitespace() {
|
||||
index += 1;
|
||||
}
|
||||
|
||||
if index >= bytes.len() {
|
||||
break;
|
||||
}
|
||||
|
||||
let mut value = String::new();
|
||||
|
||||
if bytes[index] == b'"' || bytes[index] == b'\'' {
|
||||
let quote = bytes[index];
|
||||
index += 1;
|
||||
let value_start = index;
|
||||
|
||||
while index < bytes.len() && bytes[index] != quote {
|
||||
index += 1;
|
||||
}
|
||||
|
||||
value.push_str(body[value_start..index.min(bytes.len())].trim());
|
||||
if index < bytes.len() {
|
||||
index += 1;
|
||||
}
|
||||
} else {
|
||||
let value_start = index;
|
||||
while index < bytes.len() && !bytes[index].is_ascii_whitespace() && bytes[index] != b'/' {
|
||||
index += 1;
|
||||
}
|
||||
value.push_str(body[value_start..index].trim());
|
||||
}
|
||||
|
||||
attributes.push((name.to_string(), value));
|
||||
}
|
||||
|
||||
attributes
|
||||
}
|
||||
|
||||
pub fn attribute_value<'a>(attributes: &'a [(String, String)], name: &str) -> Option<&'a str> {
|
||||
attributes.iter().find_map(|(key, value)| (key == name).then_some(value.as_str()))
|
||||
}
|
||||
|
||||
pub fn parse_fill_attribute(value: Option<&str>) -> Fill {
|
||||
let Some(value) = value.map(str::trim) else {
|
||||
return Fill::None;
|
||||
};
|
||||
|
||||
if value.eq_ignore_ascii_case("none") {
|
||||
return Fill::None;
|
||||
}
|
||||
|
||||
if let Some(hex) = value.strip_prefix('#') {
|
||||
if let Some(color) = Color::from_rgb_str(hex) {
|
||||
return Fill::Solid(color);
|
||||
}
|
||||
} else if let Some(color) = Color::from_rgb_str(value) {
|
||||
return Fill::Solid(color);
|
||||
}
|
||||
|
||||
Fill::None
|
||||
}
|
||||
|
||||
pub fn transform_from_function(name: &str, arguments: &[f64]) -> DAffine2 {
|
||||
match name {
|
||||
"translate" => match arguments.len() {
|
||||
0 => DAffine2::IDENTITY,
|
||||
1 => DAffine2::from_translation(DVec2::new(arguments[0], 0.)),
|
||||
_ => DAffine2::from_translation(DVec2::new(arguments[0], arguments[1])),
|
||||
},
|
||||
"scale" => match arguments.len() {
|
||||
0 => DAffine2::IDENTITY,
|
||||
1 => DAffine2::from_scale(DVec2::splat(arguments[0])),
|
||||
_ => DAffine2::from_scale(DVec2::new(arguments[0], arguments[1])),
|
||||
},
|
||||
"rotate" => {
|
||||
if arguments.is_empty() {
|
||||
return DAffine2::IDENTITY;
|
||||
}
|
||||
|
||||
let angle = arguments[0].to_radians();
|
||||
if arguments.len() >= 3 {
|
||||
let center = DVec2::new(arguments[1], arguments[2]);
|
||||
DAffine2::from_translation(center) * DAffine2::from_angle(angle) * DAffine2::from_translation(-center)
|
||||
} else {
|
||||
DAffine2::from_angle(angle)
|
||||
}
|
||||
}
|
||||
"matrix" if arguments.len() == 6 => DAffine2::from_cols_array(&[arguments[0], arguments[1], arguments[2], arguments[3], arguments[4], arguments[5]]),
|
||||
_ => DAffine2::IDENTITY,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_transform_attribute(value: Option<&str>) -> DAffine2 {
|
||||
let Some(value) = value.map(str::trim) else {
|
||||
return DAffine2::IDENTITY;
|
||||
};
|
||||
let mut transform = DAffine2::IDENTITY;
|
||||
let mut remaining = value;
|
||||
|
||||
while let Some(open_paren) = remaining.find('(') {
|
||||
let (name_part, after_name) = remaining.split_at(open_paren);
|
||||
let name = name_part.trim();
|
||||
let after_name = &after_name[1..];
|
||||
|
||||
if let Some(close_paren) = after_name.find(')') {
|
||||
let arguments = after_name[..close_paren]
|
||||
.split(|c| matches!(c, ',' | ' ' | '\t'))
|
||||
.filter_map(|token| {
|
||||
let trimmed = token.trim();
|
||||
if trimmed.is_empty() { None } else { trimmed.parse::<f64>().ok() }
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
transform = transform * transform_from_function(name, &arguments);
|
||||
remaining = &after_name[close_paren + 1..];
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
transform
|
||||
}
|
||||
|
||||
pub fn color_mode_from_u32(value: u32) -> ColorMode {
|
||||
match value {
|
||||
0 => ColorMode::Color,
|
||||
1 => ColorMode::Binary,
|
||||
_ => ColorMode::Color,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hierarchical_from_u32(value: u32) -> Hierarchical {
|
||||
match value {
|
||||
0 => Hierarchical::Stacked,
|
||||
1 => Hierarchical::Cutout,
|
||||
_ => Hierarchical::Stacked,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn simplify_mode_from_u32(value: u32) -> PathSimplifyMode {
|
||||
match value {
|
||||
0 => PathSimplifyMode::None,
|
||||
1 => PathSimplifyMode::Polygon,
|
||||
2 => PathSimplifyMode::Spline,
|
||||
_ => PathSimplifyMode::None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_vectorize_config(
|
||||
color_mode: u32,
|
||||
hierarchical: u32,
|
||||
path_simplify_mode: u32,
|
||||
filter_speckle: u32,
|
||||
color_precision: u32,
|
||||
layer_difference: u32,
|
||||
corner_threshold: f32,
|
||||
length_threshold: f64,
|
||||
max_iterations: u32,
|
||||
splice_threshold: f32,
|
||||
path_precision: u32,
|
||||
) -> Config {
|
||||
Config {
|
||||
color_mode: color_mode_from_u32(color_mode),
|
||||
hierarchical: hierarchical_from_u32(hierarchical),
|
||||
mode: simplify_mode_from_u32(path_simplify_mode),
|
||||
filter_speckle: filter_speckle as usize,
|
||||
color_precision: color_precision as i32,
|
||||
layer_difference: layer_difference as i32,
|
||||
corner_threshold: corner_threshold as i32,
|
||||
length_threshold,
|
||||
max_iterations: max_iterations as usize,
|
||||
splice_threshold: splice_threshold as i32,
|
||||
path_precision: Some(path_precision),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn vector_row_to_graphic_row(mut vector_row: TableRow<Vector>, source_row: &TableRowRef<'_, Raster<CPU>>) -> TableRow<Graphic> {
|
||||
vector_row.transform = *source_row.transform * vector_row.transform;
|
||||
vector_row.alpha_blending = source_row.alpha_blending.clone();
|
||||
vector_row.source_node_id = source_row.source_node_id.clone();
|
||||
|
||||
TableRow {
|
||||
element: Graphic::Vector(Table::new_from_row(vector_row)),
|
||||
transform: DAffine2::IDENTITY,
|
||||
alpha_blending: Default::default(),
|
||||
source_node_id: Default::default(),
|
||||
}
|
||||
}
|
||||
@@ -74,6 +74,8 @@ pub enum ArcType {
|
||||
PieSlice,
|
||||
}
|
||||
|
||||
// Add enum here (widget Dropdown vs Radio)
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)]
|
||||
#[widget(Radio)]
|
||||
|
||||
@@ -12,7 +12,9 @@ use crate::table::{Table, TableRow, TableRowMut};
|
||||
use crate::transform::{Footprint, ReferencePoint, Transform};
|
||||
use crate::vector::PointDomain;
|
||||
use crate::vector::algorithms::bezpath_algorithms::eval_pathseg_euclidean;
|
||||
use crate::vector::algorithms::decimation::ramer_douglas_peucker;
|
||||
use crate::vector::algorithms::merge_by_distance::MergeByDistanceExt;
|
||||
use crate::vector::algorithms::vectorize::{build_vectorize_config, parse_svg_paths_to_vector, vector_row_to_graphic_row};
|
||||
use crate::vector::misc::{MergeByDistanceAlgorithm, PointSpacingType, is_linear};
|
||||
use crate::vector::misc::{handles_to_segment, segment_to_handles};
|
||||
use crate::vector::style::{PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
|
||||
@@ -202,6 +204,177 @@ where
|
||||
content
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Raster: Filter"), path(graphene_core::vector))]
|
||||
async fn vectorize(
|
||||
_: impl Ctx,
|
||||
image: Table<Raster<CPU>>,
|
||||
color_mode: u32,
|
||||
hierarchical: u32,
|
||||
path_simplify_mode: u32,
|
||||
#[default(4)]
|
||||
#[hard_min(1.)]
|
||||
filter_speckle: u32,
|
||||
#[default(6)]
|
||||
#[hard_min(1.)]
|
||||
#[hard_max(8.)]
|
||||
color_precision: u32,
|
||||
#[default(16)]
|
||||
#[hard_min(0.)]
|
||||
#[hard_max(255.)]
|
||||
layer_difference: u32,
|
||||
#[default(60)]
|
||||
#[hard_min(0.)]
|
||||
#[hard_max(180.)]
|
||||
corner_threshold: f32,
|
||||
#[default(4.)]
|
||||
#[hard_min(3.5)]
|
||||
length_threshold: f64,
|
||||
#[default(10)]
|
||||
#[hard_min(1.)]
|
||||
max_iterations: u32,
|
||||
#[default(45)]
|
||||
#[hard_min(0.)]
|
||||
#[hard_max(180.)]
|
||||
splice_threshold: f32,
|
||||
#[default(8)]
|
||||
#[hard_min(0.)]
|
||||
path_precision: u32,
|
||||
) -> Table<Graphic> {
|
||||
let mut result_table = Table::new();
|
||||
|
||||
for row in image.iter() {
|
||||
let raster = &row.element;
|
||||
|
||||
if raster.width == 0 || raster.height == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let (pixel_data, width, height) = raster.to_flat_u8();
|
||||
let color_image = vtracer::ColorImage {
|
||||
pixels: pixel_data,
|
||||
width: width as usize,
|
||||
height: height as usize,
|
||||
};
|
||||
|
||||
let config = build_vectorize_config(
|
||||
color_mode,
|
||||
hierarchical,
|
||||
path_simplify_mode,
|
||||
filter_speckle,
|
||||
color_precision,
|
||||
layer_difference,
|
||||
corner_threshold,
|
||||
length_threshold,
|
||||
max_iterations,
|
||||
splice_threshold,
|
||||
path_precision,
|
||||
);
|
||||
|
||||
match vtracer::convert(color_image, config) {
|
||||
Ok(svg_file) => {
|
||||
let svg_content = svg_file.to_string();
|
||||
log::trace!("Generated SVG content:\n{}", svg_content);
|
||||
|
||||
let vector_rows = parse_svg_paths_to_vector(&svg_content);
|
||||
|
||||
let mut graphic_table = Table::new();
|
||||
for vector_row in vector_rows {
|
||||
graphic_table.push(vector_row_to_graphic_row(vector_row, &row));
|
||||
}
|
||||
|
||||
let graphic_group = Graphic::Graphic(graphic_table);
|
||||
let mut graphic_row = TableRow::new_from_element(graphic_group);
|
||||
graphic_row.transform = *row.transform;
|
||||
graphic_row.alpha_blending = *row.alpha_blending;
|
||||
graphic_row.source_node_id = *row.source_node_id;
|
||||
result_table.push(graphic_row);
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Vectorization failed: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result_table
|
||||
}
|
||||
|
||||
/// Simplifies vector paths using the Ramer-Douglas-Peucker algorithm for line decimation.
|
||||
/// Reduces the number of points in a path while preserving its overall shape within a specified tolerance.
|
||||
#[node_macro::node(category("Vector: Modifier"), path(graphene_core::vector))]
|
||||
fn decimate(
|
||||
_: impl Ctx,
|
||||
source: Table<Vector>,
|
||||
#[default(0.1)]
|
||||
#[hard_min(0.001)]
|
||||
tolerance: f64,
|
||||
) -> Table<Vector> {
|
||||
use crate::vector::misc::{dvec2_to_point, point_to_dvec2};
|
||||
use kurbo::Affine;
|
||||
|
||||
source
|
||||
.into_iter()
|
||||
.map(|row| {
|
||||
let transform = Affine::new(row.transform.to_cols_array());
|
||||
let vector = row.element;
|
||||
|
||||
let mut new_vector = Vector {
|
||||
style: vector.style.clone(),
|
||||
upstream_nested_layers: vector.upstream_nested_layers.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// Process each bezpath in the vector
|
||||
for mut bezpath in vector.stroke_bezpath_iter() {
|
||||
// Apply transform to work in world space for accurate distance calculations
|
||||
bezpath.apply_affine(transform);
|
||||
|
||||
// Extract anchor points from the bezpath
|
||||
let anchors: Vec<DVec2> = bezpath.segments().map(|seg| point_to_dvec2(seg.start())).collect();
|
||||
|
||||
// Handle the last point if the path isn't closed
|
||||
let is_closed = bezpath.elements().last() == Some(&kurbo::PathEl::ClosePath);
|
||||
let mut all_anchors = anchors;
|
||||
if !is_closed {
|
||||
if let Some(last_seg) = bezpath.segments().last() {
|
||||
all_anchors.push(point_to_dvec2(last_seg.end()));
|
||||
}
|
||||
}
|
||||
|
||||
if all_anchors.len() <= 2 {
|
||||
// Can't simplify paths with 2 or fewer points
|
||||
bezpath.apply_affine(transform.inverse());
|
||||
new_vector.append_bezpath(bezpath);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Apply RDP algorithm
|
||||
let simplified_indices = ramer_douglas_peucker(&all_anchors, tolerance);
|
||||
|
||||
// Build new bezpath from simplified points
|
||||
let mut simplified_bezpath = kurbo::BezPath::new();
|
||||
if let Some(&first_idx) = simplified_indices.first() {
|
||||
simplified_bezpath.move_to(dvec2_to_point(all_anchors[first_idx]));
|
||||
|
||||
for &idx in simplified_indices.iter().skip(1) {
|
||||
simplified_bezpath.line_to(dvec2_to_point(all_anchors[idx]));
|
||||
}
|
||||
|
||||
// Close the path if the original was closed and we have enough points
|
||||
if is_closed && simplified_indices.len() > 2 {
|
||||
simplified_bezpath.close_path();
|
||||
}
|
||||
}
|
||||
|
||||
// Transform back to local space
|
||||
simplified_bezpath.apply_affine(transform.inverse());
|
||||
new_vector.append_bezpath(simplified_bezpath);
|
||||
}
|
||||
|
||||
TableRow { element: new_vector, ..row }
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Instancing"), path(graphene_core::vector))]
|
||||
async fn repeat<I: 'n + Send + Clone>(
|
||||
_: impl Ctx,
|
||||
|
||||
@@ -240,6 +240,7 @@ tagged_value! {
|
||||
SelectiveColorChoice(graphene_raster_nodes::adjustments::SelectiveColorChoice),
|
||||
GridType(graphene_core::vector::misc::GridType),
|
||||
ArcType(graphene_core::vector::misc::ArcType),
|
||||
// Add here
|
||||
MergeByDistanceAlgorithm(graphene_core::vector::misc::MergeByDistanceAlgorithm),
|
||||
PointSpacingType(graphene_core::vector::misc::PointSpacingType),
|
||||
SpiralType(graphene_core::vector::misc::SpiralType),
|
||||
|
||||
@@ -132,6 +132,7 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::raster::adjustments::SelectiveColorChoice]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::vector::misc::GridType]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::vector::misc::ArcType]),
|
||||
// Add here
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::vector::misc::MergeByDistanceAlgorithm]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::vector::misc::PointSpacingType]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::vector::style::FillType]),
|
||||
@@ -217,6 +218,7 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
|
||||
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::raster::SelectiveColorChoice]),
|
||||
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_core::vector::misc::GridType]),
|
||||
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_core::vector::misc::ArcType]),
|
||||
// add here
|
||||
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_core::vector::misc::MergeByDistanceAlgorithm]),
|
||||
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_core::vector::misc::PointSpacingType]),
|
||||
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_core::vector::style::StrokeCap]),
|
||||
|
||||
Reference in New Issue
Block a user