mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 22:28:10 +08:00
Add shape fill overlays when closing a path (Pen tool) or filling it (Fill tool) (#2521)
* Make the Pen tool show a path being closed by drawing a filled overlay when hovering the endpoint * Add to_css to color.rs * Check before unwrapping layer * Close if in the right place * Fix typo * Format code * Support discontinuous paths for closing preview * Code review * Denser fill lines * Fill tool preview with strip lines only and revert pen shape-closing opacity * Small adjustments to fill preview * Fix line width of fill preview * Use a pattern to preview the fill tool and fix canvas clearing * Update pattern * Simplify code * Format code * Use secondary color to preview fill if shift is pressed * Code review --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
@@ -553,6 +553,94 @@ impl RegionDomain {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct HalfEdge {
|
||||
pub id: SegmentId,
|
||||
pub start: usize,
|
||||
pub end: usize,
|
||||
pub reverse: bool,
|
||||
}
|
||||
|
||||
impl HalfEdge {
|
||||
pub fn new(id: SegmentId, start: usize, end: usize, reverse: bool) -> Self {
|
||||
Self { id, start, end, reverse }
|
||||
}
|
||||
|
||||
pub fn reversed(&self) -> Self {
|
||||
Self {
|
||||
id: self.id,
|
||||
start: self.start,
|
||||
end: self.end,
|
||||
reverse: !self.reverse,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn normalize_direction(&self) -> Self {
|
||||
if self.reverse {
|
||||
Self {
|
||||
id: self.id,
|
||||
start: self.end,
|
||||
end: self.start,
|
||||
reverse: false,
|
||||
}
|
||||
} else {
|
||||
*self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct FoundSubpath {
|
||||
pub edges: Vec<HalfEdge>,
|
||||
}
|
||||
|
||||
impl FoundSubpath {
|
||||
pub fn new(segments: Vec<HalfEdge>) -> Self {
|
||||
Self { edges: segments }
|
||||
}
|
||||
|
||||
pub fn endpoints(&self) -> Option<(&HalfEdge, &HalfEdge)> {
|
||||
match (self.edges.first(), self.edges.last()) {
|
||||
(Some(first), Some(last)) => Some((first, last)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push(&mut self, segment: HalfEdge) {
|
||||
self.edges.push(segment);
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, index: usize, segment: HalfEdge) {
|
||||
self.edges.insert(index, segment);
|
||||
}
|
||||
|
||||
pub fn extend(&mut self, segments: impl IntoIterator<Item = HalfEdge>) {
|
||||
self.edges.extend(segments);
|
||||
}
|
||||
|
||||
pub fn splice<I>(&mut self, range: std::ops::Range<usize>, replace_with: I)
|
||||
where
|
||||
I: IntoIterator<Item = HalfEdge>,
|
||||
{
|
||||
self.edges.splice(range, replace_with);
|
||||
}
|
||||
|
||||
pub fn is_closed(&self) -> bool {
|
||||
match (self.edges.first(), self.edges.last()) {
|
||||
(Some(first), Some(last)) => first.start == last.end,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_segment(segment: HalfEdge) -> Self {
|
||||
Self { edges: vec![segment] }
|
||||
}
|
||||
|
||||
pub fn contains(&self, segment_id: SegmentId) -> bool {
|
||||
self.edges.iter().any(|s| s.id == segment_id)
|
||||
}
|
||||
}
|
||||
|
||||
impl VectorData {
|
||||
/// Construct a [`bezier_rs::Bezier`] curve spanning from the resolved position of the start and end points with the specified handles.
|
||||
fn segment_to_bezier_with_index(&self, start: usize, end: usize, handles: bezier_rs::BezierHandles) -> bezier_rs::Bezier {
|
||||
@@ -592,6 +680,114 @@ impl VectorData {
|
||||
.map(to_bezier)
|
||||
}
|
||||
|
||||
pub fn auto_join_paths(&self) -> Vec<FoundSubpath> {
|
||||
let segments = self.segment_domain.iter().map(|(id, start, end, _)| HalfEdge::new(id, start, end, false));
|
||||
|
||||
let mut paths: Vec<FoundSubpath> = Vec::new();
|
||||
let mut current_path: Option<&mut FoundSubpath> = None;
|
||||
let mut previous: Option<(usize, usize)> = None;
|
||||
|
||||
// First pass. Generates subpaths from continuous segments.
|
||||
for seg_ref in segments {
|
||||
let (start, end) = (seg_ref.start, seg_ref.end);
|
||||
|
||||
if previous.is_some_and(|(_, prev_end)| start == prev_end) {
|
||||
if let Some(path) = current_path.as_mut() {
|
||||
path.push(seg_ref);
|
||||
}
|
||||
} else {
|
||||
paths.push(FoundSubpath::from_segment(seg_ref));
|
||||
current_path = paths.last_mut();
|
||||
}
|
||||
|
||||
previous = Some((start, end));
|
||||
}
|
||||
|
||||
// Second pass. Try to join paths together.
|
||||
let mut joined_paths = Vec::new();
|
||||
|
||||
loop {
|
||||
let mut prev_index: Option<usize> = None;
|
||||
let original_len = paths.len();
|
||||
|
||||
for current in paths.into_iter() {
|
||||
// If there's no previous subpath, start a new one
|
||||
if prev_index.is_none() {
|
||||
joined_paths.push(current);
|
||||
prev_index = Some(joined_paths.len() - 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
let prev = &mut joined_paths[prev_index.unwrap()];
|
||||
|
||||
// Compare segment connections
|
||||
let (prev_first, prev_last) = prev.endpoints().unwrap();
|
||||
let (cur_first, cur_last) = current.endpoints().unwrap();
|
||||
|
||||
// Join paths if the endpoints connect
|
||||
if prev_last.end == cur_first.start {
|
||||
prev.edges.extend(current.edges.into_iter().map(|s| s.normalize_direction()));
|
||||
} else if prev_first.start == cur_last.end {
|
||||
prev.edges.splice(0..0, current.edges.into_iter().rev().map(|s| s.normalize_direction()));
|
||||
} else if prev_last.end == cur_last.end {
|
||||
prev.edges.extend(current.edges.into_iter().rev().map(|s| s.reversed().normalize_direction()));
|
||||
} else if prev_first.start == cur_first.start {
|
||||
prev.edges.splice(0..0, current.edges.into_iter().map(|s| s.reversed().normalize_direction()));
|
||||
} else {
|
||||
// If not connected, start a new subpath
|
||||
joined_paths.push(current);
|
||||
prev_index = Some(joined_paths.len() - 1);
|
||||
}
|
||||
}
|
||||
|
||||
// If no paths were joined in this pass, we're done
|
||||
if joined_paths.len() == original_len {
|
||||
return joined_paths;
|
||||
}
|
||||
|
||||
// Repeat pass with newly joined paths
|
||||
paths = joined_paths;
|
||||
joined_paths = Vec::new();
|
||||
}
|
||||
}
|
||||
|
||||
/// Construct a [`bezier_rs::Bezier`] curve from an iterator of segments with (handles, start point, end point) independently of discontinuities.
|
||||
pub fn subpath_from_segments_ignore_discontinuities(&self, segments: impl Iterator<Item = (bezier_rs::BezierHandles, usize, usize)>) -> Option<bezier_rs::Subpath<PointId>> {
|
||||
let mut first_point = None;
|
||||
let mut groups = Vec::new();
|
||||
let mut last: Option<(usize, bezier_rs::BezierHandles)> = None;
|
||||
|
||||
for (handle, start, end) in segments {
|
||||
first_point = Some(first_point.unwrap_or(start));
|
||||
|
||||
groups.push(bezier_rs::ManipulatorGroup {
|
||||
anchor: self.point_domain.positions()[start],
|
||||
in_handle: last.and_then(|(_, handle)| handle.end()),
|
||||
out_handle: handle.start(),
|
||||
id: self.point_domain.ids()[start],
|
||||
});
|
||||
|
||||
last = Some((end, handle));
|
||||
}
|
||||
|
||||
let closed = groups.len() > 1 && last.map(|(point, _)| point) == first_point;
|
||||
|
||||
if let Some((end, last_handle)) = last {
|
||||
if closed {
|
||||
groups[0].in_handle = last_handle.end();
|
||||
} else {
|
||||
groups.push(bezier_rs::ManipulatorGroup {
|
||||
anchor: self.point_domain.positions()[end],
|
||||
in_handle: last_handle.end(),
|
||||
out_handle: None,
|
||||
id: self.point_domain.ids()[end],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Some(bezier_rs::Subpath::new(groups, closed))
|
||||
}
|
||||
|
||||
/// Construct a [`bezier_rs::Bezier`] curve from an iterator of segments with (handles, start point, end point). Returns None if any ids are invalid or if the segments are not continuous.
|
||||
fn subpath_from_segments(&self, segments: impl Iterator<Item = (bezier_rs::BezierHandles, usize, usize)>) -> Option<bezier_rs::Subpath<PointId>> {
|
||||
let mut first_point = None;
|
||||
@@ -670,12 +866,12 @@ impl VectorData {
|
||||
|
||||
/// Construct a [`bezier_rs::Bezier`] curve for stroke.
|
||||
pub fn stroke_bezier_paths(&self) -> impl Iterator<Item = bezier_rs::Subpath<PointId>> {
|
||||
self.build_stroke_path_iter().into_iter().map(|(group, closed)| bezier_rs::Subpath::new(group, closed))
|
||||
self.build_stroke_path_iter().map(|(group, closed)| bezier_rs::Subpath::new(group, closed))
|
||||
}
|
||||
|
||||
/// Construct a [`kurbo::BezPath`] curve for stroke.
|
||||
pub fn stroke_bezpath_iter(&self) -> impl Iterator<Item = kurbo::BezPath> {
|
||||
self.build_stroke_path_iter().into_iter().map(|(group, closed)| {
|
||||
self.build_stroke_path_iter().map(|(group, closed)| {
|
||||
let mut bezpath = kurbo::BezPath::new();
|
||||
let mut out_handle;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user