Fix 'Jitter Points' and 'Sample Polylines' working incorrectly with X or Y scale of 0 content (#3984)
* Fix NaN points produced by Sample Polylines on 0-scaled input * Fix Jitter Points inverse transform for zero-scale axes and stop resetting stroke transform * Remove a couple confusing Debug nodes * Fix edge case * Update demo art * Fix order change in Jitter Points causing different results from earlier * Fix bug in bisect tool * Break out functionality into helper functions
This commit is contained in:
parent
79bf1ab688
commit
1543d974ac
File diff suppressed because one or more lines are too long
|
|
@ -155,7 +155,7 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
|
||||||
// TODO: Auto-generate this from its proto node macro
|
// TODO: Auto-generate this from its proto node macro
|
||||||
DocumentNodeDefinition {
|
DocumentNodeDefinition {
|
||||||
identifier: "Monitor",
|
identifier: "Monitor",
|
||||||
category: "Debug",
|
category: "",
|
||||||
node_template: NodeTemplate {
|
node_template: NodeTemplate {
|
||||||
document_node: DocumentNode {
|
document_node: DocumentNode {
|
||||||
implementation: DocumentNodeImplementation::ProtoNode(memo::monitor::IDENTIFIER),
|
implementation: DocumentNodeImplementation::ProtoNode(memo::monitor::IDENTIFIER),
|
||||||
|
|
@ -1530,7 +1530,7 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
|
||||||
},
|
},
|
||||||
DocumentNodeDefinition {
|
DocumentNodeDefinition {
|
||||||
identifier: "Extract",
|
identifier: "Extract",
|
||||||
category: "Debug",
|
category: "",
|
||||||
node_template: NodeTemplate {
|
node_template: NodeTemplate {
|
||||||
document_node: DocumentNode {
|
document_node: DocumentNode {
|
||||||
implementation: DocumentNodeImplementation::Extract,
|
implementation: DocumentNodeImplementation::Extract,
|
||||||
|
|
|
||||||
|
|
@ -1822,6 +1822,9 @@ impl ShapeState {
|
||||||
/// Find the `t` value along the path segment we have clicked upon, together with that segment ID.
|
/// 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> {
|
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);
|
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 layer_pos = transform.inverse().transform_point2(position);
|
||||||
|
|
||||||
let tolerance = tolerance + 0.5;
|
let tolerance = tolerance + 0.5;
|
||||||
|
|
|
||||||
|
|
@ -85,17 +85,18 @@ pub fn tangent_on_bezpath(bezpath: &BezPath, t_value: TValue, segments_length: O
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn sample_polyline_on_bezpath(
|
/// Computes sample locations along a bezpath, returning parametric `(segment_index, t)` pairs and whether the path was closed.
|
||||||
bezpath: BezPath,
|
/// 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,
|
point_spacing_type: PointSpacingType,
|
||||||
amount: f64,
|
amount: f64,
|
||||||
start_offset: f64,
|
start_offset: f64,
|
||||||
stop_offset: f64,
|
stop_offset: f64,
|
||||||
adaptive_spacing: bool,
|
adaptive_spacing: bool,
|
||||||
segments_length: &[f64],
|
segments_length: &[f64],
|
||||||
) -> Option<BezPath> {
|
) -> Option<(Vec<(usize, f64)>, bool)> {
|
||||||
let mut sample_bezpath = BezPath::new();
|
|
||||||
|
|
||||||
let was_closed = matches!(bezpath.elements().last(), Some(PathEl::ClosePath));
|
let was_closed = matches!(bezpath.elements().last(), Some(PathEl::ClosePath));
|
||||||
|
|
||||||
// Calculate the total length of the collected segments.
|
// Calculate the total length of the collected segments.
|
||||||
|
|
@ -142,7 +143,8 @@ pub fn sample_polyline_on_bezpath(
|
||||||
let sample_count_usize = sample_count as usize;
|
let sample_count_usize = sample_count as usize;
|
||||||
let max_i = if was_closed { sample_count_usize } else { sample_count_usize + 1 };
|
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 length_up_to_previous_segment = 0.;
|
||||||
let mut next_segment_index = 0;
|
let mut next_segment_index = 0;
|
||||||
|
|
||||||
|
|
@ -167,20 +169,11 @@ pub fn sample_polyline_on_bezpath(
|
||||||
|
|
||||||
let segment = bezpath.get_seg(next_segment_index + 1).unwrap();
|
let segment = bezpath.get_seg(next_segment_index + 1).unwrap();
|
||||||
let t = eval_pathseg_euclidean(segment, t, DEFAULT_ACCURACY);
|
let t = eval_pathseg_euclidean(segment, t, DEFAULT_ACCURACY);
|
||||||
let point = segment.eval(t);
|
|
||||||
|
|
||||||
if sample_bezpath.elements().is_empty() {
|
locations.push((next_segment_index, t));
|
||||||
sample_bezpath.move_to(point)
|
|
||||||
} else {
|
|
||||||
sample_bezpath.line_to(point)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if was_closed {
|
Some((locations, was_closed))
|
||||||
sample_bezpath.close_path();
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(sample_bezpath)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ use core_types::registry::types::{Angle, Length, Multiplier, Percentage, PixelLe
|
||||||
use core_types::table::{Table, TableRow, TableRowMut};
|
use core_types::table::{Table, TableRow, TableRowMut};
|
||||||
use core_types::transform::Footprint;
|
use core_types::transform::Footprint;
|
||||||
use core_types::{CloneVarArgs, Color, Context, Ctx, ExtractAll, OwnedContextImpl};
|
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::Vector;
|
||||||
use graphic_types::raster_types::{CPU, GPU, Raster};
|
use graphic_types::raster_types::{CPU, GPU, Raster};
|
||||||
use graphic_types::{Graphic, IntoGraphicTable};
|
use graphic_types::{Graphic, IntoGraphicTable};
|
||||||
|
|
@ -16,7 +16,7 @@ use rand::{Rng, SeedableRng};
|
||||||
use std::collections::hash_map::DefaultHasher;
|
use std::collections::hash_map::DefaultHasher;
|
||||||
use vector_types::subpath::{BezierHandles, ManipulatorGroup};
|
use vector_types::subpath::{BezierHandles, ManipulatorGroup};
|
||||||
use vector_types::vector::PointDomain;
|
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::merge_by_distance::MergeByDistanceExt;
|
||||||
use vector_types::vector::algorithms::offset_subpath::offset_bezpath;
|
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};
|
use vector_types::vector::algorithms::spline::{solve_spline_first_handle_closed, solve_spline_first_handle_open};
|
||||||
|
|
@ -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.
|
// 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;
|
let mut next_segment_index = 0;
|
||||||
|
|
||||||
for mut bezpath in bezpaths {
|
for local_bezpath in bezpaths {
|
||||||
// Apply the tranformation to the current bezpath to calculate points after transformation.
|
// Apply the transform to compute sample locations in world space (for correct distance-based spacing)
|
||||||
bezpath.apply_affine(Affine::new(row.transform.to_cols_array()));
|
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.
|
// 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];
|
let current_bezpath_segments_length = &subpath_segment_lengths[next_segment_index..next_segment_index + segment_count];
|
||||||
|
|
@ -1371,14 +1372,30 @@ async fn sample_polyline(
|
||||||
PointSpacingType::Separation => separation,
|
PointSpacingType::Separation => separation,
|
||||||
PointSpacingType::Quantity => quantity as f64,
|
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;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Reverse the transformation applied to the bezpath as the `result` already has the transformation set.
|
// Evaluate the sample locations on the untransformed bezpath and append the result
|
||||||
sample_bezpath.apply_affine(Affine::new(row.transform.to_cols_array()).inverse());
|
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);
|
result.append_bezpath(sample_bezpath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1879,6 +1896,59 @@ async fn spline(_: impl Ctx, content: Table<Vector>) -> Table<Vector> {
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Computes the inverse of a transform's linear (matrix2) part, handling singular transforms
|
||||||
|
/// (e.g. zero scale on one axis) by replacing the collapsed axis with a unit perpendicular
|
||||||
|
/// so offsets still apply there (visible if the transform is later replaced).
|
||||||
|
fn inverse_linear_or_repair(linear: DMat2) -> DMat2 {
|
||||||
|
if linear.determinant() != 0. {
|
||||||
|
return linear.inverse();
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
(true, _) => DMat2::from_cols(col0, col0.perp().normalize()),
|
||||||
|
(false, true) => DMat2::from_cols(col1.perp().normalize(), col1),
|
||||||
|
(false, false) => DMat2::IDENTITY,
|
||||||
|
};
|
||||||
|
repaired.inverse()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Applies per-point displacement deltas to the point and handle positions of a vector element.
|
||||||
|
fn apply_point_deltas(element: &mut Vector, deltas: &[DVec2], transform: DAffine2) {
|
||||||
|
let mut already_applied = vec![false; element.point_domain.positions().len()];
|
||||||
|
|
||||||
|
for (handles, start, end) in element.segment_domain.handles_and_points_mut() {
|
||||||
|
let start_delta = deltas[*start];
|
||||||
|
let end_delta = deltas[*end];
|
||||||
|
|
||||||
|
if !already_applied[*start] {
|
||||||
|
let start_position = element.point_domain.positions()[*start];
|
||||||
|
element.point_domain.set_position(*start, start_position + start_delta);
|
||||||
|
already_applied[*start] = true;
|
||||||
|
}
|
||||||
|
if !already_applied[*end] {
|
||||||
|
let end_position = element.point_domain.positions()[*end];
|
||||||
|
element.point_domain.set_position(*end, end_position + end_delta);
|
||||||
|
already_applied[*end] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
match handles {
|
||||||
|
BezierHandles::Cubic { handle_start, handle_end } => {
|
||||||
|
*handle_start += start_delta;
|
||||||
|
*handle_end += end_delta;
|
||||||
|
}
|
||||||
|
BezierHandles::Quadratic { handle } => {
|
||||||
|
*handle = transform.transform_point2(*handle) + (start_delta + end_delta) / 2.;
|
||||||
|
}
|
||||||
|
BezierHandles::Linear => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Perturbs the positions of anchor points in vector geometry by random amounts and directions.
|
/// Perturbs the positions of anchor points in vector geometry by random amounts and directions.
|
||||||
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
|
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
|
||||||
async fn jitter_points(
|
async fn jitter_points(
|
||||||
|
|
@ -1899,11 +1969,9 @@ async fn jitter_points(
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|mut row| {
|
.map(|mut row| {
|
||||||
let mut rng = rand::rngs::StdRng::seed_from_u64(seed.into());
|
let mut rng = rand::rngs::StdRng::seed_from_u64(seed.into());
|
||||||
|
let inverse_linear = inverse_linear_or_repair(row.transform.matrix2);
|
||||||
|
|
||||||
let transform = row.transform;
|
let deltas: Vec<_> = (0..row.element.point_domain.positions().len())
|
||||||
let inverse_transform = if transform.matrix2.determinant() != 0. { transform.inverse() } else { Default::default() };
|
|
||||||
|
|
||||||
let deltas = (0..row.element.point_domain.positions().len())
|
|
||||||
.map(|point_index| {
|
.map(|point_index| {
|
||||||
let normal = if along_normals {
|
let normal = if along_normals {
|
||||||
row.element.segment_domain.point_tangent(point_index, row.element.point_domain.positions()).map(|t| t.perp())
|
row.element.segment_domain.point_tangent(point_index, row.element.point_domain.positions()).map(|t| t.perp())
|
||||||
|
|
@ -1912,44 +1980,17 @@ async fn jitter_points(
|
||||||
};
|
};
|
||||||
|
|
||||||
let offset = if let Some(normal) = normal {
|
let offset = if let Some(normal) = normal {
|
||||||
(rng.random::<f64>() * 2. - 1.) * normal
|
normal * (rng.random::<f64>() * 2. - 1.)
|
||||||
} else {
|
} else {
|
||||||
rng.random::<f64>() * DVec2::from_angle(rng.random::<f64>() * TAU)
|
DVec2::from_angle(rng.random::<f64>() * TAU) * rng.random::<f64>()
|
||||||
};
|
};
|
||||||
|
|
||||||
inverse_transform.transform_vector2(offset * amount)
|
inverse_linear * (offset * amount)
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect();
|
||||||
let mut already_applied = vec![false; row.element.point_domain.positions().len()];
|
|
||||||
|
|
||||||
for (handles, start, end) in row.element.segment_domain.handles_and_points_mut() {
|
apply_point_deltas(&mut row.element, &deltas, row.transform);
|
||||||
let start_delta = deltas[*start];
|
|
||||||
let end_delta = deltas[*end];
|
|
||||||
|
|
||||||
if !already_applied[*start] {
|
|
||||||
let start_position = row.element.point_domain.positions()[*start];
|
|
||||||
row.element.point_domain.set_position(*start, start_position + start_delta);
|
|
||||||
already_applied[*start] = true;
|
|
||||||
}
|
|
||||||
if !already_applied[*end] {
|
|
||||||
let end_position = row.element.point_domain.positions()[*end];
|
|
||||||
row.element.point_domain.set_position(*end, end_position + end_delta);
|
|
||||||
already_applied[*end] = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
match handles {
|
|
||||||
BezierHandles::Cubic { handle_start, handle_end } => {
|
|
||||||
*handle_start += start_delta;
|
|
||||||
*handle_end += end_delta;
|
|
||||||
}
|
|
||||||
BezierHandles::Quadratic { handle } => {
|
|
||||||
*handle = row.transform.transform_point2(*handle) + (start_delta + end_delta) / 2.;
|
|
||||||
}
|
|
||||||
BezierHandles::Linear => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
row.element.style.set_stroke_transform(DAffine2::IDENTITY);
|
|
||||||
row
|
row
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
|
|
|
||||||
|
|
@ -214,6 +214,36 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||||
commits = fetched;
|
commits = fetched;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function extendCommitsForward() {
|
||||||
|
if (commits.length === 0) return false;
|
||||||
|
|
||||||
|
const newest = commits[commits.length - 1];
|
||||||
|
const since = new Date(newest.date.getTime() + 1000).toISOString();
|
||||||
|
|
||||||
|
let /** @type {any[]} */ allRaw = [];
|
||||||
|
let page = 1;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const raw = await fetchCommitList(since, undefined, page);
|
||||||
|
if (!raw || raw.length === 0) break;
|
||||||
|
allRaw = allRaw.concat(raw);
|
||||||
|
if (raw.length < 100) break;
|
||||||
|
page++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allRaw.length === 0) return false;
|
||||||
|
|
||||||
|
let fetched = parseCommits(allRaw);
|
||||||
|
fetched.reverse();
|
||||||
|
|
||||||
|
const existingShas = new Set(commits.map((c) => c.sha));
|
||||||
|
fetched = fetched.filter((c) => !existingShas.has(c.sha));
|
||||||
|
if (fetched.length === 0) return false;
|
||||||
|
|
||||||
|
commits = [...commits, ...fetched];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
async function extendCommitsBackward() {
|
async function extendCommitsBackward() {
|
||||||
if (commits.length === 0) return false;
|
if (commits.length === 0) return false;
|
||||||
|
|
||||||
|
|
@ -340,8 +370,9 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||||
badIndex = currentIndex;
|
badIndex = currentIndex;
|
||||||
boundarySearching = true;
|
boundarySearching = true;
|
||||||
} else {
|
} else {
|
||||||
// Absent at starting commit. The newest commit should have it (user assumes master has it).
|
// Absent at starting commit, so the issue was introduced more recently. Extend the commit list forward (towards HEAD) before narrowing.
|
||||||
goodIndex = currentIndex;
|
goodIndex = currentIndex;
|
||||||
|
await extendCommitsForward();
|
||||||
badIndex = commits.length - 1;
|
badIndex = commits.length - 1;
|
||||||
bisectPhase = "binary";
|
bisectPhase = "binary";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue