Skip to content

Add new node "Decimate" #2823

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1993,6 +1993,103 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
description: Cow::Borrowed("Convert vector geometry into a polyline composed of evenly spaced points."),
properties: Some("sample_polyline_properties"),
},
DocumentNodeDefinition {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't usually need to manually define the document node definition as it can be automagically generated.

identifier: "Decimate",
category: "Vector: Modifier",
node_template: NodeTemplate {
document_node: DocumentNode {
implementation: DocumentNodeImplementation::Network(NodeNetwork {
exports: vec![NodeInput::node(NodeId(2), 0)],
nodes: [
DocumentNode {
inputs: vec![NodeInput::network(concrete!(graphene_std::vector::VectorDataTable), 0)],
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::memo::MonitorNode")),
manual_composition: Some(generic!(T)),
skip_deduplication: true,
..Default::default()
},
Comment on lines +2004 to +2010
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is there a monitor node?

DocumentNode {
inputs: vec![NodeInput::node(NodeId(0), 0), NodeInput::network(concrete!(f64), 1)],
manual_composition: Some(generic!(T)),
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::vector::DecimateNode")),
..Default::default()
},
DocumentNode {
inputs: vec![NodeInput::node(NodeId(1), 0)],
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::memo::MemoNode")),
manual_composition: Some(generic!(T)),
..Default::default()
},
Comment on lines +2017 to +2022
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is there a memo node?

]
.into_iter()
.enumerate()
.map(|(id, node)| (NodeId(id as u64), node))
.collect(),
..Default::default()
}),
inputs: vec![
NodeInput::value(TaggedValue::VectorData(graphene_std::vector::VectorDataTable::default()), true),
NodeInput::value(TaggedValue::F64(1.0), false), // Tolerance
],
..Default::default()
},
persistent_node_metadata: DocumentNodePersistentMetadata {
network_metadata: Some(NodeNetworkMetadata {
persistent_metadata: NodeNetworkPersistentMetadata {
node_metadata: [
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
display_name: "Monitor".to_string(),
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, 0)),
..Default::default()
},
..Default::default()
},
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
display_name: "Decimate".to_string(),
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(7, 0)),
..Default::default()
},
..Default::default()
},
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
display_name: "Memoize".to_string(),
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(14, 0)),
..Default::default()
},
..Default::default()
},
]
.into_iter()
.enumerate()
.map(|(id, node)| (NodeId(id as u64), node))
.collect(),
..Default::default()
},
..Default::default()
}),
input_properties: vec![
("Vector Data", "The polyline to be simplified.").into(),
PropertiesRow::with_override(
"Tolerance",
"Maximum allowed deviation from the original polyline.",
WidgetOverride::Number(NumberInputSettings {
min: Some(0.0),
step: Some(0.1),
unit: Some(" px".to_string()),
..Default::default()
}),
),
],
output_names: vec!["Vector".to_string()],
..Default::default()
},
},
description: Cow::Borrowed("Simplifies a polyline using the Ramer–Douglas–Peucker algorithm."),
properties: None,
},
DocumentNodeDefinition {
identifier: "Scatter Points",
category: "Vector: Modifier",
Expand Down
91 changes: 91 additions & 0 deletions node-graph/gcore/src/vector/vector_nodes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1138,6 +1138,97 @@ where
output_table
}

/// Simplifies a polyline using the Ramer–Douglas–Peucker algorithm.
#[node_macro::node(category("Vector: Modifier"), name("Decimate"), path(graphene_core::vector))]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't need to specify the name here.

async fn decimate(
_: impl Ctx,
vector_data: VectorDataTable,
#[default(1.0)]
#[hard_min(0.0)]
tolerance: f64,
) -> VectorDataTable {
let mut result_table = VectorDataTable::default();

for vector_data_instance in vector_data.instance_iter() {
let mut result = VectorData {
style: vector_data_instance.instance.style.clone(),
..Default::default()
};

for subpath in vector_data_instance.instance.stroke_bezier_paths() {
let points: Vec<DVec2> = subpath.manipulator_groups().iter().map(|g| g.anchor).collect();
let closed = subpath.closed();

let simplified = if points.len() > 2 { douglas_peucker(&points, tolerance, closed) } else { points };

// Rebuild the subpath from the simplified points
let mut new_groups = Vec::new();
let mut id_gen = PointId::generate();
for pt in simplified {
new_groups.push(bezier_rs::ManipulatorGroup {
anchor: pt,
in_handle: None,
out_handle: None,
id: id_gen,
});
id_gen = id_gen.next_id();
}
let new_subpath = Subpath::new(new_groups, closed);
result.append_subpath(new_subpath, closed);
}

result_table.push(Instance {
instance: result,
transform: vector_data_instance.transform,
alpha_blending: vector_data_instance.alpha_blending,
source_node_id: vector_data_instance.source_node_id,
});
}

result_table
}

fn douglas_peucker(points: &[DVec2], epsilon: f64, closed: bool) -> Vec<DVec2> {
if points.len() < 2 {
return points.to_vec();
}

let (_first, _lastt) = (0, points.len() - 1);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lastt -> last

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please don't prefix local variables with an underscore. That usually signifies that they are unused.


// For closed paths, treat as open for simplification, then close at the end
let (_first, _last, _is_closed) = if closed && points.len() > 2 { (0, points.len() - 1, true) } else { (0, points.len() - 1, false) };
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line could be simplified to

let is_closed = closed && points.len() > 2;

Since the other variables don't change and were already defined.


let mut dmax = 0.0;
let mut index = 0;

for i in (_first + 1).._last {
let d = perpendicular_distance(points[i], points[_first], points[_last]);
if d > dmax {
index = i;
dmax = d;
}
}

if dmax > epsilon {
let mut rec_results1 = douglas_peucker(&points[_first..=index], epsilon, false);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since first is always zero, this could just be points[..=index]

let mut rec_results2 = douglas_peucker(&points[index..=_last], epsilon, false);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since last is always the last index, this could just be points[index..]


// Remove the last point of the first list to avoid duplication
rec_results1.pop();
rec_results1.append(&mut rec_results2);
rec_results1
} else {
vec![points[_first], points[_last]]
}
}

/// Calculates the perpendicular distance from a point to a line defined by two points.
fn perpendicular_distance(point: DVec2, line_start: DVec2, line_end: DVec2) -> f64 {
let num = ((line_end.y - line_start.y) * point.x - (line_end.x - line_start.x) * point.y + line_end.x * line_start.y - line_end.y * line_start.x).abs();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can be simplified with project onto.

let den = (line_end - line_start).length();
if den.abs() < 1e-10 { (point - line_start).length() } else { num / den }
}

/// Convert vector geometry into a polyline composed of evenly spaced points.
#[node_macro::node(category(""), path(graphene_core::vector))]
async fn sample_polyline(
Expand Down
Loading