mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-22 12:18:12 +08:00
+1

![dependabot[bot]](/assets/img/avatar_default.png)






0f36329811
* 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>
805 lines
26 KiB
Rust
805 lines
26 KiB
Rust
use crate::consts::F64PRECISE;
|
|
use crate::intersection::{intersections, line_curve_intersections, valid_t, Intersect, Origin};
|
|
use crate::layers::shape_layer::ShapeLayer;
|
|
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::cell::RefCell;
|
|
use std::fmt::{self, Debug, Formatter};
|
|
use std::mem::swap;
|
|
|
|
#[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
|
|
}
|
|
|
|
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 Point: {:?} Segment index of A: {:?}, Segment index of B: {:?} t value of A: {:?} t value of B: {:?}",
|
|
self.intersect.point,
|
|
self.intersect.segment_index(Origin::Alpha),
|
|
self.intersect.segment_index(Origin::Beta),
|
|
self.intersect.t_value(Origin::Alpha),
|
|
self.intersect.t_value(Origin::Beta),
|
|
)
|
|
.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> {
|
|
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) => {
|
|
sub_seg.map(|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);
|
|
sub_seg.map(|end_of_beginning| self.beginning.push(end_of_beginning));
|
|
}
|
|
}
|
|
}
|
|
subdivided.last().unwrap().map(|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 last_line = match self.current.last() {
|
|
Some(start_of_final_edge) => Line {
|
|
p0: start_of_final_edge.end(),
|
|
p1: *initial_point,
|
|
},
|
|
None => {
|
|
// When None occurs the current edge has been connected to a vertex.
|
|
// Either self.beginning is Some or None, if self.beginning is Some there may be a dangling edge to connect
|
|
// if self.beginning is None, the end of the current edge may not have closed the path
|
|
match self.beginning.last() {
|
|
Some(end_of_first_edge) => Line {
|
|
p0: end_of_first_edge.end(),
|
|
p1: *initial_point,
|
|
},
|
|
None => Line {
|
|
// should never panic, either a intersection has been encountered, so self.current_start is Some.
|
|
// or no vertex has been encountered so self.beginning.last() is Some
|
|
p0: graph.vertex(self.current_start.unwrap()).intersect.point,
|
|
p1: *initial_point,
|
|
},
|
|
}
|
|
}
|
|
};
|
|
if last_line.length() > F64PRECISE {
|
|
// A closepath implicitly defines a line which closes the path and the closepath line may contain intersections
|
|
self.advance_by_seg(graph, PathSeg::Line(last_line), origin);
|
|
}
|
|
}
|
|
|
|
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();
|
|
|
|
// All valid SVG paths start with a moveto, so this will always be initialized
|
|
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) -> ShapeLayer {
|
|
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);
|
|
ShapeLayer::from_bez_path(BezPath::from_vec(curve), style.clone(), 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)`.
|
|
pub fn split_path_seg(p: &PathSeg, t: f64) -> (Option<PathSeg>, Option<PathSeg>) {
|
|
if t <= -F64PRECISE || t >= 1.0 + F64PRECISE {
|
|
return (None, None);
|
|
}
|
|
if t <= F64PRECISE {
|
|
return (None, Some(*p));
|
|
}
|
|
if t >= 1.0 - F64PRECISE {
|
|
return (Some(*p), None);
|
|
}
|
|
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
|
|
}
|
|
|
|
pub fn composite_boolean_operation(mut select: BooleanOperation, shapes: &mut Vec<RefCell<ShapeLayer>>) -> Result<Vec<ShapeLayer>, BooleanOperationError> {
|
|
if select == BooleanOperation::SubtractBack {
|
|
select = BooleanOperation::SubtractFront;
|
|
let temp_len = shapes.len();
|
|
shapes.swap(0, temp_len - 1);
|
|
}
|
|
match select {
|
|
BooleanOperation::Union | BooleanOperation::Intersection => {
|
|
// We must attempt to union each shape with every other shape
|
|
let mut subject_idx = 0;
|
|
while subject_idx < shapes.len() {
|
|
let mut shape_idx = 0;
|
|
while shape_idx < shapes.len() && subject_idx < shapes.len() {
|
|
if shape_idx == subject_idx {
|
|
shape_idx += 1;
|
|
continue;
|
|
}
|
|
let partial_union = boolean_operation(select, &mut shapes[subject_idx].borrow_mut(), &mut shapes[shape_idx].borrow_mut());
|
|
match partial_union {
|
|
Ok(temp_union) => {
|
|
// The result of a successful union will be exactly one shape
|
|
shapes.push(RefCell::new(temp_union.into_iter().next().unwrap()));
|
|
shapes.swap_remove(subject_idx);
|
|
shapes.swap_remove(shape_idx);
|
|
}
|
|
Err(BooleanOperationError::NothingDone) => shape_idx += 1,
|
|
Err(err) => return Err(err),
|
|
}
|
|
}
|
|
subject_idx += 1;
|
|
}
|
|
Ok(shapes.iter().map(|ref_shape_layer| ref_shape_layer.borrow().clone()).collect())
|
|
}
|
|
BooleanOperation::SubtractFront => {
|
|
let mut result = vec![shapes[0].borrow().clone()];
|
|
for shape_idx in 1..shapes.len() {
|
|
let mut temp = Vec::new();
|
|
for mut partial in result {
|
|
match boolean_operation(select, &mut partial, &mut shapes[shape_idx].borrow_mut()) {
|
|
Ok(mut partial_result) => temp.append(&mut partial_result),
|
|
Err(BooleanOperationError::NothingDone) => temp.push(partial),
|
|
Err(err) => return Err(err),
|
|
}
|
|
}
|
|
result = temp; // This move should be done without copying
|
|
}
|
|
Ok(result)
|
|
}
|
|
BooleanOperation::Difference => {
|
|
let mut difference = Vec::new();
|
|
for shape_idx in 0..shapes.len() {
|
|
shapes.swap(0, shape_idx);
|
|
difference.append(&mut composite_boolean_operation(BooleanOperation::SubtractFront, shapes)?);
|
|
}
|
|
Ok(difference)
|
|
}
|
|
BooleanOperation::SubtractBack => unreachable!("composite boolean operation: unreachable subtract from back"),
|
|
}
|
|
}
|
|
|
|
// 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(mut select: BooleanOperation, alpha: &mut ShapeLayer, beta: &mut ShapeLayer) -> Result<Vec<ShapeLayer>, BooleanOperationError> {
|
|
if alpha.path.is_empty() || beta.path.is_empty() {
|
|
return Err(BooleanOperationError::InvalidSelection);
|
|
}
|
|
if select == BooleanOperation::SubtractBack {
|
|
select = BooleanOperation::SubtractFront;
|
|
swap(alpha, beta);
|
|
}
|
|
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.clone()])
|
|
} else if cast_horizontal_ray(point_on_curve(&alpha.path), &beta.path) % 2 != 0 {
|
|
beta.style = alpha.style.clone();
|
|
Ok(vec![beta.clone()])
|
|
} 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.clone();
|
|
Ok(vec![beta.clone()])
|
|
} else if cast_horizontal_ray(point_on_curve(&alpha.path), &beta.path) % 2 != 0 {
|
|
Ok(vec![alpha.clone()])
|
|
} else {
|
|
Err(BooleanOperationError::NothingDone)
|
|
}
|
|
}
|
|
Err(err) => Err(err),
|
|
}
|
|
}
|
|
BooleanOperation::SubtractBack => {
|
|
unreachable!("Boolean operation: unreachable subtract from back");
|
|
}
|
|
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.clone() });
|
|
Ok(vec![alpha.clone()])
|
|
} else {
|
|
Err(BooleanOperationError::NothingDone)
|
|
}
|
|
}
|
|
Err(err) => Err(err),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// TODO check bounding boxes more rigorously
|
|
pub fn cast_horizontal_ray(from: Point, into: &BezPath) -> usize {
|
|
let mut ray = PathSeg::Line(Line {
|
|
p0: from,
|
|
p1: Point { x: from.x + 1.0, y: from.y },
|
|
});
|
|
let mut intersects = Vec::new();
|
|
for ref mut seg in into.segments() {
|
|
if seg.bounding_box().x1 > from.x {
|
|
line_curve_intersections((&mut ray, seg), |_, 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<ShapeLayer>, 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)));
|
|
}
|
|
}
|
|
// Exclude cycles with 0.0 area
|
|
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());
|
|
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
|
|
})
|
|
}
|