Boolean shape operations (#470)

* added BooleanUnion message for select tool, which is sent by the BooleanUnion button

* select to dispatches a BooleanUnion message
   - as far as I can tell the selected layers aren't stored anywhere

* Added neccesary messages and functions for boolean operation

* Intersection code, as yet untested
does not compile

* Updated intersection algorithm

* Fixed shapes_as_seen
   - should not be effected by root transform, should be effected by the shape transform
Working line intercepts

* updated intersection algorithm

* added SubCurve struct, to reduce creation of new PathSegs and more efficiently calculate bounding boxes

* intersection algorithm modifications

* changed sub_curve to do less copying

* intersection algorithm working for ellipses
   - idk why though, the algorithm isn't finding close, but nearby intersections

* Code for the sub-shape identification pieces of boolean operation algorithm

* cycle direction calculations
boolean_operation code

* final touches (a.k.a. the very begining touches before the debugging)

* removed intersection testing code to draw intersects
added code to add new shapes to document

* Bug Fix: Cycle needs to track egde origin
Bug Fix: vertex markers
Working for simple shapes

* Bug Fix: multiple intersections in same PathSeg

* Bug Fix: compare the absolute value of area magnitudes

* Bug Fix: subdivision algorithm

* cargo fmt

* comments

* Bug Fix: Difference and Subtraction operations

* simplified overlap

* changed shapes_as_seen to transformed_shapes
    - modified function to use existing code for multiplying transforms and traversing layer tree

removed shape_as_seen, helper function to shapes_as_seen

* Bug Fix: selected layers must be sorted

* Changed SubFront to SubtractFront and SubBack to SubtractBack

* moved F64PRECISION to graphene::consts

* Best practices changes
intersection algorithm changes

* Added BooleanOperationError

* modified intersection algorithm to use more dynamic thresholds

* intersection algorithm modification

* Added "do_if"

* Bug Fix: properly subdivide segments with multiple intersections

* Added tests for intersection algorithm

* restructured flow control in intersection algorithm
restructured intersection algorithm to modify single vector
    - should have done this yeeeaars ago

* Shapes will have the pathstyle of alpha

* collect_shapes now uses closure

* BugFix: fixed PathGraph:get_cycles, prevent multicounting cycles

* Added boolean_ops::reverse_path function

* Curves are now reversed before intersections are found

* cleanup

* BugFix: subfront and subback chose wrong shape pathstyle
BugFix: path concat should remove internal movetos and closepaths

* BugFix: prevent movetos from being added due to intersect imprecision

* Changed intersection quality threshhold to CURVE_FIDELITY

* Added functions for cubic/quadratic roots
!Does not compile!

* Added special case algorithms for line-curve intersections
Added tests for cubic root algorithm

* changed line_curve_intersections structure

* Handle intersecting shape without curve intersection case

* Behavior for SubtratFront, SubtractBack, Union, and Intersection for the no-intersect cases

* reformatted PathGraph::add_edges_from_path
Fixed bugs involving closepaths and subpaths

* Bug fix: fix refactoring error

* Bug fix: don't consider 0 length cycles
Bug Fix: dummy vertices are not intersections

* the function document_message_handler::sort_layers sorts the layers from bottom to top
    - we want the reverse

* Bug Fix: ClosePath must be appended to shapes constructed from cycle
    - By default PathGraph edges are not closed.
    - When the input shape was not closed there would be multiple unclosed paths in the resulting shape, causing bugs

* close_path now closes all subpaths in a path
    - if a shape has any subpath which is not closed boolean operations are undefined
        (Also PathGraph::add_edges_from_path breaks when the path isn't closed)

* clean up

* removed unused function "intersectoin_candidates"

* comments

* removed duplicate check for valid intersection in horizontal_ray_cast

* Temporary fix for ray casting

* it wouldn't build cause one of these ->' pesky things was hanging around in the wrong neighborhood

* ignoring intersection test cases because they do exact fp comparison

* Some spelling fixes and abbreviation burnination (more needed)

* spelling fixes
close_path bug

* Fixed: local extrema were not being properly filtered out for subcurves
Fixed: reversing PathSeg removes any closepaths

* spelling

* Code review pass

* partial implementation overlapping identical curves

* Untested implementation for test for overlapping curves
    - Test is not yet used in intersection algorithm

* -Removed PathGraph::intersect()

* readability improvements
changed line_t_value to be more forgiving to error

* Bug Fix: match_control_polygon didn't properly compare different degree polygons

* Added colinear() function
Added projection_on_line() function

* BugFix project_onto_line

* removed extra log

* rust fmt

Co-authored-by: Keavon Chambers <keavon@keavon.com>
Co-authored-by: TrueDoctor <dennis@kobert.dev>
This commit is contained in:
caleb 2022-02-12 09:30:08 -08:00 committed by Keavon Chambers
parent 91e4201cb1
commit f9f66db5a2
10 changed files with 1647 additions and 8 deletions

View File

@ -2,6 +2,7 @@ use super::layer_panel::LayerMetadata;
use super::utility_types::{AlignAggregate, AlignAxis, FlipAxis}; use super::utility_types::{AlignAggregate, AlignAxis, FlipAxis};
use crate::message_prelude::*; use crate::message_prelude::*;
use graphene::boolean_ops::BooleanOperation as BooleanOperationType;
use graphene::layers::blend_mode::BlendMode; use graphene::layers::blend_mode::BlendMode;
use graphene::layers::style::ViewMode; use graphene::layers::style::ViewMode;
use graphene::LayerId; use graphene::LayerId;
@ -41,6 +42,7 @@ pub enum DocumentMessage {
axis: AlignAxis, axis: AlignAxis,
aggregate: AlignAggregate, aggregate: AlignAggregate,
}, },
BooleanOperation(BooleanOperationType),
CommitTransaction, CommitTransaction,
CreateEmptyFolder { CreateEmptyFolder {
container_path: Vec<LayerId>, container_path: Vec<LayerId>,

View File

@ -741,6 +741,16 @@ impl MessageHandler<DocumentMessage, &InputPreprocessorMessageHandler> for Docum
responses.push_back(ToolMessage::DocumentIsDirty.into()); responses.push_back(ToolMessage::DocumentIsDirty.into());
} }
} }
BooleanOperation(op) => {
// convert Vec<&[LayerId]> to Vec<Vec<&LayerId>> because Vec<&[LayerId]> does not implement several traits (Debug, Serialize, Deserialize, ...) required by DocumentOperation enum
responses.push_back(
DocumentOperation::BooleanOperation {
operation: op,
selected: self.selected_layers_sorted().iter().map(|slice| (*slice).into()).collect(),
}
.into(),
);
}
CommitTransaction => (), CommitTransaction => (),
CreateEmptyFolder { mut container_path } => { CreateEmptyFolder { mut container_path } => {
let id = generate_uuid(); let id = generate_uuid();

View File

@ -11,7 +11,7 @@ use crate::message_prelude::*;
use crate::misc::{HintData, HintGroup, HintInfo, KeysGroup}; use crate::misc::{HintData, HintGroup, HintInfo, KeysGroup};
use crate::viewport_tools::snapping::SnapHandler; use crate::viewport_tools::snapping::SnapHandler;
use crate::viewport_tools::tool::{DocumentToolData, Fsm, ToolActionHandlerData, ToolType}; use crate::viewport_tools::tool::{DocumentToolData, Fsm, ToolActionHandlerData, ToolType};
use graphene::boolean_ops::BooleanOperation;
use graphene::document::Document; use graphene::document::Document;
use graphene::intersection::Quad; use graphene::intersection::Quad;
use graphene::layers::layer_info::LayerDataType; use graphene::layers::layer_info::LayerDataType;
@ -186,35 +186,35 @@ impl PropertyHolder for Select {
icon: "BooleanUnion".into(), icon: "BooleanUnion".into(),
tooltip: "Boolean Union".into(), tooltip: "Boolean Union".into(),
size: 24, size: 24,
on_update: WidgetCallback::new(|_| FrontendMessage::DisplayDialogComingSoon { issue: Some(197) }.into()), on_update: WidgetCallback::new(|_| DocumentMessage::BooleanOperation(BooleanOperation::Union).into()),
..IconButton::default() ..IconButton::default()
})), })),
WidgetHolder::new(Widget::IconButton(IconButton { WidgetHolder::new(Widget::IconButton(IconButton {
icon: "BooleanSubtractFront".into(), icon: "BooleanSubtractFront".into(),
tooltip: "Boolean Subtract Front".into(), tooltip: "Boolean Subtract Front".into(),
size: 24, size: 24,
on_update: WidgetCallback::new(|_| FrontendMessage::DisplayDialogComingSoon { issue: Some(197) }.into()), on_update: WidgetCallback::new(|_| DocumentMessage::BooleanOperation(BooleanOperation::SubtractFront).into()),
..IconButton::default() ..IconButton::default()
})), })),
WidgetHolder::new(Widget::IconButton(IconButton { WidgetHolder::new(Widget::IconButton(IconButton {
icon: "BooleanSubtractBack".into(), icon: "BooleanSubtractBack".into(),
tooltip: "Boolean Subtract Back".into(), tooltip: "Boolean Subtract Back".into(),
size: 24, size: 24,
on_update: WidgetCallback::new(|_| FrontendMessage::DisplayDialogComingSoon { issue: Some(197) }.into()), on_update: WidgetCallback::new(|_| DocumentMessage::BooleanOperation(BooleanOperation::SubtractBack).into()),
..IconButton::default() ..IconButton::default()
})), })),
WidgetHolder::new(Widget::IconButton(IconButton { WidgetHolder::new(Widget::IconButton(IconButton {
icon: "BooleanIntersect".into(), icon: "BooleanIntersect".into(),
tooltip: "Boolean Intersect".into(), tooltip: "Boolean Intersect".into(),
size: 24, size: 24,
on_update: WidgetCallback::new(|_| FrontendMessage::DisplayDialogComingSoon { issue: Some(197) }.into()), on_update: WidgetCallback::new(|_| DocumentMessage::BooleanOperation(BooleanOperation::Intersection).into()),
..IconButton::default() ..IconButton::default()
})), })),
WidgetHolder::new(Widget::IconButton(IconButton { WidgetHolder::new(Widget::IconButton(IconButton {
icon: "BooleanDifference".into(), icon: "BooleanDifference".into(),
tooltip: "Boolean Difference".into(), tooltip: "Boolean Difference".into(),
size: 24, size: 24,
on_update: WidgetCallback::new(|_| FrontendMessage::DisplayDialogComingSoon { issue: Some(197) }.into()), on_update: WidgetCallback::new(|_| DocumentMessage::BooleanOperation(BooleanOperation::Difference).into()),
..IconButton::default() ..IconButton::default()
})), })),
WidgetHolder::new(Widget::Separator(Separator { WidgetHolder::new(Widget::Separator(Separator {

744
graphene/src/boolean_ops.rs Normal file
View File

@ -0,0 +1,744 @@
use crate::consts::{F64PRECISE, RAY_FUDGE_FACTOR};
use crate::intersection::{intersections, line_curve_intersections, valid_t, Intersect, Origin};
use crate::layers::simple_shape::Shape;
use crate::layers::style::PathStyle;
use kurbo::{BezPath, CubicBez, Line, ParamCurve, ParamCurveArclen, ParamCurveArea, ParamCurveExtrema, PathEl, PathSeg, Point, QuadBez, Rect};
use serde::{Deserialize, Serialize};
use std::fmt::{self, Debug, Formatter};
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq)]
pub enum BooleanOperation {
Union,
Difference,
Intersection,
SubtractFront,
SubtractBack,
}
#[derive(Debug, Clone, Copy, Deserialize, Serialize)]
pub enum BooleanOperationError {
InvalidSelection,
InvalidIntersections,
NoIntersections,
NothingDone, // Not necessarily an error
DirectionUndefined,
Unexpected, // For debugging, when complete nothing should be unexpected
}
/// A simple and idiomatic way to write short "if let Some(_)" statements which do nothing in the None case
macro_rules! do_if {
($option:expr, $name:ident{$todo:expr}) => {
if let Some($name) = $option {
$todo
}
};
}
struct Edge {
pub from: Origin,
pub destination: usize,
pub curve: BezPath,
}
impl Debug for Edge {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str(format!("\n To: {}, Type: {:?}", self.destination, self.from).as_str())?;
f.write_str(format!(" {:?}", self.curve).as_str())
}
}
struct Vertex {
pub intersect: Intersect,
pub edges: Vec<Edge>,
}
impl Debug for Vertex {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str(format!("\n Intersect@ {:?}", self.intersect.point).as_str())?;
f.debug_list().entries(self.edges.iter()).finish()
}
}
#[derive(PartialEq, Eq, Clone, Copy, Debug)]
enum Direction {
Ccw,
Cw,
}
/// Behavior: Intersection and Union cases are distinguished between by cycle area magnitude.
/// This only affects shapes whose intersection is a single shape, and the intersection is similarly sized to the union.
/// Can be solved by first computing at low accuracy, and if the values are close recomputing.
#[derive(Clone)]
struct Cycle {
vertices: Vec<(usize, Origin)>,
direction: Option<Direction>,
area: f64,
}
impl Cycle {
pub fn new(start_vertex_index: usize, edge_origin: Origin) -> Self {
Cycle {
vertices: vec![(start_vertex_index, edge_origin)],
direction: None,
area: 0.0,
}
}
/// Returns true when the cycle is complete, a cycle is complete when it revisits its first vertex where edge is the edge traversed in order to get to vertex.
/// For purposes of computing direction this function assumes vertices are traversed in order
fn extend(&mut self, vertex: usize, edge_origin: Origin, edge_curve: &BezPath) -> bool {
self.vertices.push((vertex, edge_origin));
self.area += path_area(edge_curve);
vertex == self.vertices[0].0
}
/// Returns number of vertices == number of edges in cycle.
fn len(&self) -> usize {
self.vertices.len() - 1
}
pub fn prev_edge_origin(&self) -> Origin {
self.vertices.last().unwrap().1
}
pub fn prev_vertex(&self) -> usize {
self.vertices.last().unwrap().0
}
pub fn vertices(&self) -> &Vec<(usize, Origin)> {
&self.vertices
}
pub fn area(&self) -> f64 {
self.area
}
pub fn direction(&mut self) -> Result<Direction, BooleanOperationError> {
match self.direction {
Some(direction) => Ok(direction),
None => {
if self.area > 0.0 {
self.direction = Some(Direction::Ccw);
Ok(Direction::Ccw)
} else if self.area < 0.0 {
self.direction = Some(Direction::Cw);
Ok(Direction::Cw)
} else {
Err(BooleanOperationError::DirectionUndefined)
}
}
}
}
/// If the path is empty (has no segments), the function `Err`s.
/// If the path crosses itself, the computed direction may (or probably will) be wrong, on account of it not really being defined.
pub fn direction_for_path(path: &BezPath) -> Result<Direction, BooleanOperationError> {
let mut area = 0.0;
path.segments().for_each(|path_segment| area += path_segment.signed_area());
if area > 0.0 {
Ok(Direction::Ccw)
} else if area < 0.0 {
Ok(Direction::Cw)
} else {
Err(BooleanOperationError::DirectionUndefined)
}
}
}
/// Optimization: store computed segment bounding boxes, or even edge bounding boxes to prevent recomputation.
#[derive(Debug)]
struct PathGraph {
vertices: Vec<Vertex>,
}
/// # Boolean Operation Algorithm
/// `PathGraph` represents a directional graph with edges "colored" by `Origin`.
/// Each edge also represents a portion of a visible shape.
/// Has somewhat (totally?) undefined behavior when shapes have self intersections.
impl PathGraph {
pub fn from_paths(alpha: &BezPath, beta: &BezPath) -> Result<PathGraph, BooleanOperationError> {
// TODO: check for closed paths somewhere, maybe here?
let mut new = PathGraph {
vertices: intersections(alpha, beta).into_iter().map(|i| Vertex { intersect: i, edges: Vec::new() }).collect(),
};
// We only consider graphs with even numbers of intersections.
// An odd number of intersections occurs when either:
// 1. There exists a tangential intersection (which shouldn't affect boolean ops)
// 2. The algorithm has found an extra intersection or missed an intersection
if new.size() == 0 {
return Err(BooleanOperationError::NoIntersections);
}
if new.size() % 2 != 0 {
return Err(BooleanOperationError::InvalidIntersections);
}
new.add_edges_from_path(alpha, Origin::Alpha);
new.add_edges_from_path(beta, Origin::Beta);
// log::debug!("size: {}, {:?}", new.size(), new);
Ok(new)
}
// TODO: NOTE: about intersection time_val order
/// Expects `path` (and all subpaths in `path`) to be closed.
/// # Panics
/// This function panics when `path` is empty.
fn add_edges_from_path(&mut self, path: &BezPath, origin: Origin) {
struct AlgorithmState {
//current_start holds the index of the vertex the current edge is starting from
current_start: Option<usize>,
current: Vec<PathSeg>,
// in order to iterate through once, store information for incomplete first edge
beginning: Vec<PathSeg>,
start_index: Option<usize>,
// seg index != el_index
seg_index: i32,
}
impl AlgorithmState {
fn new() -> Self {
AlgorithmState {
current_start: None,
current: Vec::new(),
beginning: Vec::new(),
start_index: None,
seg_index: 0,
}
}
fn reset(&mut self) {
self.current_start = None;
self.current = Vec::new();
self.beginning = Vec::new();
self.start_index = None;
}
fn advance_by_seg(&mut self, graph: &mut PathGraph, seg: PathSeg, origin: Origin) {
let (vertex_ids, mut t_values) = graph.intersects_in_seg(self.seg_index, origin);
if !vertex_ids.is_empty() {
let subdivided = subdivide_path_seg(&seg, &mut t_values);
for (vertex_id, sub_seg) in vertex_ids.into_iter().zip(subdivided.iter()) {
match self.current_start {
Some(index) => {
do_if!(sub_seg, end_of_edge { self.current.push(*end_of_edge)});
graph.add_edge(origin, index, vertex_id, self.current.clone());
self.current_start = Some(vertex_id);
self.current = Vec::new();
}
None => {
self.current_start = Some(vertex_id);
self.start_index = Some(vertex_id);
do_if!(sub_seg, end_of_beginning {self.beginning.push(*end_of_beginning)});
}
}
}
do_if!(subdivided.last().unwrap(), start_of_edge {self.current.push(*start_of_edge)});
} else {
match self.current_start {
Some(_) => self.current.push(seg),
None => self.beginning.push(seg),
}
}
self.seg_index += 1;
}
fn advance_by_closepath(&mut self, graph: &mut PathGraph, initial_point: &mut Point, origin: Origin) {
// *when a curve ends in a closepath and its start point does not equal its endpoint they should be connected with a line
let end_seg = match self.current.last() {
Some(seg) => seg,
None => self.beginning.last().unwrap(), // if both current and beginning are empty, the path is empty
};
let temp_copy = end_seg.end();
if temp_copy != *initial_point {
// a closepath implicitly defines a line which closes the path
self.advance_by_seg(graph, PathSeg::Line(Line { p0: temp_copy, p1: *initial_point }), origin);
}
// when a closepath is not followed by moveto, the next path starts at the end of the current path
*initial_point = temp_copy;
}
fn finalize_sub_path(&mut self, graph: &mut PathGraph, origin: Origin) {
if let (Some(current_start_), Some(start_index_)) = (self.current_start, self.start_index) {
//complete the current path
self.current.append(&mut self.beginning);
graph.add_edge(origin, current_start_, start_index_, self.current.clone());
} else {
//path has a subpath with no intersects
//create a dummy vertex with single edge which will be identified as cycle
let dumb_id = graph.add_vertex(Intersect::new(self.beginning[0].start(), 0.0, 0.0, -1, -1));
graph.add_edge(origin, dumb_id, dumb_id, self.beginning.clone());
}
}
}
let mut algorithm_state = AlgorithmState::new();
let mut initial_point = Point::new(0.0, 0.0);
for (el_index, el) in path.iter().enumerate() {
match el {
PathEl::MoveTo(p) => initial_point = p,
PathEl::ClosePath => {
algorithm_state.advance_by_closepath(self, &mut initial_point, origin);
algorithm_state.finalize_sub_path(self, origin);
algorithm_state.reset();
}
_ => {
algorithm_state.advance_by_seg(self, path.get_seg(el_index).unwrap(), origin);
}
}
}
}
fn add_vertex(&mut self, intersect: Intersect) -> usize {
self.vertices.push(Vertex { intersect, edges: Vec::new() });
self.vertices.len() - 1
}
fn add_edge(&mut self, origin: Origin, vertex: usize, destination: usize, curve: Vec<PathSeg>) {
let new_edge = Edge {
from: origin,
destination,
curve: BezPath::from_path_segments(curve.into_iter()),
};
self.vertices[vertex].edges.push(new_edge);
}
/// Returns the `Vertex` index and intersect `t_value` for all intersects in the segment identified by `seg_index` from `origin`.
/// sorts both lists for ascending t_value
fn intersects_in_seg(&self, seg_index: i32, origin: Origin) -> (Vec<usize>, Vec<f64>) {
let mut vertex_index = Vec::new();
let mut t_values = Vec::new();
for (v_index, vertex) in self.vertices.iter().enumerate() {
if vertex.intersect.segment_index(origin) == seg_index {
let next_t = vertex.intersect.t_value(origin);
let insert_index = match t_values.binary_search_by(|val: &f64| (*val).partial_cmp(&next_t).unwrap_or(std::cmp::Ordering::Less)) {
Ok(val) | Err(val) => val,
};
t_values.insert(insert_index, next_t);
vertex_index.insert(insert_index, v_index)
}
}
(vertex_index, t_values)
}
// Returns the number of vertices in the graph. This is equivalent to the number of intersections.
pub fn size(&self) -> usize {
self.vertices.len()
}
pub fn vertex(&self, index: usize) -> &Vertex {
&self.vertices[index]
}
/// A properly constructed `PathGraph` has no duplicate edges of the same `Origin`.
pub fn edge(&self, from: usize, to: usize, origin: Origin) -> Option<&Edge> {
// With a data structure restructure, or a hashmap, the `find()` here could be avoided, but it probably has a minimal performance impact
self.vertex(from).edges.iter().find(|edge| edge.destination == to && edge.from == origin)
}
/// Where a valid cycle alternates edge `Origin`.
/// Single edge/single vertex "dummy" cycles are also valid.
fn get_cycle(&self, cycle: &mut Cycle, marker_map: &mut Vec<u8>) {
if cycle.prev_edge_origin() == Origin::Alpha {
marker_map[cycle.prev_vertex()] |= 1;
} else {
marker_map[cycle.prev_vertex()] |= 2;
}
if let Some(next_edge) = self.vertex(cycle.prev_vertex()).edges.iter().find(|edge| edge.from != cycle.prev_edge_origin()) {
if !cycle.extend(next_edge.destination, next_edge.from, &next_edge.curve) {
self.get_cycle(cycle, marker_map)
}
}
}
pub fn get_cycles(&self) -> Vec<Cycle> {
let mut cycles = Vec::new();
let mut markers = Vec::new();
markers.resize(self.size(), 0);
self.vertices.iter().enumerate().for_each(|(vertex_index, _vertex)| {
if (markers[vertex_index] & 1) == 0 {
let mut temp = Cycle::new(vertex_index, Origin::Alpha);
self.get_cycle(&mut temp, &mut markers);
if temp.len() > 0 {
cycles.push(temp);
}
}
if (markers[vertex_index] & 2) == 0 {
let mut temp = Cycle::new(vertex_index, Origin::Beta);
self.get_cycle(&mut temp, &mut markers);
if temp.len() > 0 {
cycles.push(temp);
}
}
});
cycles
}
pub fn get_shape(&self, cycle: &Cycle, style: &PathStyle) -> Shape {
let mut curve = Vec::new();
let vertices = cycle.vertices();
for index in 1..vertices.len() {
// We expect the cycle to be valid so this should not panic
concat_paths(&mut curve, &self.edge(vertices[index - 1].0, vertices[index].0, vertices[index].1).unwrap().curve);
}
curve.push(PathEl::ClosePath);
Shape::from_bez_path(BezPath::from_vec(curve), *style, false)
}
}
/// If `t` is on `(0, 1)`, returns the split curve.
/// If `t` is outside `[0, 1]`, returns `(None, None)`
/// If `t` is 0 returns (None, `p`).
/// If `t` is 1 returns (`p`, None).
// TODO: test values outside 1
pub fn split_path_seg(p: &PathSeg, t: f64) -> (Option<PathSeg>, Option<PathSeg>) {
if t <= F64PRECISE {
if t >= 1.0 - F64PRECISE {
return (None, None);
}
return (Some(*p), None);
} else if t >= 1.0 - F64PRECISE {
return (None, Some(*p));
}
match p {
PathSeg::Cubic(cubic) => {
let a1 = Line::new(cubic.p0, cubic.p1).eval(t);
let a2 = Line::new(cubic.p1, cubic.p2).eval(t);
let a3 = Line::new(cubic.p2, cubic.p3).eval(t);
let b1 = Line::new(a1, a2).eval(t);
let b2 = Line::new(a2, a3).eval(t);
let c1 = Line::new(b1, b2).eval(t);
(
Some(PathSeg::Cubic(CubicBez { p0: cubic.p0, p1: a1, p2: b1, p3: c1 })),
Some(PathSeg::Cubic(CubicBez { p0: c1, p1: b2, p2: a3, p3: cubic.p3 })),
)
}
PathSeg::Quad(quad) => {
let b1 = Line::new(quad.p0, quad.p1).eval(t);
let b2 = Line::new(quad.p1, quad.p2).eval(t);
let c1 = Line::new(b1, b2).eval(t);
(
Some(PathSeg::Quad(QuadBez { p0: quad.p0, p1: b1, p2: c1 })),
Some(PathSeg::Quad(QuadBez { p0: c1, p1: b2, p2: quad.p2 })),
)
}
PathSeg::Line(line) => {
let split = line.eval(t);
(Some(PathSeg::Line(Line { p0: line.p0, p1: split })), Some(PathSeg::Line(Line { p0: split, p1: line.p1 })))
}
}
}
/// Splits `p` at each of `t_values`.
/// `t_values` should be sorted in ascending order.
/// The length of the returned `Vec` is always equal to `1 + t_values.len()`.
pub fn subdivide_path_seg(p: &PathSeg, t_values: &mut [f64]) -> Vec<Option<PathSeg>> {
let mut sub_segments = Vec::new();
let mut to_split = Some(*p);
let mut prev_split = 0.0;
for split in t_values {
if let Some(to_split_next) = to_split {
let (sub_seg, _to_split) = split_path_seg(&to_split_next, (*split - prev_split) / (1.0 - prev_split));
to_split = _to_split;
sub_segments.push(sub_seg);
prev_split = *split;
} else {
sub_segments.push(None);
}
}
sub_segments.push(to_split);
sub_segments
}
// TODO: check if shapes are filled
// TODO: Bug: shape with at least two subpaths and comprised of many unions sometimes has erroneous movetos embedded in edges
pub fn boolean_operation(select: BooleanOperation, mut alpha: Shape, mut beta: Shape) -> Result<Vec<Shape>, BooleanOperationError> {
if alpha.path.is_empty() || beta.path.is_empty() {
return Err(BooleanOperationError::InvalidSelection);
}
alpha.path = close_path(&alpha.path);
beta.path = close_path(&beta.path);
let beta_reverse = close_path(&reverse_path(&beta.path));
let alpha_dir = Cycle::direction_for_path(&alpha.path)?;
let beta_dir = Cycle::direction_for_path(&beta.path)?;
match select {
BooleanOperation::Union => {
match if beta_dir == alpha_dir {
PathGraph::from_paths(&alpha.path, &beta.path)
} else {
PathGraph::from_paths(&alpha.path, &beta_reverse)
} {
Ok(graph) => {
let mut cycles = graph.get_cycles();
// "extra calls to ParamCurveArea::area here"
let mut boolean_union = graph.get_shape(
cycles.iter().reduce(|max, cycle| if cycle.area().abs() >= max.area().abs() { cycle } else { max }).unwrap(),
&alpha.style,
);
for interior in collect_shapes(&graph, &mut cycles, |dir| dir != alpha_dir, |_| &alpha.style)? {
add_subpath(&mut boolean_union.path, interior.path);
}
Ok(vec![boolean_union])
}
Err(BooleanOperationError::NoIntersections) => {
// If shape is inside the other the Union is just the larger
// Check could also be done with area and single ray cast
if cast_horizontal_ray(point_on_curve(&beta.path), &alpha.path) % 2 != 0 {
Ok(vec![alpha])
} else if cast_horizontal_ray(point_on_curve(&alpha.path), &beta.path) % 2 != 0 {
beta.style = alpha.style;
Ok(vec![beta])
} else {
Err(BooleanOperationError::NothingDone)
}
}
Err(err) => Err(err),
}
}
BooleanOperation::Difference => {
let graph = if beta_dir != alpha_dir {
PathGraph::from_paths(&alpha.path, &beta.path)?
} else {
PathGraph::from_paths(&alpha.path, &beta_reverse)?
};
collect_shapes(&graph, &mut graph.get_cycles(), |_| true, |dir| if dir == alpha_dir { &alpha.style } else { &beta.style })
}
BooleanOperation::Intersection => {
match if beta_dir == alpha_dir {
PathGraph::from_paths(&alpha.path, &beta.path)
} else {
PathGraph::from_paths(&alpha.path, &beta_reverse)
} {
Ok(graph) => {
let mut cycles = graph.get_cycles();
// "extra calls to ParamCurveArea::area here"
cycles.remove(
cycles
.iter()
.enumerate()
.reduce(|(max_index, max), (index, cycle)| if cycle.area().abs() >= max.area().abs() { (index, cycle) } else { (max_index, max) })
.unwrap()
.0,
);
collect_shapes(&graph, &mut cycles, |dir| dir == alpha_dir, |_| &alpha.style)
}
Err(BooleanOperationError::NoIntersections) => {
// Check could also be done with area and single ray cast
if cast_horizontal_ray(point_on_curve(&beta.path), &alpha.path) % 2 != 0 {
beta.style = alpha.style;
Ok(vec![beta])
} else if cast_horizontal_ray(point_on_curve(&alpha.path), &beta.path) % 2 != 0 {
Ok(vec![alpha])
} else {
Err(BooleanOperationError::NothingDone)
}
}
Err(err) => Err(err),
}
}
BooleanOperation::SubtractBack => {
match if beta_dir != alpha_dir {
PathGraph::from_paths(&alpha.path, &beta.path)
} else {
PathGraph::from_paths(&alpha.path, &beta_reverse)
} {
Ok(graph) => collect_shapes(&graph, &mut graph.get_cycles(), |dir| dir != alpha_dir, |_| &beta.style),
Err(BooleanOperationError::NoIntersections) => {
if cast_horizontal_ray(point_on_curve(&alpha.path), &beta.path) % 2 != 0 {
add_subpath(&mut beta.path, if beta_dir == alpha_dir { reverse_path(&alpha.path) } else { alpha.path });
beta.style = alpha.style;
Ok(vec![beta])
} else {
Err(BooleanOperationError::NothingDone)
}
}
Err(err) => Err(err),
}
}
BooleanOperation::SubtractFront => {
match if beta_dir != alpha_dir {
PathGraph::from_paths(&alpha.path, &beta.path)
} else {
PathGraph::from_paths(&alpha.path, &beta_reverse)
} {
Ok(graph) => collect_shapes(&graph, &mut graph.get_cycles(), |dir| dir == alpha_dir, |_| &alpha.style),
Err(BooleanOperationError::NoIntersections) => {
if cast_horizontal_ray(point_on_curve(&beta.path), &alpha.path) % 2 != 0 {
add_subpath(&mut alpha.path, if beta_dir == alpha_dir { reverse_path(&beta.path) } else { beta.path });
Ok(vec![alpha])
} else {
Err(BooleanOperationError::NothingDone)
}
}
Err(err) => Err(err),
}
}
}
}
// TODO less hacky way to handle double counts on shared endpoints
// TODO check bounding boxes more rigorously
pub fn cast_horizontal_ray(mut from: Point, into: &BezPath) -> usize {
// In practice, this makes it less likely that a ray will intersect with shared point between two curves
from.y += RAY_FUDGE_FACTOR;
let ray = Line {
p0: from,
p1: Point {
x: from.x + 1.0,
y: from.y + RAY_FUDGE_FACTOR,
},
};
let mut intersects = Vec::new();
for ref seg in into.segments() {
if seg.bounding_box().x1 > from.x {
line_curve_intersections(&ray, seg, true, |_, b| valid_t(b), &mut intersects);
}
}
intersects.len()
}
/// Uses curve start point as point on the curve.
/// # Panics
/// This function panics if the `curve` is empty.
pub fn point_on_curve(curve: &BezPath) -> Point {
curve.segments().next().unwrap().start()
}
/// # Panics
/// This function panics if the curve has no `PathSeg`s.
pub fn bounding_box(curve: &BezPath) -> Rect {
curve
.segments()
.map(|seg| <PathSeg as ParamCurveExtrema>::bounding_box(&seg))
.reduce(|bounds, rect| bounds.union(rect))
.unwrap()
}
fn collect_shapes<'a, F, G>(graph: &PathGraph, cycles: &mut Vec<Cycle>, predicate: F, style: G) -> Result<Vec<Shape>, BooleanOperationError>
where
F: Fn(Direction) -> bool,
G: Fn(Direction) -> &'a PathStyle,
{
let mut shapes = Vec::new();
if cycles.is_empty() {
return Err(BooleanOperationError::Unexpected);
}
for cycle in cycles {
match cycle.direction() {
Ok(dir) => {
if predicate(dir) {
shapes.push(graph.get_shape(cycle, style(dir)));
}
}
Err(err) => return Err(err),
}
}
Ok(shapes)
}
pub fn reverse_path_segment(seg: &mut PathSeg) {
match seg {
PathSeg::Line(line) => std::mem::swap(&mut line.p0, &mut line.p1),
PathSeg::Quad(quad) => std::mem::swap(&mut quad.p0, &mut quad.p2),
PathSeg::Cubic(cubic) => {
std::mem::swap(&mut cubic.p0, &mut cubic.p3);
std::mem::swap(&mut cubic.p1, &mut cubic.p2);
}
}
}
/// Reverses `path` by reversing each `PathSeg`, and reversing the order of `PathSegs` within each subpath.
/// Note: a closed path might no longer be closed after applying this function.
pub fn reverse_path(path: &BezPath) -> BezPath {
let mut curve = Vec::new();
let mut temp = Vec::new();
let mut path_segments = path.segments();
for element in path.iter() {
match element {
PathEl::MoveTo(_) => {
curve.append(&mut temp.into_iter().rev().collect());
temp = Vec::new();
}
_ => {
if let Some(mut seg) = path_segments.next() {
reverse_path_segment(&mut seg);
temp.push(seg);
}
}
}
}
curve.append(&mut temp.into_iter().rev().collect());
log::debug!("{:?}", BezPath::from_path_segments(curve.clone().into_iter()));
BezPath::from_path_segments(curve.into_iter())
}
/// Close off all sub-paths in curve by inserting a `ClosePath` whenever a `MoveTo` is not preceded by one.
pub fn close_path(curve: &BezPath) -> BezPath {
let mut new = BezPath::new();
let mut path_closed_flag = true;
for el in curve.iter() {
match el {
PathEl::MoveTo(p) => {
if !path_closed_flag {
new.push(PathEl::ClosePath);
}
new.push(PathEl::MoveTo(p));
path_closed_flag = false;
}
PathEl::ClosePath => {
path_closed_flag = true;
new.push(PathEl::ClosePath);
}
element => {
new.push(element);
}
}
}
if !path_closed_flag {
new.push(PathEl::ClosePath);
}
new
}
/// Concatenate `b` to `a`, where `b` is not a new subpath but a continuation of `a`.
pub fn concat_paths(a: &mut Vec<PathEl>, b: &BezPath) {
if a.is_empty() {
a.append(&mut b.elements().to_vec());
return;
}
// Remove closepath
if let Some(PathEl::ClosePath) = a.last() {
a.remove(a.len() - 1);
}
// Skip initial `MoveTo`, which should be guaranteed to exist
b.iter().skip(1).for_each(|element| a.push(element));
}
/// Concatenate `b` to `a`, where `b` is a new subpath.
pub fn add_subpath(a: &mut BezPath, b: BezPath) {
b.into_iter().for_each(|el| a.push(el));
}
pub fn path_length(a: &BezPath, accuracy: Option<f64>) -> f64 {
let mut sum = 0.0;
// Computing arc length with `F64PRECISE` accuracy is probably ridiculous
match accuracy {
Some(val) => a.segments().for_each(|seg| sum += seg.arclen(val)),
None => a.segments().for_each(|seg| sum += seg.arclen(F64PRECISE)),
}
sum
}
pub fn path_area(a: &BezPath) -> f64 {
a.segments().fold(0.0, |mut area, seg| {
area += seg.signed_area();
area
})
}

View File

@ -3,3 +3,15 @@ use crate::color::Color;
// RENDERING // RENDERING
pub const LAYER_OUTLINE_STROKE_COLOR: Color = Color::BLACK; pub const LAYER_OUTLINE_STROKE_COLOR: Color = Color::BLACK;
pub const LAYER_OUTLINE_STROKE_WIDTH: f32 = 1.; pub const LAYER_OUTLINE_STROKE_WIDTH: f32 = 1.;
// BOOLEAN OPERATIONS
// Bezier curve intersection algorithm
pub const F64PRECISE: f64 = f64::EPSILON * 100.0; // for f64 comparisons, to allow for rounding error
pub const F64LOOSE: f64 = f64::EPSILON * 1000000.0; // == 0.0000000002220446049250313
// A bezier curve whose `available_precision()` is greater than CURVE_FIDELITY can be evaluated at least 10000 "unique" locations
pub const CURVE_FIDELITY: f64 = F64PRECISE * 100.0;
// In practice, this makes it less likely that a ray will intersect with a common anchor point between two curves
pub const RAY_FUDGE_FACTOR: f64 = 0.00001;

View File

@ -1,3 +1,4 @@
use crate::boolean_ops::boolean_operation;
use crate::intersection::Quad; use crate::intersection::Quad;
use crate::layers; use crate::layers;
use crate::layers::folder::Folder; use crate::layers::folder::Folder;
@ -8,6 +9,7 @@ use crate::layers::text::Text;
use crate::{DocumentError, DocumentResponse, Operation}; use crate::{DocumentError, DocumentResponse, Operation};
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
use kurbo::Affine;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::cmp::max; use std::cmp::max;
use std::collections::hash_map::DefaultHasher; use std::collections::hash_map::DefaultHasher;
@ -95,6 +97,25 @@ impl Document {
self.folder_mut(path)?.layer_mut(id).ok_or_else(|| DocumentError::LayerNotFound(path.into())) self.folder_mut(path)?.layer_mut(id).ok_or_else(|| DocumentError::LayerNotFound(path.into()))
} }
/// Returns vector `Shape`s for each specified in `paths`.
/// If any path is not a shape, or does not exist, `DocumentError::InvalidPath` is returned.
fn transformed_shapes(&self, paths: &[Vec<LayerId>]) -> Result<Vec<Shape>, DocumentError> {
let mut shapes: Vec<Shape> = Vec::new();
let undo_viewport = self.root.transform.inverse();
for path in paths {
match (self.multiply_transforms(path), &self.layer(path)?.data) {
(Ok(shape_transform), LayerDataType::Shape(shape)) => {
let mut new_shape = shape.clone();
new_shape.path.apply_affine(Affine::new((undo_viewport * shape_transform).to_cols_array()));
shapes.push(new_shape);
}
(Ok(_), _) => return Err(DocumentError::InvalidPath),
(Err(err), _) => return Err(err),
}
}
Ok(shapes)
}
pub fn common_layer_path_prefix<'a>(&self, layers: impl Iterator<Item = &'a [LayerId]>) -> &'a [LayerId] { pub fn common_layer_path_prefix<'a>(&self, layers: impl Iterator<Item = &'a [LayerId]>) -> &'a [LayerId] {
layers.reduce(|a, b| &a[..a.iter().zip(b.iter()).take_while(|&(a, b)| a == b).count()]).unwrap_or_default() layers.reduce(|a, b| &a[..a.iter().zip(b.iter()).take_while(|&(a, b)| a == b).count()]).unwrap_or_default()
} }
@ -531,6 +552,34 @@ impl Document {
self.set_layer(path, Layer::new(LayerDataType::Shape(Shape::poly_line(points, *style)), *transform), *insert_index)?; self.set_layer(path, Layer::new(LayerDataType::Shape(Shape::poly_line(points, *style)), *transform), *insert_index)?;
Some([vec![DocumentChanged, CreatedLayer { path: path.clone() }], update_thumbnails_upstream(path)].concat()) Some([vec![DocumentChanged, CreatedLayer { path: path.clone() }], update_thumbnails_upstream(path)].concat())
} }
Operation::BooleanOperation { operation, selected } => {
// TODO: proper difference
// TODO: proper style selection (done?)
// TODO: should generate symmetrical code
// TODO: Operations on any number of shapes
// TODO: boolean ops on any number of shapes
// TODO: handle overlapping identical curve case
// TODO: precision reached without intersection bug (maybe caused by separating a closed path, or dragging handles)
// TODO: click on shape should drag the shape
// TODO: add ability to undo
let mut responses = Vec::new();
if selected.len() > 1 && selected.len() < 3 {
// ? apparently `selected` should be reversed
let mut shapes = self.transformed_shapes(selected)?;
let mut shape_drain = shapes.drain(..).rev();
let new_shapes = boolean_operation(*operation, shape_drain.next().unwrap(), shape_drain.next().unwrap())?;
for path in selected {
self.delete(path)?;
responses.push(DocumentResponse::DeletedLayer { path: path.clone() })
}
for new_shape in new_shapes {
let new_id = self.add_layer(&[], Layer::new(LayerDataType::Shape(new_shape), DAffine2::IDENTITY.to_cols_array()), -1)?;
responses.push(DocumentResponse::CreatedLayer { path: vec![new_id] })
}
}
Some([vec![DocumentChanged, DocumentResponse::FolderChanged { path: vec![] }], responses].concat())
}
Operation::AddSpline { Operation::AddSpline {
path, path,
insert_index, insert_index,

View File

@ -1,4 +1,5 @@
use super::LayerId; use super::LayerId;
use crate::boolean_ops::BooleanOperationError;
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub enum DocumentError { pub enum DocumentError {
@ -11,3 +12,10 @@ pub enum DocumentError {
NotText, NotText,
InvalidFile(String), InvalidFile(String),
} }
// TODO: change how BooleanOperationErrors are handled
impl From<BooleanOperationError> for DocumentError {
fn from(err: BooleanOperationError) -> Self {
DocumentError::InvalidFile(format!("{:?}", err))
}
}

View File

@ -1,7 +1,14 @@
use glam::{DAffine2, DVec2}; use core::panic;
use kurbo::{BezPath, Line, PathSeg, Point, Shape};
use std::ops::Mul; use std::ops::Mul;
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};
#[derive(Debug, Clone, Default, Copy)] #[derive(Debug, Clone, Default, Copy)]
pub struct Quad([DVec2; 4]); pub struct Quad([DVec2; 4]);
@ -74,3 +81,804 @@ pub fn get_arbitrary_point_on_path(path: &BezPath) -> Option<Point> {
PathSeg::Cubic(cubic) => cubic.p0, PathSeg::Cubic(cubic) => cubic.p0,
}) })
} }
/// \/ \/
/// 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
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum Origin {
Alpha,
Beta,
}
impl std::ops::Not for Origin {
type Output = Self;
fn not(self) -> Self {
match self {
Origin::Alpha => Origin::Beta,
Origin::Beta => Origin::Alpha,
}
}
}
#[derive(Debug, PartialEq)]
pub struct Intersect {
pub point: Point,
pub t_a: f64,
pub t_b: f64,
pub a_seg_index: i32,
pub b_seg_index: i32,
// TODO: remove the `quality` field
pub quality: f64,
}
impl Intersect {
pub fn new(point: Point, t_a: f64, t_b: f64, a_seg_index: i32, b_seg_index: i32) -> Self {
Intersect {
point,
t_a,
t_b,
a_seg_index,
b_seg_index,
quality: -1.0,
}
}
pub fn add_index(&mut self, a_index: i32, b_index: i32) {
self.a_seg_index = a_index;
self.b_seg_index = b_index;
}
pub fn segment_index(&self, o: Origin) -> i32 {
match o {
Origin::Alpha => self.a_seg_index,
Origin::Beta => self.b_seg_index,
}
}
pub fn t_value(&self, o: Origin) -> f64 {
match o {
Origin::Alpha => self.t_a,
Origin::Beta => self.t_b,
}
}
}
impl From<(Point, f64, f64)> for Intersect {
fn from(place_time: (Point, f64, f64)) -> Self {
Intersect {
point: place_time.0,
t_a: place_time.1,
t_b: place_time.2,
a_seg_index: 0,
b_seg_index: 0,
quality: 0.0,
}
}
}
struct SubCurve<'a> {
pub curve: &'a PathSeg,
pub start_t: f64,
pub end_t: f64,
local: [Point; 2], // local endpoints
pub extrema: &'a Vec<(Point, f64)>,
}
impl<'a> SubCurve<'a> {
// TODO: Fix this Clippy lint error
pub fn new(parent: &'a PathSeg, extrema: &'a Vec<(Point, f64)>) -> Self {
SubCurve {
curve: parent,
start_t: 0.0,
end_t: 1.0,
local: [parent.eval(0.0), parent.eval(1.0)],
extrema,
}
}
fn bounding_box(&self) -> Rect {
let mut bound = Rect {
x0: self.start().x,
y0: self.start().y,
x1: self.end().x,
y1: self.end().y,
};
self.local
.iter()
.chain(
self.extrema
.iter()
.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| {
if p.x < bound.x0 {
bound.x0 = p.x;
}
if p.x > bound.x1 {
bound.x1 = p.x;
}
if p.y < bound.y0 {
bound.y0 = p.y;
}
if p.y > bound.y1 {
bound.y1 = p.y;
}
});
bound
}
fn available_precision(&self) -> f64 {
(self.start_t - self.end_t).abs()
}
/// 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) {
let split_t = self.start_t + t * (self.end_t - self.start_t);
(
SubCurve {
curve: self.curve,
start_t: self.start_t,
end_t: split_t,
local: [self.start(), self.curve.eval(split_t)],
extrema: self.extrema,
},
SubCurve {
curve: self.curve,
start_t: split_t,
end_t: self.end_t,
local: [self.curve.eval(split_t), self.end()],
extrema: self.extrema,
},
)
}
fn start(&self) -> Point {
self.local[0]
}
fn end(&self) -> Point {
self.local[1]
}
}
// 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?
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);
return;
}
// We are close enough to try linear approximation
if recursion < (1 << 10) as f64 {
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.quality = guess_quality(a.curve, b.curve, &cross);
// log::debug!("checking: {:?}", cross.quality);
if cross.quality <= CURVE_FIDELITY {
intersections.push(cross);
return;
}
// Eventually the points in the curve become too close together to split the curve meaningfully
// Also provides a base case and prevents infinite recursion
if a.available_precision() <= F64PRECISE || b.available_precision() <= F64PRECISE {
log::debug!("precision reached");
intersections.push(cross);
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 {
log::debug!("precision reached without finding intersect");
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);
}
}
pub fn line_curve_intersections<F>(line: &Line, curve: &PathSeg, is_line_a: bool, t_validate: F, intersections: &mut Vec<Intersect>)
where
F: Fn(f64, f64) -> bool,
{
if let (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 {
// Forced to construct a `Vec` here because match arms must return same type, and E0716
let roots = match curve {
PathSeg::Quad(quad) => Vec::from(quad_line_intersect(line, quad)),
PathSeg::Cubic(cubic) => Vec::from(cubic_line_intersect(line, cubic)),
_ => vec![], // Should never occur
};
intersections.extend(
roots
.iter()
.filter_map(|time_option| {
if let Some(time) = time_option {
let point = match curve {
PathSeg::Cubic(cubic) => cubic.eval(*time),
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
let line_time = projection_on_line(line, &point);
if !t_validate(line_time, *time) {
return None;
}
if is_line_a {
Some(Intersect::from((point, line_time, *time)))
} else {
Some(Intersect::from((point, *time, line_time)))
}
} else {
None
}
})
.collect::<Vec<Intersect>>(),
);
}
}
/// 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 {
let at_a = b.eval(guess.t_b);
let at_b = a.eval(guess.t_a);
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
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();
// 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();
match b_on_a_not_none + a_on_b_not_none {
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];
t1b = 0.0;
t2a = a_ts[1];
t2b = 1.0;
(*b, subdivide_path_seg(a, a_ts.as_mut_slice())[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];
t2a = 1.0;
t2b = b_ts[1];
(*a, subdivide_path_seg(a, b_ts.as_mut_slice())[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!(),
},
)
};
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]
}
}
_ => [None, None],
}
}
/// Returns true if the Bezier curves described by A and B have the same control polygon
/// TODO: test this
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);
if a_polygon.first().unwrap() == b_polygon.last().unwrap() && a_polygon.last().unwrap() == b_polygon.first().unwrap() {
b_polygon.reverse()
}
if a_polygon.len() == b_polygon.len() {
a_polygon.iter().eq(b_polygon.iter())
} else {
// A sneaky higher degree Bezier curve may pose as a lower degree one
let (a_ref, b_ref) = if a_polygon.len() < b_polygon.len() {
(&mut b_polygon, &mut a_polygon)
} else {
(&mut a_polygon, &mut b_polygon)
};
let mut a_iter = a_ref.iter();
for b_point in b_ref.iter() {
let a_point = a_iter.next().unwrap();
if *a_point != *b_point {
loop {
if let Some(a_line) = a_iter.next() {
if !colinear(&[a_point, b_point, a_line]) {
return false;
}
if *a_line == *b_point {
break;
}
} else {
return false;
}
}
}
}
true
}
}
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() {
return false;
}
}
true
}
pub fn get_control_polygon(a: &PathSeg) -> Vec<Point> {
match a {
PathSeg::Line(Line { p0, p1 }) => vec![*p0, *p1],
PathSeg::Quad(QuadBez { p0, p1, p2 }) => vec![*p0, *p1, *p2],
PathSeg::Cubic(CubicBez { p0, p1, p2, p3 }) => vec![*p0, *p1, *p2, *p3],
}
}
/// 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
pub fn point_t_value(a: &PathSeg, p: &Point) -> Option<f64> {
match a {
PathSeg::Line(line) => line_t_value(line, p),
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())))
.flatten()
}
PathSeg::Cubic(cubic) => {
let [mut p0, p1, p2, p3] = cubic_bezier_coefficients(cubic);
p0 -= p.to_vec2();
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())))
.flatten()
}
}
}
pub fn intersections(a: &BezPath, b: &BezPath) -> Vec<Intersect> {
// log::info!("{:?}", a.to_svg());
// log::info!("{:?}", b.to_svg());
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();
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 mut intersects = Vec::new();
path_intersections(&SubCurve::new(&a_seg, &a_extrema), &SubCurve::new(&b_seg, &b_extrema), 1.0, &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
});
}
})
});
// log::info!("{:?}", intersections);
intersections
}
/// Returns the intersection point as if lines extended forever.
pub fn line_intersect_point(a: &Line, b: &Line) -> Option<Point> {
line_intersection_unchecked(a, b).map(|intersect| intersect.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
}
} else {
None
}
}
/// Returns intersection point and `t` values, treating lines as rays.
pub fn line_intersection_unchecked(a: &Line, b: &Line) -> Option<Intersect> {
let slopes = DMat2::from_cols_array(&[(b.p1 - b.p0).x, (b.p1 - b.p0).y, (a.p0 - a.p1).x, (a.p0 - a.p1).y]);
if slopes.determinant() == 0.0 {
return None;
}
let t_values = slopes.inverse() * DVec2::new((a.p0 - b.p0).x, (a.p0 - 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
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)))
}
pub fn cubic_line_intersect(a: &Line, b: &CubicBez) -> [Option<f64>; 3] {
let l_y = a.p1.x - a.p0.x;
let l_x = a.p1.y - a.p0.y;
let bp0 = b.p0.to_vec2();
let bp1 = b.p1.to_vec2();
let bp2 = b.p2.to_vec2();
let bp3 = b.p3.to_vec2();
let c0 = bp0;
let c1 = -3.0 * bp0 + 3.0 * bp1;
let c2 = 3.0 * bp0 - 6.0 * bp1 + 3.0 * bp2;
let c3 = -1.0 * bp0 + 3.0 * bp1 - 3.0 * bp2 + bp3;
cubic_real_roots(
-a.p0.y * l_y + a.p0.x * l_x - l_x * c0.x + l_y * c0.y,
l_y * c1.y - l_x * c1.x,
l_y * c2.y - l_x * c2.x,
l_y * c3.y - l_x * c3.x,
)
}
pub fn quad_line_intersect(a: &Line, b: &QuadBez) -> [Option<f64>; 2] {
let l_y = a.p1.x - a.p0.x;
let l_x = a.p1.y - a.p0.y;
let bp0 = b.p0.to_vec2();
let bp1 = b.p1.to_vec2();
let bp2 = b.p2.to_vec2();
let c0 = bp0;
let c1 = -2.0 * bp0 + 2.0 * bp1;
let c2 = bp0 - 2.0 * bp1 + bp2;
quadratic_real_roots(-a.p0.y * l_y + a.p0.x * l_x - l_x * c0.x + l_y * c0.y, l_y * c1.y - l_x * c1.x, l_y * c2.y - l_x * c2.x)
}
/// 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
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;
a0 /= a3;
a1 /= a3;
a2 /= a3;
let q: f64 = a1 / 3.0 - a2 * a2 / 9.0;
let r: f64 = (a1 * a2 - 3.0 * a0) / 6.0 - a2 * a2 * a2 / 27.0;
let r2_q3 = r * r + q * q * q;
if r2_q3 > 0.0 {
#[allow(non_snake_case)] // Allow name `A` for consistency with algorithm
let A = (r.abs() + r2_q3.sqrt()).cbrt();
let t1 = match r {
r if r >= 0.0 => A - q / A,
r if r < 0.0 => q / A - A,
_ => 0.0, // Should never occur
};
[Some(t1 - a2 / 3.0), None, None]
} else {
let phi = match q > -F64PRECISE && q < F64PRECISE {
true => 0.0,
false => (r / (-q).powf(3.0 / 2.0)).acos() / 3.0,
};
[
Some(2.0 * (-q).sqrt() * (phi).cos() - a2 / 3.0),
Some(2.0 * (-q).sqrt() * (phi + 2.0 * PI_3).cos() - a2 / 3.0),
Some(2.0 * (-q).sqrt() * (phi - 2.0 * PI_3).cos() - a2 / 3.0),
]
}
}
/// 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();
let p2 = cubic.p2.to_vec2();
let p3 = cubic.p3.to_vec2();
let c0 = p0;
let c1 = -3.0 * p0 + 3.0 * p1;
let c2 = 3.0 * p0 - 6.0 * p1 + 3.0 * p2;
let c3 = -1.0 * p0 + 3.0 * p1 - 3.0 * p2 + p3;
[c0, c1, c2, c3]
}
/// Returns real roots to quadratic equation: `f(t) = a0 + t*a1 + t^2*a2`.
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 {
return [None, None];
}
[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]
pub fn quadratic_bezier_coefficients(quad: &QuadBez) -> [Vec2; 3] {
let p0 = quad.p0.to_vec2();
let p1 = quad.p1.to_vec2();
let p2 = quad.p2.to_vec2();
let c0 = p0;
let c1 = -2.0 * p0 + 2.0 * p1;
let c2 = p0 - 2.0 * p1 + p2;
[c0, c1, c2]
}
/// Returns root to linear equation: `f(t) = a0 + t*a1`.
pub fn linear_root(a0: f64, a1: f64) -> [Option<f64>; 1] {
if a1 == 0.0 {
return [None];
}
[Some(-a0 / a1)]
}
/// Returns `true` if rectangles overlap, even if either rectangle has 0 area.
/// Uses `kurbo::Rect{x0, y0, x1, y1}` where `x0 <= x1` and `y0 <= y1`.
pub fn overlap(a: &Rect, b: &Rect) -> bool {
a.x0 <= b.x1 && a.y0 <= b.y1 && b.x0 <= a.x1 && b.y0 <= a.y1
}
/// Tests if a `t` value belongs to `[0.0, 1.0)`.
/// Uses F64PRECISE to allow a slightly larger range of values.
pub fn valid_t(t: f64) -> bool {
t > -F64PRECISE && t < 1.0
}
/// 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.
mod tests {
#[allow(unused_imports)] // This import is used
use super::*;
/// Two intersect points, on different `PathSegs`.
#[ignore]
#[test]
fn curve_intersection_basic() {
let a =
BezPath::from_svg("M-739.7999877929688 -50.89999999999998L-676.7999877929688 -50.89999999999998L-676.7999877929688 27.100000000000023L-739.7999877929688 27.100000000000023Z").expect("");
let b = BezPath::from_svg("M-649.2999877929688 72.10000000000002L-694.7999877929688 72.10000000000002L-694.7999877929688 0.8222196224152754L-649.2999877929688 0.8222196224152754Z").expect("");
let expected = [
Intersect {
point: Point::new(-676.7999877929688, 0.8222196224152754),
t_a: 0.6631053797745545,
t_b: 0.3956043956043956,
a_seg_index: 1,
b_seg_index: 2,
quality: 0.0,
},
Intersect {
point: Point::new(-694.7999877929688, 27.10000000000003),
t_a: 0.2857142857142857,
t_b: 0.6313327906904278,
a_seg_index: 2,
b_seg_index: 1,
quality: 0.0,
},
];
let result = intersections(&a, &b);
assert_eq!(expected.len(), result.len());
assert!(expected.iter().zip(result.iter()).fold(true, |equal, (a, b)| equal && a == b));
let a =
BezPath::from_svg("M-663.1000244140627 -549.4740810512067C-663.1000244140627 -516.8197385387762 -690.6345122994688 -490.3481636282921 -724.6000244140627 -490.3481636282921C-758.5655365286565 -490.3481636282921 -786.1000244140627 -516.8197385387762 -786.1000244140627 -549.4740810512067C-786.1000244140627 -582.128423563637 -758.5655365286565 -608.5999984741211 -724.6000244140627 -608.5999984741211C-690.6345122994688 -608.5999984741211 -663.1000244140627 -582.128423563637 -663.1000244140627 -549.4740810512067").expect("");
let b = BezPath::from_svg("M-834.7843084184785 -566.2292363273158C-834.7843084184785 -597.2326143708982 -805.749982642916 -622.3658181414634 -769.9343267290242 -622.3658181414634C-734.1186708151323 -622.3658181414634 -705.0843450395697 -597.2326143708982 -705.0843450395697 -566.2292363273158C-705.0843450395697 -535.2258582837335 -734.1186708151323 -510.0926545131682 -769.9343267290242 -510.0926545131682C-805.749982642916 -510.0926545131682 -834.7843084184785 -535.2258582837334 -834.7843084184785 -566.2292363273158").expect("");
let expected = [
Intersect {
point: Point::new(-770.4753350264828, -510.09456728384305),
t_a: 0.5368149286026136,
t_b: 0.005039955230097687,
a_seg_index: 1,
b_seg_index: 3,
quality: 0.0,
},
Intersect {
point: Point::new(-727.3175070060661, -608.5433117814998),
t_a: 0.9731908875121124,
t_b: 0.45548363569548905,
a_seg_index: 2,
b_seg_index: 1,
quality: 0.0,
},
];
let result = intersections(&a, &b);
assert_eq!(expected.len(), result.len());
assert!(expected.iter().zip(result.iter()).fold(true, |equal, (a, b)| equal && a == b));
let a =
BezPath::from_svg("M-421.6225245705596 -963.1740648809906L-446.65763791855386 -1011.5169335848782L-496.72786461454245 -1011.5169335848782L-521.7629779625368 -963.1740648809906L-496.7278646145425 -914.831196177103L-446.6576379185539 -914.831196177103Z").expect("");
let b = BezPath::from_svg("M-561.0072096972251 -1026.4026766566521L-502.81748678026577 -1026.4026766566521L-502.81748678026577 -945.8843391320225L-561.0072096972251 -945.8843391320225Z")
.expect("");
let expected = [
Intersect {
point: Point::new(-502.81748678026577, -999.757857413672),
t_a: 0.24324324324304233,
t_b: 0.33091616223235865,
a_seg_index: 2,
b_seg_index: 1,
quality: 0.0,
},
Intersect {
point: Point::new(-512.8092221038916, -945.8843391320225),
t_a: 0.35764790573087535,
t_b: 0.1717096219530834,
a_seg_index: 3,
b_seg_index: 2,
quality: 0.0,
},
];
let result = intersections(&a, &b);
assert_eq!(expected.len(), result.len());
assert!(expected.iter().zip(result.iter()).fold(true, |equal, (a, b)| equal && a == b));
}
/// Intersect points at ends of `PathSegs`.
#[ignore]
#[test]
fn curve_intersection_seg_edges() {
let a =
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("");
let b = BezPath::from_svg("M-450.7808181070286 -146.42509665727817C-450.7808181070286 -185.38383768558714 -421.2406499166092 -216.96613737992166 -384.8010057614469 -216.96613737992166C-348.3613616062847 -216.96613737992166 -318.82119341586525 -185.38383768558714 -318.82119341586525 -146.4250966572782C-318.82119341586525 -107.46635562896924 -348.3613616062846 -75.88405593463473 -384.8010057614469 -75.88405593463472C-421.2406499166092 -75.8840559346347 -450.78081810702855 -107.46635562896921 -450.7808181070286 -146.42509665727817").expect("");
let expected = [
Intersect {
point: Point::new(-449.629331039312, -133.2349088577284),
t_a: 0.1383488820074267,
t_b: 0.8842879656175459,
a_seg_index: 1,
b_seg_index: 3,
quality: 0.00000000000002842170943040401,
},
Intersect {
point: Point::new(-355.5702650533912, -209.683276560014),
t_a: 0.9606918211578568,
t_b: 0.28804943846673475,
a_seg_index: 3,
b_seg_index: 1,
quality: 0.0,
},
];
let result = intersections(&a, &b);
assert_eq!(expected.len(), result.len());
assert!(expected.iter().zip(result.iter()).fold(true, |equal, (a, b)| equal && a == b));
}
#[test]
#[ignore]
fn cubic_roots_intersection() {
let roots = cubic_real_roots(1.5, 1.1, 3.6, 1.0);
assert_eq!(roots.iter().filter_map(|r| *r).last().unwrap(), -3.4063481215142195);
let roots = cubic_real_roots(-7.1, 1.1, 3.6, 1.0);
assert_eq!(roots.iter().filter_map(|r| *r).last().unwrap(), 1.115909058984805);
let roots = cubic_real_roots(-7.1, -9.5, -4.6, 1.0);
assert_eq!(roots.iter().filter_map(|r| *r).last().unwrap(), 6.289837710873103);
let roots = cubic_real_roots(-1.5, -3.3, 1.6, 1.0);
assert_eq!(roots, [Some(1.4330896870185468), Some(-2.636017358627879), Some(-0.3970723283906693)]);
// TODO: 3 real root case
// for root in roots {
// if let Some(num) = root {
// print!("{:.32}", num);
// }
// }
}
#[test]
#[ignore]
fn test_colinear() {
let p1 = Point { x: 0.0001, y: 3.0002 };
let p2 = Point { x: 0.029, y: 3.058 };
let p3 = Point { x: 100.237, y: 203.474 };
let p4 = Point { x: 720.297, y: 1443.594 };
assert!(colinear(&[&p1, &p2, &p3, &p4]));
}
}

View File

@ -1,3 +1,4 @@
pub mod boolean_ops;
pub mod color; pub mod color;
pub mod consts; pub mod consts;
pub mod document; pub mod document;

View File

@ -1,3 +1,4 @@
use crate::boolean_ops::BooleanOperation as BooleanOperationType;
use crate::color::Color; use crate::color::Color;
use crate::layers::blend_mode::BlendMode; use crate::layers::blend_mode::BlendMode;
use crate::layers::layer_info::Layer; use crate::layers::layer_info::Layer;
@ -96,6 +97,10 @@ pub enum Operation {
style: style::PathStyle, style: style::PathStyle,
closed: bool, closed: bool,
}, },
BooleanOperation {
operation: BooleanOperationType,
selected: Vec<Vec<LayerId>>,
},
DeleteLayer { DeleteLayer {
path: Vec<LayerId>, path: Vec<LayerId>,
}, },