Skip to content
Open
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 @@ -155,7 +155,7 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
// TODO: Auto-generate this from its proto node macro
DocumentNodeDefinition {
identifier: "Monitor",
category: "Debug",
category: "",
node_template: NodeTemplate {
document_node: DocumentNode {
implementation: DocumentNodeImplementation::ProtoNode(memo::monitor::IDENTIFIER),
Expand Down Expand Up @@ -1530,7 +1530,7 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
},
DocumentNodeDefinition {
identifier: "Extract",
category: "Debug",
category: "",
node_template: NodeTemplate {
document_node: DocumentNode {
implementation: DocumentNodeImplementation::Extract,
Expand Down
3 changes: 3 additions & 0 deletions editor/src/messages/tool/common_functionality/shape_editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1822,6 +1822,9 @@ impl ShapeState {
/// Find the `t` value along the path segment we have clicked upon, together with that segment ID.
fn closest_segment(&self, network_interface: &NodeNetworkInterface, layer: LayerNodeIdentifier, position: glam::DVec2, tolerance: f64) -> Option<ClosestSegment> {
let transform = network_interface.document_metadata().transform_to_viewport_if_feeds(layer, network_interface);
if transform.matrix2.determinant() == 0. {
return None;
}
let layer_pos = transform.inverse().transform_point2(position);

let tolerance = tolerance + 0.5;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,17 +85,18 @@ pub fn tangent_on_bezpath(bezpath: &BezPath, t_value: TValue, segments_length: O
}
}

pub fn sample_polyline_on_bezpath(
bezpath: BezPath,
/// Computes sample locations along a bezpath, returning parametric `(segment_index, t)` pairs and whether the path was closed.
/// The `bezpath` is used for euclidean-to-parametric conversion, and `segments_length` provides pre-calculated world-space segment lengths.
/// Callers can evaluate these locations on any bezpath with the same topology (e.g., an untransformed version).
pub fn compute_sample_locations(
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();

) -> Option<(Vec<(usize, f64)>, bool)> {
let was_closed = matches!(bezpath.elements().last(), Some(PathEl::ClosePath));

// Calculate the total length of the collected segments.
Expand Down Expand Up @@ -142,7 +143,8 @@ pub fn sample_polyline_on_bezpath(
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.
// Generate sample locations along the path based on calculated intervals.
let mut locations = Vec::with_capacity(max_i);
let mut length_up_to_previous_segment = 0.;
let mut next_segment_index = 0;

Expand All @@ -167,20 +169,11 @@ pub fn sample_polyline_on_bezpath(

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();
locations.push((next_segment_index, t));
}

Some(sample_bezpath)
Some((locations, was_closed))
}

#[derive(Debug, Clone, Copy)]
Expand Down
62 changes: 48 additions & 14 deletions node-graph/nodes/vector/src/vector_nodes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use core_types::registry::types::{Angle, Length, Multiplier, Percentage, PixelLe
use core_types::table::{Table, TableRow, TableRowMut};
use core_types::transform::Footprint;
use core_types::{CloneVarArgs, Color, Context, Ctx, ExtractAll, OwnedContextImpl};
use glam::{DAffine2, DVec2};
use glam::{DAffine2, DMat2, DVec2};
use graphic_types::Vector;
use graphic_types::raster_types::{CPU, GPU, Raster};
use graphic_types::{Graphic, IntoGraphicTable};
Expand All @@ -16,7 +16,7 @@ use rand::{Rng, SeedableRng};
use std::collections::hash_map::DefaultHasher;
use vector_types::subpath::{BezierHandles, ManipulatorGroup};
use vector_types::vector::PointDomain;
use vector_types::vector::algorithms::bezpath_algorithms::{self, TValue, eval_pathseg_euclidean, evaluate_bezpath, sample_polyline_on_bezpath, split_bezpath, tangent_on_bezpath};
use vector_types::vector::algorithms::bezpath_algorithms::{self, TValue, eval_pathseg_euclidean, evaluate_bezpath, split_bezpath, tangent_on_bezpath};
use vector_types::vector::algorithms::merge_by_distance::MergeByDistanceExt;
use vector_types::vector::algorithms::offset_subpath::offset_bezpath;
use vector_types::vector::algorithms::spline::{solve_spline_first_handle_closed, solve_spline_first_handle_open};
Expand Down Expand Up @@ -1355,11 +1355,12 @@ async fn sample_polyline(
// Keeps track of the index of the first segment of the next bezpath in order to get lengths of all segments.
let mut next_segment_index = 0;

for mut bezpath in bezpaths {
// Apply the tranformation to the current bezpath to calculate points after transformation.
bezpath.apply_affine(Affine::new(row.transform.to_cols_array()));
for local_bezpath in bezpaths {
// Apply the transform to compute sample locations in world space (for correct distance-based spacing)
let mut world_bezpath = local_bezpath.clone();
world_bezpath.apply_affine(Affine::new(row.transform.to_cols_array()));

let segment_count = bezpath.segments().count();
let segment_count = world_bezpath.segments().count();

// For the current bezpath we get its segment's length by calculating the start index and end index.
let current_bezpath_segments_length = &subpath_segment_lengths[next_segment_index..next_segment_index + segment_count];
Expand All @@ -1371,14 +1372,30 @@ async fn sample_polyline(
PointSpacingType::Separation => separation,
PointSpacingType::Quantity => quantity as f64,
};
let Some(mut sample_bezpath) = sample_polyline_on_bezpath(bezpath, spacing, amount, start_offset, stop_offset, adaptive_spacing, current_bezpath_segments_length) else {

// Compute sample locations using world-space distances, then evaluate positions on the untransformed bezpath.
// This avoids needing to invert the transform (which fails when the transform is singular, e.g. zero scale).
let Some((locations, was_closed)) =
bezpath_algorithms::compute_sample_locations(&world_bezpath, spacing, amount, start_offset, stop_offset, adaptive_spacing, current_bezpath_segments_length)
else {
continue;
};

// Reverse the transformation applied to the bezpath as the `result` already has the transformation set.
sample_bezpath.apply_affine(Affine::new(row.transform.to_cols_array()).inverse());
// Evaluate the sample locations on the untransformed bezpath and append the result
let mut sample_bezpath = BezPath::new();
for &(segment_index, t) in &locations {
let segment = local_bezpath.get_seg(segment_index + 1).unwrap();
let point = segment.eval(t);

// Append the bezpath (subpath) that connects generated points by lines.
if sample_bezpath.elements().is_empty() {
sample_bezpath.move_to(point);
} else {
sample_bezpath.line_to(point);
}
}
if was_closed {
sample_bezpath.close_path();
}
result.append_bezpath(sample_bezpath);
}

Expand Down Expand Up @@ -1900,8 +1917,26 @@ async fn jitter_points(
.map(|mut row| {
let mut rng = rand::rngs::StdRng::seed_from_u64(seed.into());

let transform = row.transform;
let inverse_transform = if transform.matrix2.determinant() != 0. { transform.inverse() } else { Default::default() };
// Map world-space jitter offsets back to local space, compensating for the transform's scaling.
// When the transform is singular (e.g. zero scale on one axis), the collapsed axis is replaced
// with a unit perpendicular so jitter still applies there (visible if the transform is later replaced).
let linear = row.transform.matrix2;
let inverse_linear = if linear.determinant() != 0. {
linear.inverse()
} else {
let col0 = linear.col(0);
let col1 = linear.col(1);
let col0_exists = col0.length_squared() > (f64::EPSILON * 1e3).powi(2);
let col1_exists = col1.length_squared() > (f64::EPSILON * 1e3).powi(2);

let repaired = match (col0_exists, col1_exists) {
// Replace the collapsed axis (like scale.x=2, skew.y=2) with a unit perpendicular of the surviving one
(true, _) => DMat2::from_cols(col0, col0.perp().normalize()),
(false, true) => DMat2::from_cols(col1.perp().normalize(), col1),
(false, false) => DMat2::IDENTITY,
};
repaired.inverse()
};

let deltas = (0..row.element.point_domain.positions().len())
.map(|point_index| {
Expand All @@ -1917,7 +1952,7 @@ async fn jitter_points(
rng.random::<f64>() * DVec2::from_angle(rng.random::<f64>() * TAU)
};

inverse_transform.transform_vector2(offset * amount)
inverse_linear * (offset * amount)
})
.collect::<Vec<_>>();
let mut already_applied = vec![false; row.element.point_domain.positions().len()];
Expand Down Expand Up @@ -1949,7 +1984,6 @@ async fn jitter_points(
}
}

row.element.style.set_stroke_transform(DAffine2::IDENTITY);
row
})
.collect()
Expand Down
Loading