Add the "Along Normals" parameter to the 'Jitter Points' node (#3983)
* Add the "Along Normals" parameter to the 'Jitter Points' node * Fix the edge case of a self-loops
This commit is contained in:
parent
d41883a942
commit
211b9113a1
|
|
@ -1860,6 +1860,19 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
|
|||
.set_input(&InputConnector::node(*node_id, 1), NodeInput::value(TaggedValue::ScaleType(ScaleType::Magnitude), false), network_path);
|
||||
}
|
||||
|
||||
// Add the "Along Normals" parameter to the "Jitter Points" node
|
||||
if reference == DefinitionIdentifier::ProtoNode(graphene_std::vector::jitter_points::IDENTIFIER) && inputs_count == 3 {
|
||||
let mut node_template = resolve_document_node_type(&reference)?.default_node_template();
|
||||
let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template)?;
|
||||
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 2), old_inputs[2].clone(), network_path);
|
||||
document
|
||||
.network_interface
|
||||
.set_input(&InputConnector::node(*node_id, 3), NodeInput::value(TaggedValue::Bool(false), false), network_path);
|
||||
}
|
||||
|
||||
// ==================================
|
||||
// PUT ALL MIGRATIONS ABOVE THIS LINE
|
||||
// ==================================
|
||||
|
|
|
|||
|
|
@ -436,6 +436,93 @@ impl SegmentDomain {
|
|||
self.all_connected(point).next().is_some()
|
||||
}
|
||||
|
||||
/// Computes the direction-of-travel tangent at one endpoint of a segment.
|
||||
/// Uses the "first distinct control point" pattern: iterates through the Bezier control points
|
||||
/// from the anchor outward, returning the direction to the first one that differs in position.
|
||||
/// This handles zero-length handles by finding the tangent direction in the limit.
|
||||
/// Returns `DVec2::ZERO` if all control points coincide (fully degenerate segment).
|
||||
fn segment_tangent_at_endpoint(&self, segment_index: usize, positions: &[DVec2], at_start: bool) -> DVec2 {
|
||||
let anchor_start = positions[self.start_point[segment_index]];
|
||||
let anchor_end = positions[self.end_point[segment_index]];
|
||||
|
||||
// Build ordered control points for this segment
|
||||
let (points, count) = match self.handles[segment_index] {
|
||||
BezierHandles::Linear => ([anchor_start, anchor_end, DVec2::ZERO, DVec2::ZERO], 2),
|
||||
BezierHandles::Quadratic { handle } => ([anchor_start, handle, anchor_end, DVec2::ZERO], 3),
|
||||
BezierHandles::Cubic { handle_start, handle_end } => ([anchor_start, handle_start, handle_end, anchor_end], 4),
|
||||
};
|
||||
|
||||
let not_near = |a: DVec2, b: DVec2| a.distance_squared(b) > f64::EPSILON * 1e3;
|
||||
|
||||
if at_start {
|
||||
let anchor = points[0];
|
||||
points[1..count].iter().find(|&&p| not_near(p, anchor)).map_or(DVec2::ZERO, |&point| point - anchor)
|
||||
} else {
|
||||
let anchor = points[count - 1];
|
||||
points[..count - 1].iter().rev().find(|&&p| not_near(p, anchor)).map_or(DVec2::ZERO, |&point| anchor - point)
|
||||
}
|
||||
}
|
||||
|
||||
/// Computes the average tangent direction at a point based on its 1 or 2 connected segments.
|
||||
/// Returns `None` for points with 0 or 3+ connections (ambiguous or undefined tangent),
|
||||
/// or if the tangent is degenerate (all control points coincide).
|
||||
pub fn point_tangent(&self, point_index: usize, positions: &[DVec2]) -> Option<DVec2> {
|
||||
// Collect connected segments with their relationship to this point (at_start flag)
|
||||
let mut connections: [(usize, bool); 2] = [(0, false); 2];
|
||||
let mut connection_count = 0;
|
||||
|
||||
for (segment_index, (&start, &end)) in self.start_point.iter().zip(&self.end_point).enumerate() {
|
||||
// Self-loop segments count as two connections (outgoing and incoming)
|
||||
let is_start = start == point_index;
|
||||
let is_end = end == point_index;
|
||||
|
||||
if !is_start && !is_end {
|
||||
continue;
|
||||
}
|
||||
|
||||
if is_start {
|
||||
if connection_count >= 2 {
|
||||
return None;
|
||||
}
|
||||
connections[connection_count] = (segment_index, true);
|
||||
connection_count += 1;
|
||||
}
|
||||
if is_end {
|
||||
if connection_count >= 2 {
|
||||
return None;
|
||||
}
|
||||
connections[connection_count] = (segment_index, false);
|
||||
connection_count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if connection_count == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Compute the direction-of-travel tangent for the first connection
|
||||
let (segment_index, at_start) = connections[0];
|
||||
let tangent1 = self.segment_tangent_at_endpoint(segment_index, positions, at_start).try_normalize();
|
||||
|
||||
if connection_count == 1 {
|
||||
return tangent1;
|
||||
}
|
||||
|
||||
// Compute the direction-of-travel tangent for the second connection
|
||||
let (segment_index, at_start) = connections[1];
|
||||
let tangent2 = self.segment_tangent_at_endpoint(segment_index, positions, at_start).try_normalize();
|
||||
|
||||
// Average the two normalized tangents
|
||||
let average = tangent1? + tangent2?;
|
||||
|
||||
// If the tangents are nearly opposite (straight-through), use t1 directly
|
||||
if average.length_squared() < (f64::EPSILON * 1e3).powi(2) {
|
||||
return tangent1;
|
||||
}
|
||||
|
||||
average.try_normalize()
|
||||
}
|
||||
|
||||
/// Iterates over segments in the domain.
|
||||
///
|
||||
/// Tuple is: (id, start point, end point, handles)
|
||||
|
|
|
|||
|
|
@ -1879,14 +1879,21 @@ async fn spline(_: impl Ctx, content: Table<Vector>) -> Table<Vector> {
|
|||
.collect()
|
||||
}
|
||||
|
||||
/// Perturbs the positions of anchor points in vector geometry by random amounts and directions.
|
||||
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
|
||||
async fn jitter_points(
|
||||
_: impl Ctx,
|
||||
/// The vector geometry with points to be jittered.
|
||||
content: Table<Vector>,
|
||||
#[unit(" px")]
|
||||
/// The maximum extent of the random distance each point can be offset.
|
||||
#[default(5.)]
|
||||
#[unit(" px")]
|
||||
amount: f64,
|
||||
/// Seed used to determine unique variations on all randomized offsets.
|
||||
seed: SeedValue,
|
||||
/// Whether to offset anchor points along their normal direction (perpendicular to the path) or in a random direction. Free-floating and branching points have no normal direction, so they receive a random-angled offset regardless of this setting.
|
||||
#[default(true)]
|
||||
along_normals: bool,
|
||||
) -> Table<Vector> {
|
||||
content
|
||||
.into_iter()
|
||||
|
|
@ -1897,10 +1904,20 @@ async fn jitter_points(
|
|||
let inverse_transform = if transform.matrix2.determinant() != 0. { transform.inverse() } else { Default::default() };
|
||||
|
||||
let deltas = (0..row.element.point_domain.positions().len())
|
||||
.map(|_| {
|
||||
let angle = rng.random::<f64>() * TAU;
|
||||
.map(|point_index| {
|
||||
let normal = if along_normals {
|
||||
row.element.segment_domain.point_tangent(point_index, row.element.point_domain.positions()).map(|t| t.perp())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
inverse_transform.transform_vector2(DVec2::from_angle(angle) * rng.random::<f64>() * amount)
|
||||
let offset = if let Some(normal) = normal {
|
||||
(rng.random::<f64>() * 2. - 1.) * normal
|
||||
} else {
|
||||
rng.random::<f64>() * DVec2::from_angle(rng.random::<f64>() * TAU)
|
||||
};
|
||||
|
||||
inverse_transform.transform_vector2(offset * amount)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let mut already_applied = vec![false; row.element.point_domain.positions().len()];
|
||||
|
|
|
|||
Loading…
Reference in New Issue