mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-23 06:48:11 +08:00
Shape boolean operation improvements (#547)
* changed path_intersection structure
* comment
* Removed do_if!
* Create project website with near-complete home page
* Added support for undoing
- i gotta say the undo system is quite nice
* Website responsive resizing improvements
* Add newsletter signup to website
* Pen tool fixes (#563)
Resolves 3 known bugs with the pen tool.
* Fixed crash pointed out by @caleb-ad
* Fixed issue with final path segment losing handle data
* Replace curves with lines when under a drag threshold, improves usability.
* Readability improvements, improved comments
* Color Input (#565)
* initial working prototype
* clean up component
* Fix alignment
* Code review tweaks
Co-authored-by: Keavon Chambers <keavon@keavon.com>
* Made the non-inclusive end of a pathseg less inclusive
Fixed Bug: When doing a closepath it is possible that the current and beginning edge are both None
Fixed Numerous other things
* changed how closepath works
modified how overlapping_curve_intersections is working
* Add "New Folder"/"Delete Selected" buttons to layer panel
Closes #532
* Update npm dependencies
* Set text color based on its fill when it's being edited
* Reorder tool icons, update favicon and logo, and other icon cleanup
* Bug Fix: Line-Line intersect origin wasn't being preserved
* Bug Fix: proper assignment of t_values in overlapping_curve_intersections
* Honestly, i don't even know what I was thinking when i wrote the logic for splitting a subcurve at endpoints, but it was wrong.
* Feature: overlapping rectangles behave properly, (except when intersections aren't found correctly)
* Remake node type icons (closes #483); color picker cleanup
* Change tool shelf icon colors to use classes not style
* Add Image node icon and rename node from Path to Shape
* Bug Fix: proper intersection construction in partial overlap case
* cleaned up log statements
* Add website revisions and many new pages
* Add features page and fixes to website
* Fix clippy lints and update packages (#568)
* Fix type error in Brave browser (#569)
* Small website text improvements
* Various website fixes
* Adjusted constants
Rearranged intersection algorithm
* Changed BooleanOperation::SubtractBack to use SubtractFront
Added composite_boolean_operation for operations with more than one shape
* Add node graph mockup to website
* Differentiate between scale and dimensions (#570)
* Differentiate between scale and dimensions
* Fix layout and naming of properties
* Add embedable images (#564)
* Add embedable bitmaps
* Initial work on blob urls
* Finish implementing data url
* Fix some bugs
* Rename bitmap to image
* Fix loading image on document load
* Add transform properties for image
* Remove some logging
* Add image dimensions
* Implement system copy and paste
* Fix pasting images
* Fix test
* Address code review
Co-authored-by: Keavon Chambers <keavon@keavon.com>
* Bump minimist from 1.2.5 to 1.2.6 in /frontend (#571)
Bumps [minimist](https://github.com/substack/minimist) from 1.2.5 to 1.2.6.
- [Release notes](https://github.com/substack/minimist/releases)
- [Commits](https://github.com/substack/minimist/compare/1.2.5...1.2.6)
---
updated-dependencies:
- dependency-name: minimist
dependency-type: indirect
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* Fix panic dialogue when handling a message (#573)
* Fix panic dialogue when handling a message
* Fix indents for github reporting
* More whitespace improvements
* Save: line_intersection
* Add documentation to many parts of the Rust codebase (#552)
* add lots of doccomments
* add conversion traits from layerdatatypes to layers
* add suggested doc improvements
* Code review changes
Co-authored-by: Keavon Chambers <keavon@keavon.com>
* Add additional stroke properties (#582)
* Add aditional stroke properties
* Add comment explaining clones for closure
* Improve labels
* Fix doc test
Co-authored-by: Keavon Chambers <keavon@keavon.com>
* Bug Fix: .y not .x
* Expand upon the "Announcing Graphite alpha" blog post
* All shapes now have a Fill in the properties panel; color inputs are now optional (#583)
* Add aditional stroke properties
* Make the colour input optional
* Fix fmt
* Apply code review changes
* Code review nitpicks
* Fix recursion
Co-authored-by: Keavon Chambers <keavon@keavon.com>
* New blog post: "Distributed computing with the Graphene runtime"
* Fix gradient transformation (#588)
* Fix with perfect circle
* Actually fix rotated gradient
* Gradient transform & fix on rotated canvas
* Cleanup & remove logging
* Add properties panel entries for artboards (#572)
* Artboards can have properties
* fix crash when renaming artboards
* moved target document to utility types
* moved import and added test for file version information
* fixed missing import
* fix error from merging
* - typed properties message handler data
- removed name from WidgetRow
* clippy warnings
* artboards have seperate properties section
* - color input can be forced to have selection
- crop tool shows on switch
- select tool shows on switch
* variable renamed
* change to use PropType<boolean> instead of PropType<Boolean>
* Add an artboard icon
* Add the "Delete Artboard" hint
* fix unselect glitch
* even better
* Remove the Transform properties group
Co-authored-by: Keavon Chambers <keavon@keavon.com>
* Bug Fix: boolean union on multiple shapes
* Font selection for text layers (#585)
* Add font dropdown
* Add fonts
* Font tool options
* Fix tests
* Replace http with https
* Add variant selection
* Do not embed default font
* Use proxied font list API
* Change default font to Merriweather
* Remove outdated comment
* Specify font once & load font into foreignobject
* Fix tests
* Rename variant to font_style
* Change TextAreaInput to use FieldInput (WIP, breaks functionality)
* Fix textarea functionality
* Fix types
* Add weight name mapping
* Change labeling of "Italic"
* Remove commented HTML node
* Rename font "name" to "font_family" and "file" "font_file"
* Fix errors
* Fix fmt
Co-authored-by: Keavon Chambers <keavon@keavon.com>
* Improvements to the layer transform cage UX (#589)
* Allow input system to handle mousedown while dragging
* Fix abort
* Add apsect ratio
* Make comment more explicit
* Fix abort when dragging
* Constrain when dragging edge
* Rename Crop tool to Artboard tool
* Stop pointerdown event from properties panel (#600)
* Change stroke weight from ints to floats (#601)
Also rename stroke "width" to "weight" in some places. Closes #587
* Change stroke weight from ints to floats
* "miter_limit" -> "line_join_miter_limit"
* Bump file format version
* cargo update
Prevent iterating beyond list bounds
* Bug fix: proper composite intersection behavior
* fix warnings
* Improved ray casting and common intersection cases
Finding intersections near path segment endpoints was previously unreliable
because of imprecision, and the necessity of avoiding double counting any
intersections. And, because of snapping, intersections on the endpoints
of path segments are a common case.
This also improved the ray casting use case, which previously used a "fudge factor"
to mitigate the common problem of casting a ray into line endpoints.
* fixed warnings
* Fix properties deselect (#606)
* Fix properties panel deselect
* Fix arrow cursors on select tool
* Fix drag from UI to document causing mouse down
* Fix tests
* Cleanup
* cleanup messages
* Draw the outlines of shapes on hover and selection (#609)
* Add hover outline overlay
* Increase selection tolerance
* Increase weight
* Only check if top intersection is selected
* Outline selected paths
* Reduce outline weight
* Increase path tool outline thickness to match hover
* Update to use unreachable! instead of panic!
* Upgrade vue-cli to version 5 (#594)
* Upgrade to Vue CLI 5 (fails to compile)
* Upgrade versions with last few weeks of changes
* Updated to fork-ts-checker-webpack-plugin 7.2.3
* Remove package.json overrides in lieu of the fixed fork-ts-checker-webpack-plugin@6.5.1
* Fix svg importing
* Comments
* For debugging only: added infrastructureLogging to vue.config.js
* Now works on Windows, waiting on fork-ts-checker-webpack-plugin backport if possible
* Switch to the fixed fork-ts-checker-webpack-plugin@6.5.2
* Fix license checker build compilation
Co-authored-by: 0hypercube <0hypercube@gmail.com>
* Tidy up the full frontend codebase and use optional chaining where possible (#620)
* Tidy up the full frontend codebase and use optional chaining where possible
* Code review changes
* Add a hotkey to select a random primary color (#622)
* Add shortcut to select a random primary color (#549)
* Rename random primary color message and reduce the number of calls to
generate_uuid
* Add documentation for SelectRandomPrimaryColor message
* Set the alpha value to 255 instead of a random value #622
Co-authored-by: Florent Collin <florentcollin23@gmail.com>
* Move the Layer Tree panel's New Folder and Delete icons into the options bar
* Migrate dialogs to Rust and add a New File dialog (#623)
* Migrate coming soon and about dialog to Rust
* Migrate confirm close and close all
* Migrate dialog error
* Improve keyboard navigation throughout UI
* Cleanup and fix panic dialog
* Reduce css spacing to better match old dialogs
* Add new document modal
* Fix crash when generating default name
* Populate rust about graphite data on startup
* Code review changes
* Move one more :focus CSS rule into App.vue
* Add a dialog message and move dialogs
* Split out keyboard input navigation from this branch
* Improvements including simplifying panic dialog code
Co-authored-by: Keavon Chambers <keavon@keavon.com>
* Snapping system improvements and refactor (#621)
* Snap to points and refactor
* Improve dot position on bounds
* Add snap matrix
* Cleanup
* Code review
* Half axis fade rather than increase it
* Fix fmt
* Hide snap to point overlay when active
Co-authored-by: Keavon Chambers <keavon@keavon.com>
* Add the File > Export dialog and PNG/JPG downloading (#629)
* Add export dialog
* Code review changes
* More code review feedback
* Fix compilation on stable Rust
* Fixes to problems
Co-authored-by: Keavon Chambers <keavon@keavon.com>
* Code review
Co-authored-by: Keavon Chambers <keavon@keavon.com>
Co-authored-by: Oliver Davies <oliver@psyfer.io>
Co-authored-by: mfish33 <32677537+mfish33@users.noreply.github.com>
Co-authored-by: 0HyperCube <78500760+0HyperCube@users.noreply.github.com>
Co-authored-by: TrueDoctor <dennis@kobert.dev>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Alaska <simon.wuelker@arcor.de>
Co-authored-by: 0hypercube <0hypercube@gmail.com>
Co-authored-by: FlorentCollin <florentcollinpro@gmail.com>
Co-authored-by: Florent Collin <florentcollin23@gmail.com>
This commit is contained in:
committed by
Keavon Chambers
co-authored by
Florent Collin
Keavon Chambers
Oliver Davies
mfish33
0HyperCube
TrueDoctor
dependabot[bot]
Alaska
0hypercube
FlorentCollin
parent
8dec73c344
commit
bd997cef60
+424
-172
@@ -1,13 +1,10 @@
|
||||
use core::panic;
|
||||
use std::ops::Mul;
|
||||
use crate::boolean_ops::{split_path_seg, subdivide_path_seg};
|
||||
use crate::consts::{F64LOOSE, F64PRECISE};
|
||||
|
||||
use crate::{
|
||||
boolean_ops::split_path_seg,
|
||||
boolean_ops::subdivide_path_seg,
|
||||
consts::{CURVE_FIDELITY, F64LOOSE, F64PRECISE},
|
||||
};
|
||||
use glam::{DAffine2, DMat2, DVec2};
|
||||
use kurbo::{BezPath, CubicBez, Line, ParamCurve, ParamCurveExtrema, PathSeg, Point, QuadBez, Rect, Shape, Vec2};
|
||||
use kurbo::{BezPath, CubicBez, Line, ParamCurve, ParamCurveDeriv, ParamCurveExtrema, PathSeg, Point, QuadBez, Rect, Shape, Vec2};
|
||||
use std::collections::VecDeque;
|
||||
use std::ops::Mul;
|
||||
|
||||
#[derive(Debug, Clone, Default, Copy)]
|
||||
/// A quad defined by four vertices.
|
||||
@@ -60,26 +57,27 @@ fn to_point(vec: DVec2) -> Point {
|
||||
|
||||
/// Return `true` if `quad` intersects `shape`.
|
||||
/// This is the case if any of the following conditions are true:
|
||||
/// * the edges of `quad` and `shape` intersect
|
||||
/// * `shape` is entirely contained within `quad`
|
||||
/// * `filled` is `true` and `quad` is entirely contained within `shape`.
|
||||
/// - the edges of `quad` and `shape` intersect
|
||||
/// - `shape` is entirely contained within `quad`
|
||||
/// - `filled` is `true` and `quad` is entirely contained within `shape`.
|
||||
pub fn intersect_quad_bez_path(quad: Quad, shape: &BezPath, filled: bool) -> bool {
|
||||
let mut shape = shape.clone();
|
||||
// for filled shapes act like shape was closed even if it isn't
|
||||
|
||||
// For filled shapes act like shape was closed even if it isn't
|
||||
if filled && shape.elements().last() != Some(&kurbo::PathEl::ClosePath) {
|
||||
shape.close_path();
|
||||
}
|
||||
|
||||
// check if outlines intersect
|
||||
// Check if outlines intersect
|
||||
if shape.segments().any(|path_segment| quad.lines().iter().any(|line| !path_segment.intersect_line(*line).is_empty())) {
|
||||
return true;
|
||||
}
|
||||
// check if selection is entirely within the shape
|
||||
// Check if selection is entirely within the shape
|
||||
if filled && shape.contains(to_point(quad.0[0])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// check if shape is entirely within selection
|
||||
// Check if shape is entirely within selection
|
||||
get_arbitrary_point_on_path(&shape).map(|shape_point| quad.path().contains(shape_point)).unwrap_or_default()
|
||||
}
|
||||
|
||||
@@ -93,12 +91,12 @@ pub fn get_arbitrary_point_on_path(path: &BezPath) -> Option<Point> {
|
||||
})
|
||||
}
|
||||
|
||||
/// \/ \/
|
||||
/// Bezier Curve Intersection algorithm
|
||||
/// \/ \/
|
||||
//
|
||||
// Bezier Curve Intersection algorithm
|
||||
//
|
||||
|
||||
/// Each intersection has two curves. This enum helps distinguished between the two.
|
||||
// TODO: refactor so actual curve data and Origin aren't separate
|
||||
// TODO: refactor so actual curve data and `Origin` aren't separate
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
pub enum Origin {
|
||||
Alpha,
|
||||
@@ -122,7 +120,6 @@ pub struct Intersect {
|
||||
pub t_b: f64,
|
||||
pub a_seg_index: i32,
|
||||
pub b_seg_index: i32,
|
||||
// TODO: remove the `quality` field
|
||||
pub quality: f64,
|
||||
}
|
||||
|
||||
@@ -171,16 +168,18 @@ impl From<(Point, f64, f64)> for Intersect {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct SubCurve<'a> {
|
||||
pub curve: &'a PathSeg,
|
||||
pub start_t: f64,
|
||||
pub end_t: f64,
|
||||
local: [Point; 2], // local endpoints
|
||||
/// Local endpoints
|
||||
local: [Point; 2],
|
||||
pub extrema: &'a Vec<(Point, f64)>,
|
||||
}
|
||||
|
||||
impl<'a> SubCurve<'a> {
|
||||
// TODO: Fix this Clippy lint error
|
||||
/// Extrema given by [SubCurve::subcurve_extrema], they are stored externally so they don't have to recalculated
|
||||
pub fn new(parent: &'a PathSeg, extrema: &'a Vec<(Point, f64)>) -> Self {
|
||||
SubCurve {
|
||||
curve: parent,
|
||||
@@ -191,6 +190,16 @@ impl<'a> SubCurve<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn subcurve_extrema(parent: &PathSeg) -> Vec<(Point, f64)> {
|
||||
// Extrema at endpoints should not be included here as they must be calculated for each subcurve
|
||||
// Note: below filtering may filter out extrema near the endpoints
|
||||
parent
|
||||
.extrema()
|
||||
.iter()
|
||||
.filter_map(|t| if *t > F64PRECISE && *t < 1.0 - F64PRECISE { Some((parent.eval(*t), *t)) } else { None })
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn bounding_box(&self) -> Rect {
|
||||
let mut bound = Rect {
|
||||
x0: self.start().x,
|
||||
@@ -203,6 +212,7 @@ impl<'a> SubCurve<'a> {
|
||||
.chain(
|
||||
self.extrema
|
||||
.iter()
|
||||
// Filter out "internal extrema which are not contained within this subcurve"
|
||||
.filter_map(|place_time| if place_time.1 > self.start_t && place_time.1 < self.end_t { Some(&place_time.0) } else { None }),
|
||||
)
|
||||
.for_each(|p| {
|
||||
@@ -227,7 +237,7 @@ impl<'a> SubCurve<'a> {
|
||||
}
|
||||
|
||||
/// Split subcurve at `t`, as though the subcurve is a bezier curve, where `t` is a value between `0.0` and `1.0`.
|
||||
fn split(&self, t: f64) -> (SubCurve, SubCurve) {
|
||||
fn split<'sub_life>(self: &'sub_life SubCurve<'a>, t: f64) -> (SubCurve<'a>, SubCurve<'a>) {
|
||||
let split_t = self.start_t + t * (self.end_t - self.start_t);
|
||||
(
|
||||
SubCurve {
|
||||
@@ -257,42 +267,54 @@ impl<'a> SubCurve<'a> {
|
||||
}
|
||||
|
||||
// TODO: use the cool algorithm described in: https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.99.9678&rep=rep1&type=pdf
|
||||
// Bezier Curve Intersection Algorithm
|
||||
// TODO: how does f64 precision affect the algorithm?
|
||||
// Error correction schemes?
|
||||
// TODO: profile algorithm
|
||||
// TODO: intersections of overlapping curve
|
||||
// If the algorithm is rewritten to be non-recursive it can be restructured to be more breadth first then depth first
|
||||
// Test for overlapping curves by splitting the curves
|
||||
// Behavior: deep recursion could result in stack overflow
|
||||
// Improvement: intersections on the end of segments
|
||||
// Improvement: more adaptive way to decide when "close enough"
|
||||
// Optimization: any extra copying happening?
|
||||
/// Bezier Curve Intersection Algorithm
|
||||
fn path_intersections(a: &SubCurve, b: &SubCurve, intersections: &mut Vec<Intersect>) {
|
||||
// At recursion depth 10:
|
||||
// - maximum recursive execution paths = 4^10 = 1048576
|
||||
// - minimum recursive execution paths = 1
|
||||
// - up to 9 cubic Bezier intersections
|
||||
// - `SubCurve` is 1/2^10 of the original curve
|
||||
// - conservatively: there should never be more than 9 * 100 = 1000 total recursive execution paths
|
||||
// TODO: Can probably be much less, should find a better capacity
|
||||
const MAX_CALL_NUM: usize = 1000;
|
||||
let mut call_buffer: VecDeque<(SubCurve, SubCurve)> = VecDeque::with_capacity(MAX_CALL_NUM);
|
||||
let mut recursion = 1.0;
|
||||
|
||||
fn path_intersections(a: &SubCurve, b: &SubCurve, mut recursion: f64, intersections: &mut Vec<Intersect>) {
|
||||
if overlap(&a.bounding_box(), &b.bounding_box()) {
|
||||
if let (PathSeg::Line(line), _) = (a.curve, b) {
|
||||
line_curve_intersections(line, b.curve, true, |a, b| valid_t(a) && valid_t(b), intersections);
|
||||
return;
|
||||
}
|
||||
if let (_, PathSeg::Line(line)) = (a, b.curve) {
|
||||
line_curve_intersections(line, a.curve, false, |a, b| valid_t(a) && valid_t(b), intersections);
|
||||
fn helper<'a, 'b: 'a>(a: &'a SubCurve<'b>, b: &'a SubCurve<'b>, recursion: f64, intersections: &mut Vec<Intersect>, call_buffer: &'a mut VecDeque<(SubCurve<'b>, SubCurve<'b>)>) {
|
||||
if let (PathSeg::Line(_), _) | (_, PathSeg::Line(_)) = (a.curve, b.curve) {
|
||||
line_curve_intersections((&mut a.curve.clone(), &mut b.curve.clone()), |a, b| valid_t(a) && valid_t(b), intersections);
|
||||
return;
|
||||
}
|
||||
// We are close enough to try linear approximation
|
||||
if recursion < (1 << 10) as f64 {
|
||||
// If the number of sub-curves being checked could exceed the threshold, check for overlap
|
||||
if call_buffer.len() >= MAX_CALL_NUM - 4 {
|
||||
overlapping_curve_intersections(a.curve, b.curve)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.for_each(|intersect| intersections.push(intersect));
|
||||
// Regardless of whether intersections were found, need to return to prevent crashing the editor
|
||||
// If no intersections are found above the curves are very close to overlapping but not quite
|
||||
return;
|
||||
}
|
||||
if let Some(mut cross) = line_intersection(&Line { p0: a.start(), p1: a.end() }, &Line { p0: b.start(), p1: b.end() }) {
|
||||
// Intersection `t_value` equals the recursive `t_value` + interpolated intersection value
|
||||
cross.t_a = a.start_t + cross.t_a * recursion;
|
||||
cross.t_b = b.start_t + cross.t_b * recursion;
|
||||
cross.t_a = a.start_t + cross.t_a * recursion;
|
||||
cross.quality = guess_quality(a.curve, b.curve, &cross);
|
||||
|
||||
// log::debug!("checking: {:?}", cross.quality);
|
||||
if cross.quality <= CURVE_FIDELITY {
|
||||
intersections.push(cross);
|
||||
if cross.quality <= F64LOOSE {
|
||||
// Invalid intersections should still be rejected
|
||||
// Rejects "valid" intersections on the non-inclusive end of a `PathSeg`
|
||||
if valid_t(cross.t_a) && valid_t(cross.t_b) {
|
||||
intersections.push(cross);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Eventually the points in the curve become too close together to split the curve meaningfully
|
||||
// Return the best estimate of intersection regardless of quality
|
||||
// Also provides a base case and prevents infinite recursion
|
||||
if a.available_precision() <= F64PRECISE || b.available_precision() <= F64PRECISE {
|
||||
log::debug!("precision reached");
|
||||
@@ -300,7 +322,6 @@ fn path_intersections(a: &SubCurve, b: &SubCurve, mut recursion: f64, intersecti
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Alternate base case
|
||||
// Note: may occur for the less forgiving side of a `PathSeg` endpoint intersect
|
||||
if a.available_precision() <= F64PRECISE || b.available_precision() <= F64PRECISE {
|
||||
@@ -308,28 +329,71 @@ fn path_intersections(a: &SubCurve, b: &SubCurve, mut recursion: f64, intersecti
|
||||
return;
|
||||
}
|
||||
}
|
||||
recursion /= 2.0;
|
||||
let (a1, a2) = a.split(0.5);
|
||||
let (b1, b2) = b.split(0.5);
|
||||
path_intersections(&a1, &b1, recursion, intersections);
|
||||
path_intersections(&a1, &b2, recursion, intersections);
|
||||
path_intersections(&a2, &b1, recursion, intersections);
|
||||
path_intersections(&a2, &b2, recursion, intersections);
|
||||
|
||||
if overlap(&a1.bounding_box(), &b1.bounding_box()) {
|
||||
call_buffer.push_back((a1, b1));
|
||||
}
|
||||
if overlap(&a1.bounding_box(), &b2.bounding_box()) {
|
||||
call_buffer.push_back((a1, b2));
|
||||
}
|
||||
if overlap(&a2.bounding_box(), &b1.bounding_box()) {
|
||||
call_buffer.push_back((a2, b1));
|
||||
}
|
||||
if overlap(&a2.bounding_box(), &b2.bounding_box()) {
|
||||
call_buffer.push_back((a2, b2));
|
||||
}
|
||||
}
|
||||
|
||||
call_buffer.push_back((*a, *b));
|
||||
while !call_buffer.is_empty() {
|
||||
let mut current_level = call_buffer.len();
|
||||
while current_level > 0 {
|
||||
let (a, b) = call_buffer.pop_front().unwrap();
|
||||
helper(&a, &b, recursion, intersections, &mut call_buffer);
|
||||
recursion /= 2.0;
|
||||
current_level -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn line_curve_intersections<F>(line: &Line, curve: &PathSeg, is_line_a: bool, t_validate: F, intersections: &mut Vec<Intersect>)
|
||||
/// Does nothing when neither PathSeg in `line_curve` is a line.
|
||||
/// Closure `t_validate` takes the two t_values of an Intersect as arguments.
|
||||
/// The order of the t_values corresponds with the order of the PathSegs in `line_curve`,
|
||||
/// `t_validate` should return true for allowable intersection t_values, valid intersections will be added to `intersections`.
|
||||
pub fn line_curve_intersections<F>(line_curve: (&mut PathSeg, &mut PathSeg), t_validate: F, intersections: &mut Vec<Intersect>)
|
||||
where
|
||||
F: Fn(f64, f64) -> bool,
|
||||
{
|
||||
if let (line, PathSeg::Line(line2)) = (line, curve) {
|
||||
extend_curve(line_curve.0, F64PRECISE);
|
||||
extend_curve(line_curve.1, F64PRECISE);
|
||||
|
||||
if let (PathSeg::Line(line), PathSeg::Line(line2)) = line_curve {
|
||||
if let Some(cross) = line_intersection(line, line2) {
|
||||
if t_validate(cross.t_a, cross.t_b) {
|
||||
intersections.push(cross);
|
||||
}
|
||||
} else {
|
||||
//the lines may be overlapping
|
||||
overlapping_curve_intersections(line_curve.0, line_curve.1)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.for_each(|intersect| intersections.push(intersect))
|
||||
}
|
||||
} else {
|
||||
// Forced to construct a `Vec` here because match arms must return same type, and E0716
|
||||
let is_line_a;
|
||||
let (line, curve) = match line_curve {
|
||||
(PathSeg::Line(line), curve) => {
|
||||
is_line_a = true;
|
||||
(line, curve)
|
||||
}
|
||||
(curve, PathSeg::Line(line)) => {
|
||||
is_line_a = false;
|
||||
(line, curve)
|
||||
}
|
||||
_ => return,
|
||||
};
|
||||
let roots = match curve {
|
||||
PathSeg::Quad(quad) => Vec::from(quad_line_intersect(line, quad)),
|
||||
PathSeg::Cubic(cubic) => Vec::from(cubic_line_intersect(line, cubic)),
|
||||
@@ -345,7 +409,7 @@ where
|
||||
PathSeg::Quad(quad) => quad.eval(*time),
|
||||
_ => Point::new(0.0, 0.0), // Should never occur
|
||||
};
|
||||
// the intersection point should be on the line, unless FP math error produces bad results
|
||||
// The intersection point should be on the line, unless floating point math error produces bad results
|
||||
let line_time = projection_on_line(line, &point);
|
||||
if !t_validate(line_time, *time) {
|
||||
return None;
|
||||
@@ -364,6 +428,23 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Extend the starting point of `curve` backwards along its derivative.
|
||||
/// Used to make finding intersections near endpoints reliable.
|
||||
pub fn extend_curve(curve: &mut PathSeg, distance: f64) {
|
||||
fn extended_start<C: ParamCurve + ParamCurveDeriv>(c: &mut C, d: f64) -> Point {
|
||||
let mut c_prime = c.deriv().eval(0.0);
|
||||
c_prime.x *= d / c_prime.distance(Point::ORIGIN);
|
||||
c_prime.y *= d / c_prime.distance(Point::ORIGIN);
|
||||
let es_vec = c.eval(0.0) - c_prime;
|
||||
Point { x: es_vec.x, y: es_vec.y }
|
||||
}
|
||||
match curve {
|
||||
PathSeg::Line(line) => line.p0 = extended_start(line, distance),
|
||||
PathSeg::Quad(quad) => quad.p0 = extended_start(quad, distance),
|
||||
PathSeg::Cubic(cubic) => cubic.p0 = extended_start(cubic, distance),
|
||||
};
|
||||
}
|
||||
|
||||
/// For quality Q in the worst case, the point on curve `a` corresponding to `guess` is distance Q from the point on curve `b`.
|
||||
// TODO: Optimization: inline? maybe..
|
||||
fn guess_quality(a: &PathSeg, b: &PathSeg, guess: &Intersect) -> f64 {
|
||||
@@ -372,15 +453,17 @@ fn guess_quality(a: &PathSeg, b: &PathSeg, guess: &Intersect) -> f64 {
|
||||
at_a.distance(guess.point) + at_b.distance(guess.point)
|
||||
}
|
||||
|
||||
/// Returns either [None, None] or [Some(_), Some(_)],
|
||||
/// The above may not be true when either a or b is very small (their endpoints are close together), so that when the algorithm does curve splitting
|
||||
/// TODO: test this
|
||||
/// TODO: use this
|
||||
/// If curves overlap, returns intersections corresponding to the endpoints of the overlapping section
|
||||
///
|
||||
/// # Panics
|
||||
/// May panic if either curve is very short, or has endpoints which are close together
|
||||
// TODO: test the case where a and b are identical
|
||||
// TODO: test this, especially the overlapping curve cases which are more complex
|
||||
pub fn overlapping_curve_intersections(a: &PathSeg, b: &PathSeg) -> [Option<Intersect>; 2] {
|
||||
// To check if two curves overlap we find if the endpoints of either curve are on the other curve.
|
||||
// Then, the curves are split at these points, if the resulting control polygons match the curves are the same
|
||||
let mut b_on_a: Vec<Option<f64>> = [point_t_value(a, &b.start()), point_t_value(a, &b.end())].into_iter().collect();
|
||||
let mut a_on_b: Vec<Option<f64>> = [point_t_value(b, &a.start()), point_t_value(b, &a.end())].into_iter().collect();
|
||||
let b_on_a: Vec<Option<f64>> = [point_t_value(a, &b.start()), point_t_value(a, &b.end())].into_iter().collect();
|
||||
let a_on_b: Vec<Option<f64>> = [point_t_value(b, &a.start()), point_t_value(b, &a.end())].into_iter().collect();
|
||||
// I think, but have not mathematically shown, that if a and b are parts of the same curve then b_on_a and a_on_b should together have no more than three non-None elements. Which occurs when a or b is a cubic bezier which crosses itself
|
||||
let b_on_a_not_none = b_on_a.iter().filter_map(|o| *o).count();
|
||||
let a_on_b_not_none = a_on_b.iter().filter_map(|o| *o).count();
|
||||
@@ -388,66 +471,73 @@ pub fn overlapping_curve_intersections(a: &PathSeg, b: &PathSeg) -> [Option<Inte
|
||||
2 | 3 => {
|
||||
let (t1a, t1b, t2a, t2b): (f64, f64, f64, f64);
|
||||
let to_compare = if b_on_a_not_none == 2 {
|
||||
b_on_a.sort_by(|val1, val2| (val1).partial_cmp(val2).unwrap_or(std::cmp::Ordering::Less));
|
||||
let mut a_ts = b_on_a.iter_mut().filter_map(|o| *o).collect::<Vec<f64>>();
|
||||
t1a = a_ts[0];
|
||||
t1a = b_on_a[0].unwrap();
|
||||
t1b = 0.0;
|
||||
t2a = a_ts[1];
|
||||
t2a = b_on_a[1].unwrap();
|
||||
t2b = 1.0;
|
||||
(*b, subdivide_path_seg(a, a_ts.as_mut_slice())[1].unwrap())
|
||||
let mut split_at = if t1a > t2a { [t2a, t1a] } else { [t1a, t2a] };
|
||||
(*b, subdivide_path_seg(a, &mut split_at)[1].unwrap())
|
||||
} else if a_on_b_not_none == 2 {
|
||||
a_on_b.sort_by(|val1, val2| (val1).partial_cmp(val2).unwrap_or(std::cmp::Ordering::Less));
|
||||
let mut b_ts = a_on_b.iter_mut().filter_map(|o| *o).collect::<Vec<f64>>();
|
||||
t1a = 0.0;
|
||||
t1b = b_ts[0];
|
||||
t1b = a_on_b[0].unwrap();
|
||||
t2a = 1.0;
|
||||
t2b = b_ts[1];
|
||||
(*a, subdivide_path_seg(a, b_ts.as_mut_slice())[1].unwrap())
|
||||
t2b = a_on_b[1].unwrap();
|
||||
let mut split_at = if t1b > t2b { [t2b, t1b] } else { [t1b, t2b] };
|
||||
(*a, subdivide_path_seg(b, &mut split_at)[1].unwrap())
|
||||
} else {
|
||||
(
|
||||
match (b_on_a[0], b_on_a[1], a_on_b[0], a_on_b[1]) {
|
||||
(None, Some(_), _, Some(t_val)) | (None, Some(_), Some(t_val), _) => {
|
||||
t1b = t_val;
|
||||
t2b = 1.0;
|
||||
split_path_seg(b, t_val).1.unwrap()
|
||||
}
|
||||
(Some(_), None, _, Some(t_val)) | (Some(_), None, Some(t_val), _) => {
|
||||
t1b = 0.0;
|
||||
t2b = t_val;
|
||||
split_path_seg(b, t_val).0.unwrap()
|
||||
}
|
||||
_ => panic!(),
|
||||
},
|
||||
match (a_on_b[0], a_on_b[1], b_on_a[0], b_on_a[1]) {
|
||||
(None, Some(_), _, Some(t_val)) | (None, Some(_), Some(t_val), _) => {
|
||||
t1a = t_val;
|
||||
t2a = 1.0;
|
||||
split_path_seg(a, t_val).1.unwrap()
|
||||
}
|
||||
(Some(_), None, _, Some(t_val)) | (Some(_), None, Some(t_val), _) => {
|
||||
t1a = 0.0;
|
||||
t2a = t_val;
|
||||
split_path_seg(a, t_val).0.unwrap()
|
||||
}
|
||||
_ => panic!(),
|
||||
},
|
||||
)
|
||||
match (b_on_a[0], b_on_a[1], a_on_b[0], a_on_b[1]) {
|
||||
(None, Some(a_val), None, Some(b_val)) => {
|
||||
t1b = b_val;
|
||||
t2b = 1.0;
|
||||
t1a = 1.0;
|
||||
t2a = a_val;
|
||||
(split_path_seg(b, b_val).1.unwrap(), split_path_seg(a, a_val).1.unwrap())
|
||||
}
|
||||
(None, Some(a_val), Some(b_val), None) => {
|
||||
t1b = b_val;
|
||||
t2b = 1.0;
|
||||
t1a = 0.0;
|
||||
t2a = a_val;
|
||||
(split_path_seg(b, b_val).1.unwrap(), split_path_seg(a, a_val).0.unwrap())
|
||||
}
|
||||
(Some(a_val), None, None, Some(b_val)) => {
|
||||
t1b = 0.0;
|
||||
t2b = b_val;
|
||||
t1a = a_val;
|
||||
t2a = 1.0;
|
||||
(split_path_seg(b, b_val).0.unwrap(), split_path_seg(a, a_val).1.unwrap())
|
||||
}
|
||||
(Some(a_val), None, Some(b_val), None) => {
|
||||
t1b = 0.0;
|
||||
t2b = b_val;
|
||||
t1a = a_val;
|
||||
t2a = 0.0;
|
||||
(split_path_seg(b, b_val).0.unwrap(), split_path_seg(a, a_val).0.unwrap())
|
||||
}
|
||||
_ => unreachable!("Overlapping curve intersections: too many intersections for match arm"),
|
||||
}
|
||||
};
|
||||
let mut to_return = [None, None];
|
||||
if match_control_polygon(&to_compare.0, &to_compare.1) {
|
||||
[Some(Intersect::from((to_compare.0.start(), t1a, t1b))), Some(Intersect::from((to_compare.0.end(), t2a, t2b)))]
|
||||
} else {
|
||||
[None, None]
|
||||
if valid_t(t1a) && valid_t(t1b) {
|
||||
to_return[0] = Some(Intersect::from((to_compare.0.start(), t1a, t1b)));
|
||||
}
|
||||
if valid_t(t2a) && valid_t(t2b) {
|
||||
to_return[1] = Some(Intersect::from((to_compare.0.end(), t2a, t2b)));
|
||||
}
|
||||
}
|
||||
to_return
|
||||
}
|
||||
_ => [None, None],
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the Bezier curves described by A and B have the same control polygon
|
||||
/// TODO: test this
|
||||
/// Returns true if the Bezier curves described by `a` and `b` have the same control polygon.
|
||||
/// The order of the polygon does not effect the result,
|
||||
pub fn match_control_polygon(a: &PathSeg, b: &PathSeg) -> bool {
|
||||
let mut a_polygon = get_control_polygon(a);
|
||||
let mut b_polygon = get_control_polygon(b);
|
||||
// Allow matching of polygons whose points are reverse ordered
|
||||
if a_polygon.first().unwrap() == b_polygon.last().unwrap() && a_polygon.last().unwrap() == b_polygon.first().unwrap() {
|
||||
b_polygon.reverse()
|
||||
}
|
||||
@@ -486,7 +576,7 @@ pub fn match_control_polygon(a: &PathSeg, b: &PathSeg) -> bool {
|
||||
pub fn colinear(points: &[&Point]) -> bool {
|
||||
let ray = Line { p0: *points[0], p1: *points[1] };
|
||||
for p in points.iter().skip(2) {
|
||||
if line_t_value(&ray, p).is_none() {
|
||||
if point_t_value(&PathSeg::Line(ray), p).is_none() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -501,18 +591,34 @@ pub fn get_control_polygon(a: &PathSeg) -> Vec<Point> {
|
||||
}
|
||||
}
|
||||
|
||||
/// if p in on pathseg a, returns Some(t_value) for p
|
||||
/// in the edge case where the path crosses itself, and p is at the cross, the first t_value found (but not necessarily the smallest) is returned
|
||||
/// If `p` in on `PathSeg` `a`, returns `Some(t_value)` for `p` in the edge case where the path crosses itself,
|
||||
/// and `p` is at the cross, the first t_value found (but not necessarily the smallest `t_value`) is returned.
|
||||
// TODO: create a trait or something for roots to remove duplicate code
|
||||
pub fn point_t_value(a: &PathSeg, p: &Point) -> Option<f64> {
|
||||
match a {
|
||||
PathSeg::Line(line) => line_t_value(line, p),
|
||||
PathSeg::Line(line) => {
|
||||
let [mut p0, p1] = linear_bezier_coefficients(line);
|
||||
p0 -= p.to_vec2();
|
||||
let x_root = linear_root(p0.x, p1.x);
|
||||
let y_root = linear_root(p0.y, p1.y);
|
||||
if let (Some(x_root_val), Some(y_root_val)) = (x_root, y_root) {
|
||||
if (y_root_val - x_root_val).abs() < F64LOOSE {
|
||||
return Some(x_root_val);
|
||||
}
|
||||
}
|
||||
return None;
|
||||
}
|
||||
PathSeg::Quad(quad) => {
|
||||
let [mut p0, p1, p2] = quadratic_bezier_coefficients(quad);
|
||||
p0 -= p.to_vec2();
|
||||
let x_roots = quadratic_real_roots(p0.x, p1.x, p2.x);
|
||||
quadratic_real_roots(p0.y, p1.y, p2.y)
|
||||
.into_iter()
|
||||
.find(|yt_option| x_roots.iter().any(|xt_option| yt_option.is_some() && xt_option.is_some() && (yt_option.unwrap() == xt_option.unwrap())))
|
||||
.find(|yt_option| {
|
||||
x_roots
|
||||
.iter()
|
||||
.any(|xt_option| yt_option.is_some() && xt_option.is_some() && ((yt_option.unwrap() - xt_option.unwrap()).abs() < F64LOOSE))
|
||||
})
|
||||
.flatten()
|
||||
}
|
||||
PathSeg::Cubic(cubic) => {
|
||||
@@ -521,10 +627,15 @@ pub fn point_t_value(a: &PathSeg, p: &Point) -> Option<f64> {
|
||||
let x_roots = cubic_real_roots(p0.x, p1.x, p2.x, p3.x);
|
||||
cubic_real_roots(p0.y, p1.y, p2.y, p3.y)
|
||||
.into_iter()
|
||||
.find(|yt_option| x_roots.iter().any(|xt_option| yt_option.is_some() && xt_option.is_some() && (yt_option.unwrap() == xt_option.unwrap())))
|
||||
.find(|yt_option| {
|
||||
x_roots
|
||||
.iter()
|
||||
.any(|xt_option| yt_option.is_some() && xt_option.is_some() && ((yt_option.unwrap() - xt_option.unwrap()).abs() < F64LOOSE))
|
||||
})
|
||||
.flatten()
|
||||
}
|
||||
}
|
||||
.and_then(|t| if valid_t(t) { Some(t) } else { None })
|
||||
}
|
||||
|
||||
pub fn intersections(a: &BezPath, b: &BezPath) -> Vec<Intersect> {
|
||||
@@ -534,26 +645,14 @@ pub fn intersections(a: &BezPath, b: &BezPath) -> Vec<Intersect> {
|
||||
let mut intersections: Vec<Intersect> = Vec::new();
|
||||
// There is some duplicate computation of b_extrema here, but I doubt it's significant
|
||||
a.segments().enumerate().for_each(|(a_index, a_seg)| {
|
||||
// Extrema at endpoints should not be included here as they must be calculated for each subcurve
|
||||
// Note: below filtering may filter out extrema near the endpoints
|
||||
let a_extrema = a_seg
|
||||
.extrema()
|
||||
.iter()
|
||||
.filter_map(|t| if *t > F64PRECISE && *t < 1.0 - F64PRECISE { Some((a_seg.eval(*t), *t)) } else { None })
|
||||
.collect();
|
||||
let a_extrema = SubCurve::subcurve_extrema(&a_seg);
|
||||
b.segments().enumerate().for_each(|(b_index, b_seg)| {
|
||||
let b_extrema = b_seg
|
||||
.extrema()
|
||||
.iter()
|
||||
.filter_map(|t| if *t > F64PRECISE && *t < 1.0 - F64PRECISE { Some((b_seg.eval(*t), *t)) } else { None })
|
||||
.collect();
|
||||
let b_extrema = SubCurve::subcurve_extrema(&b_seg);
|
||||
let mut intersects = Vec::new();
|
||||
path_intersections(&SubCurve::new(&a_seg, &a_extrema), &SubCurve::new(&b_seg, &b_extrema), 1.0, &mut intersects);
|
||||
path_intersections(&SubCurve::new(&a_seg, &a_extrema), &SubCurve::new(&b_seg, &b_extrema), &mut intersects);
|
||||
for mut path_intersection in intersects {
|
||||
intersections.push({
|
||||
path_intersection.add_index(a_index.try_into().unwrap(), b_index.try_into().unwrap());
|
||||
path_intersection
|
||||
});
|
||||
path_intersection.add_index(a_index.try_into().unwrap(), b_index.try_into().unwrap());
|
||||
intersections.push(path_intersection);
|
||||
}
|
||||
})
|
||||
});
|
||||
@@ -570,14 +669,15 @@ pub fn line_intersect_point(a: &Line, b: &Line) -> Option<Point> {
|
||||
|
||||
/// Returns intersection point and `t` values, treating lines as Bezier curves.
|
||||
pub fn line_intersection(a: &Line, b: &Line) -> Option<Intersect> {
|
||||
if let Some(intersect) = line_intersection_unchecked(a, b) {
|
||||
if valid_t(intersect.t_a) && valid_t(intersect.t_b) {
|
||||
Some(intersect)
|
||||
} else {
|
||||
None
|
||||
match line_intersection_unchecked(a, b) {
|
||||
Some(intersect) => {
|
||||
if valid_t(intersect.t_a) && valid_t(intersect.t_b) {
|
||||
Some(intersect)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -587,31 +687,11 @@ pub fn line_intersection_unchecked(a: &Line, b: &Line) -> Option<Intersect> {
|
||||
if slopes.determinant() == 0.0 {
|
||||
return None;
|
||||
}
|
||||
let t_values = slopes.inverse() * DVec2::new((a.p0 - b.p0).x, (a.p0 - b.p0).y);
|
||||
let t_values = slopes.inverse() * DVec2::new(a.p0.x - b.p0.x, a.p0.y - b.p0.y);
|
||||
Some(Intersect::from((b.eval(t_values[0]), t_values[1], t_values[0])))
|
||||
}
|
||||
|
||||
/// if p in on line a, returns Some(t_value) for p
|
||||
/// t_values seem to be accurate to roughly 7-10 decimal places
|
||||
pub fn line_t_value(a: &Line, p: &Point) -> Option<f64> {
|
||||
let from_x = (p.x - a.p0.x) / (a.p1.x - a.p0.x);
|
||||
let from_y = (p.y - a.p0.y) / (a.p1.y - a.p0.y);
|
||||
if !from_x.is_normal() {
|
||||
if !from_y.is_normal() {
|
||||
None
|
||||
} else {
|
||||
Some(from_y)
|
||||
}
|
||||
} else if !from_y.is_normal() {
|
||||
Some(from_x)
|
||||
} else if (from_x - from_y).abs() < F64LOOSE {
|
||||
Some(0.5 * (from_x + from_y))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// return the t_value of the point nearest to p on a
|
||||
/// Returns the `t_value` of the point nearest to `p` on `a`.
|
||||
pub fn projection_on_line(a: &Line, p: &Point) -> f64 {
|
||||
let ray = a.p1.to_vec2() - a.p0.to_vec2();
|
||||
ray.dot(p.to_vec2() - a.p0.to_vec2()) / ((ray.to_point().distance(Point::ORIGIN)) * (ray.to_point().distance(Point::ORIGIN)))
|
||||
@@ -651,7 +731,7 @@ pub fn quad_line_intersect(a: &Line, b: &QuadBez) -> [Option<f64>; 2] {
|
||||
}
|
||||
|
||||
/// Returns real roots to cubic equation: `f(t) = a0 + t*a1 + t^2*a2 + t^3*a3`.
|
||||
/// This function uses the Cardano-Viete and Numerical Recipes algorithm, found here: <https://quarticequations.com/Cubic.pdf>
|
||||
/// This function uses the Cardano-Viete and Numerical Recipes algorithm, found here: https://quarticequations.com/Cubic.pdf
|
||||
pub fn cubic_real_roots(mut a0: f64, mut a1: f64, mut a2: f64, a3: f64) -> [Option<f64>; 3] {
|
||||
use std::f64::consts::FRAC_PI_3 as PI_3;
|
||||
|
||||
@@ -688,8 +768,8 @@ pub fn cubic_real_roots(mut a0: f64, mut a1: f64, mut a2: f64, a3: f64) -> [Opti
|
||||
}
|
||||
}
|
||||
|
||||
/// a quadratic bezier can be written x = p0 + t*p1 + t^2*p2 + t^3*p3, where x, p0, p1, p2, and p3 are vectors
|
||||
/// this function returns [p0, p1, p2, p3]
|
||||
/// A quadratic bezier can be written `x = p0 + t*p1 + t^2*p2 + t^3*p3`, where `x`, `p0`, `p1`, `p2`, and `p3` are vectors.
|
||||
/// This function returns `[p0, p1, p2, p3]`.
|
||||
pub fn cubic_bezier_coefficients(cubic: &CubicBez) -> [Vec2; 4] {
|
||||
let p0 = cubic.p0.to_vec2();
|
||||
let p1 = cubic.p1.to_vec2();
|
||||
@@ -702,7 +782,8 @@ pub fn cubic_bezier_coefficients(cubic: &CubicBez) -> [Vec2; 4] {
|
||||
[c0, c1, c2, c3]
|
||||
}
|
||||
|
||||
/// Returns real roots to quadratic equation: `f(t) = a0 + t*a1 + t^2*a2`.
|
||||
/// Returns real roots to the quadratic equation: `f(t) = a0 + t*a1 + t^2*a2`.
|
||||
// TODO: make numerically stable
|
||||
pub fn quadratic_real_roots(a0: f64, a1: f64, a2: f64) -> [Option<f64>; 2] {
|
||||
let radicand = a1 * a1 - 4.0 * a2 * a0;
|
||||
if radicand < 0.0 {
|
||||
@@ -711,8 +792,8 @@ pub fn quadratic_real_roots(a0: f64, a1: f64, a2: f64) -> [Option<f64>; 2] {
|
||||
[Some((-a1 + radicand.sqrt()) / (2.0 * a2)), Some((-a1 - radicand.sqrt()) / (2.0 * a2))]
|
||||
}
|
||||
|
||||
/// a quadratic bezier can be written x = p0 + t*p1 + t^2*p2, where x, p0, p1, and p2 are vectors
|
||||
/// this function returns [p0, p1, p2]
|
||||
/// A quadratic bezier can be written `x = p0 + t*p1 + t^2*p2`, where `x`, `p0`, `p1`, and `p2` are vectors.
|
||||
/// This function returns `[p0, p1, p2]`.
|
||||
pub fn quadratic_bezier_coefficients(quad: &QuadBez) -> [Vec2; 3] {
|
||||
let p0 = quad.p0.to_vec2();
|
||||
let p1 = quad.p1.to_vec2();
|
||||
@@ -723,12 +804,23 @@ pub fn quadratic_bezier_coefficients(quad: &QuadBez) -> [Vec2; 3] {
|
||||
[c0, c1, c2]
|
||||
}
|
||||
|
||||
/// Returns root to linear equation: `f(t) = a0 + t*a1`.
|
||||
pub fn linear_root(a0: f64, a1: f64) -> [Option<f64>; 1] {
|
||||
/// Returns the root to the linear equation: `f(t) = a0 + t*a1`.
|
||||
pub fn linear_root(a0: f64, a1: f64) -> Option<f64> {
|
||||
if a1 == 0.0 {
|
||||
return [None];
|
||||
return None;
|
||||
}
|
||||
[Some(-a0 / a1)]
|
||||
if a1.is_infinite() {
|
||||
return Some(a0);
|
||||
}
|
||||
Some(-a0 / a1)
|
||||
}
|
||||
|
||||
/// A line can be written `x = p0 + t*p1`, where `x`, `p0` and `p1` are vectors.
|
||||
/// Returns `[p0, p1]`.
|
||||
pub fn linear_bezier_coefficients(line: &Line) -> [Vec2; 2] {
|
||||
let p0 = line.p0.to_vec2();
|
||||
let p1 = line.p1.to_vec2();
|
||||
[p0, p1 - p0]
|
||||
}
|
||||
|
||||
/// Returns `true` if rectangles overlap, even if either rectangle has 0 area.
|
||||
@@ -738,16 +830,21 @@ pub fn overlap(a: &Rect, b: &Rect) -> bool {
|
||||
}
|
||||
|
||||
/// Tests if a `t` value belongs to `[0.0, 1.0)`.
|
||||
/// Uses F64PRECISE to allow a slightly larger range of values.
|
||||
/// Uses [crate::consts::F64PRECISE] to allow a slightly larger range of values.
|
||||
pub fn valid_t(t: f64) -> bool {
|
||||
t > -F64PRECISE && t < 1.0
|
||||
t > -F64PRECISE && t < (1.0 - F64PRECISE)
|
||||
}
|
||||
|
||||
/// Each of these tests has been visually, but not mathematically verified.
|
||||
/// These tests are all ignored because each test looks for exact floating point comparisons, so isn't flexible to small adjustments in the algorithm.
|
||||
/// Each of these tests have been visually, but not mathematically, verified.
|
||||
/// These tests are all ignored because each test looks for exact floating point comparisons, so isn't tolerant to small adjustments in the algorithm.
|
||||
mod tests {
|
||||
#[allow(unused_imports)] // This import is used
|
||||
// These imports are used in the tests which are #[ignore]
|
||||
#[allow(unused_imports)]
|
||||
use super::*;
|
||||
#[allow(unused_imports)]
|
||||
use crate::boolean_ops::point_on_curve;
|
||||
#[allow(unused_imports)]
|
||||
use std::{fs::File, io::Write};
|
||||
|
||||
/// Two intersect points, on different `PathSegs`.
|
||||
#[ignore]
|
||||
@@ -830,7 +927,7 @@ mod tests {
|
||||
assert!(expected.iter().zip(result.iter()).fold(true, |equal, (a, b)| equal && a == b));
|
||||
}
|
||||
|
||||
/// Intersect points at ends of `PathSegs`.
|
||||
/// Intersect points at ends of `PathSeg`s.
|
||||
#[ignore]
|
||||
#[test]
|
||||
fn curve_intersection_seg_edges() {
|
||||
@@ -892,4 +989,159 @@ mod tests {
|
||||
let p4 = Point { x: 720.297, y: 1443.594 };
|
||||
assert!(colinear(&[&p1, &p2, &p3, &p4]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn test_point_t_value() {
|
||||
let vertical_line = Line::new(Point::new(0.0, -10.0), Point::new(0.0, 10.0));
|
||||
let t_value = point_t_value(&PathSeg::Line(vertical_line), &Point::new(0.0, 1.0));
|
||||
assert_eq!(t_value.unwrap(), 0.55);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn test_kurbo_eval_stability() {
|
||||
let mut test_results = File::create("..\\target\\debug\\test_kurbo_eval_results.txt").expect("");
|
||||
let test_curve = BezPath::from_svg("M-355.41190151646936 -204.93220299904385C-355.41190151646936 -164.32790664074417 -389.9224217662629 -131.4116207799262 -432.4933059063151 -131.4116207799262C-475.06419004636723 -131.4116207799262 -509.5747102961608 -164.32790664074417 -509.5747102961608 -204.93220299904382C-509.5747102961608 -245.53649935734347 -475.06419004636723 -278.45278521816147 -432.4933059063151 -278.45278521816147C-389.9224217662629 -278.45278521816147 -355.41190151646936 -245.5364993573435 -355.41190151646936 -204.93220299904385").expect("").segments().next().unwrap();
|
||||
let mut val = 0.0;
|
||||
while val < 0.0 + 1000000.0 * f64::EPSILON {
|
||||
writeln!(&mut test_results, "{:?}, {:?}", val, test_curve.eval(val).x).expect("");
|
||||
val += f64::EPSILON;
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn test_quality_stability() {
|
||||
let mut test_results = File::create("..\\target\\debug\\test_quality_results.txt").expect("");
|
||||
let mut val = 0.0;
|
||||
|
||||
while val < 0.0 + 1000000.0 * f64::EPSILON {
|
||||
let a = Line::new(Point::new(0.0, 0.0), Point::new(1.0 + val + val, 1.0 + val + val));
|
||||
let b = Line::new(Point::new(0.0, 1.0 + val + val), Point::new(1.0 + val + val, 0.0));
|
||||
let guess = Intersect::from((Point::new(0.5 + val, 0.5 + val), 0.5, 0.5));
|
||||
|
||||
writeln!(&mut test_results, "{:?}, {:?}", val, guess_quality(&PathSeg::Line(a), &PathSeg::Line(b), &guess)).expect("");
|
||||
val += f64::EPSILON;
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn test_quality_cubic_stability() {
|
||||
let mut test_results = File::create("..\\target\\debug\\test_quality_cubic_results.txt").expect("");
|
||||
let mut val = 0.0;
|
||||
|
||||
while val < 0.0 + 1000000.0 * f64::EPSILON {
|
||||
let a = PathSeg::Cubic(CubicBez::new(
|
||||
Point::new(0.0 + val, 0.0),
|
||||
Point::new(0.25 + val, 0.661437827766),
|
||||
Point::new(0.75 + val, 0.968245836552),
|
||||
Point::new(1.0 + val, 1.0),
|
||||
));
|
||||
let b = PathSeg::Cubic(CubicBez::new(
|
||||
Point::new(0.0, 1.0),
|
||||
Point::new(0.25, 0.968245836552),
|
||||
Point::new(0.75, 0.661437827766),
|
||||
Point::new(1.0, 0.0),
|
||||
));
|
||||
let guess = Intersect::from((Point::new(0.5 + val, 0.5 + val), 0.5 + val, 0.5 + val));
|
||||
|
||||
writeln!(&mut test_results, "{:?}, {:?}", val, guess_quality(&a, &b, &guess)).expect("");
|
||||
val += f64::EPSILON;
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn test_line_intersection_stability() {
|
||||
let mut test_results = File::create("..\\target\\debug\\test_line_intersect_results.txt").expect("");
|
||||
let mut val = 0.0;
|
||||
|
||||
while val < 0.0 + 1000000.0 * f64::EPSILON {
|
||||
let a = Line::new(Point::new(0.0 + val, 0.0), Point::new(1.0 + val, 1.0));
|
||||
let b = Line::new(Point::new(0.0, 1.0), Point::new(1.0, 0.0));
|
||||
|
||||
let line_intersection = line_intersection(&a, &b).unwrap();
|
||||
writeln!(
|
||||
&mut test_results,
|
||||
"{:?}, {:?}, {:?}, {:?}",
|
||||
val,
|
||||
line_intersection.t_a,
|
||||
line_intersection.point.x,
|
||||
guess_quality(&PathSeg::Line(a), &PathSeg::Line(b), &line_intersection)
|
||||
)
|
||||
.expect("");
|
||||
val += f64::EPSILON;
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn test_line_intersection_cancellation() {
|
||||
let mut test_results = File::create("..\\target\\debug\\test_line_intersection_cancellation_results.txt").expect("");
|
||||
let val = 1.0;
|
||||
let mut theta = F64PRECISE;
|
||||
|
||||
while theta < std::f64::consts::FRAC_PI_2 - 0.1 {
|
||||
let a = Line::new(Point::new(1.0, 1.0), Point::new(1.0 + val, 1.0 + val));
|
||||
let b = Line::new(Point::new(1.0, 1.0 + val * f64::cos(theta)), Point::new(1.0 + val, 1.0 + val * f64::sin(theta)));
|
||||
|
||||
let line_intersection = line_intersection(&a, &b).unwrap();
|
||||
writeln!(
|
||||
&mut test_results,
|
||||
"{:?}, {:?}, {:?}, {:?}",
|
||||
theta, line_intersection.t_a, line_intersection.t_b, line_intersection.point.x,
|
||||
)
|
||||
.expect("");
|
||||
theta += f64::powf(2.0, 20.0) * F64LOOSE;
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn test_intersections_stability() {
|
||||
let mut test_results_intersection = File::create("..\\target\\debug\\test_curve_intersections_multi_results.txt").expect("");
|
||||
let mut val = 0.0;
|
||||
|
||||
while val < 0.0 + 1000000.0 * f64::EPSILON {
|
||||
let a = PathSeg::Cubic(CubicBez::new(
|
||||
Point::new(0.0 + val, 0.0),
|
||||
Point::new(0.25 + val, 0.661437827766),
|
||||
Point::new(0.75 + val, 0.968245836552),
|
||||
Point::new(1.0 + val, 1.0),
|
||||
));
|
||||
let b = PathSeg::Cubic(CubicBez::new(
|
||||
Point::new(0.0, 1.0),
|
||||
Point::new(0.25, 0.968245836552),
|
||||
Point::new(0.75, 0.661437827766),
|
||||
Point::new(1.0, 0.0),
|
||||
));
|
||||
let aex = SubCurve::subcurve_extrema(&a);
|
||||
let bex = SubCurve::subcurve_extrema(&b);
|
||||
let a_sub = SubCurve::new(&a, &aex);
|
||||
let b_sub = SubCurve::new(&b, &bex);
|
||||
let mut intersections = Vec::new();
|
||||
path_intersections(&a_sub, &b_sub, &mut intersections);
|
||||
|
||||
writeln!(
|
||||
&mut test_results_intersection,
|
||||
"{:?}, {:?}, {:?}, {:?}",
|
||||
val,
|
||||
intersections.first().unwrap().point.x,
|
||||
intersections.first().unwrap().quality,
|
||||
intersections.first().unwrap().t_a
|
||||
)
|
||||
.expect("");
|
||||
|
||||
val += f64::EPSILON;
|
||||
}
|
||||
}
|
||||
|
||||
#[ignore]
|
||||
#[test]
|
||||
fn test_test_dir() {
|
||||
use std::env::current_dir;
|
||||
println!("{:?}", current_dir());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user