mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-12-23 10:11:54 +00: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:
parent
a4a0e110d7
commit
3d37ef79ac
11 changed files with 350 additions and 17 deletions
|
|
@ -30,7 +30,9 @@ ron = ["dep:ron"]
|
|||
# Local dependencies
|
||||
graphite-proc-macros = { path = "../proc-macros" }
|
||||
graph-craft = { path = "../node-graph/graph-craft" }
|
||||
interpreted-executor = { path = "../node-graph/interpreted-executor", features = ["serde"] }
|
||||
interpreted-executor = { path = "../node-graph/interpreted-executor", features = [
|
||||
"serde",
|
||||
] }
|
||||
graphene-core = { path = "../node-graph/gcore" }
|
||||
graphene-std = { path = "../node-graph/gstd", features = ["serde"] }
|
||||
|
||||
|
|
@ -58,6 +60,9 @@ web-sys = { workspace = true, features = [
|
|||
"Element",
|
||||
"HtmlCanvasElement",
|
||||
"CanvasRenderingContext2d",
|
||||
"CanvasPattern",
|
||||
"OffscreenCanvas",
|
||||
"OffscreenCanvasRenderingContext2d",
|
||||
"TextMetrics",
|
||||
] }
|
||||
|
||||
|
|
|
|||
|
|
@ -282,6 +282,7 @@ pub fn input_mappings() -> Mapping {
|
|||
entry!(KeyDown(Enter); action_dispatch=SplineToolMessage::Confirm),
|
||||
//
|
||||
// FillToolMessage
|
||||
entry!(PointerMove; refresh_keys=[Shift], action_dispatch=FillToolMessage::PointerMove),
|
||||
entry!(KeyDown(MouseLeft); action_dispatch=FillToolMessage::FillPrimaryColor),
|
||||
entry!(KeyDown(MouseLeft); modifiers=[Shift], action_dispatch=FillToolMessage::FillSecondaryColor),
|
||||
entry!(KeyUp(MouseLeft); action_dispatch=FillToolMessage::PointerUp),
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ impl MessageHandler<OverlaysMessage, OverlaysMessageData<'_>> for OverlaysMessag
|
|||
|
||||
let [a, b, c, d, e, f] = DAffine2::from_scale(DVec2::splat(device_pixel_ratio)).to_cols_array();
|
||||
let _ = context.set_transform(a, b, c, d, e, f);
|
||||
context.clear_rect(0., 0., ipp.viewport_bounds.size().x, ipp.viewport_bounds.size().y);
|
||||
context.clear_rect(0., 0., canvas.width().into(), canvas.height().into());
|
||||
let _ = context.reset_transform();
|
||||
|
||||
if overlays_visible {
|
||||
|
|
|
|||
|
|
@ -8,10 +8,12 @@ use bezier_rs::{Bezier, Subpath};
|
|||
use core::borrow::Borrow;
|
||||
use core::f64::consts::{FRAC_PI_2, TAU};
|
||||
use glam::{DAffine2, DVec2};
|
||||
use graphene_core::Color;
|
||||
use graphene_core::renderer::Quad;
|
||||
use graphene_std::vector::{PointId, SegmentId, VectorData};
|
||||
use std::collections::HashMap;
|
||||
use wasm_bindgen::JsValue;
|
||||
use wasm_bindgen::{JsCast, JsValue};
|
||||
use web_sys::{OffscreenCanvas, OffscreenCanvasRenderingContext2d};
|
||||
|
||||
pub type OverlayProvider = fn(OverlayContext) -> Message;
|
||||
|
||||
|
|
@ -447,6 +449,7 @@ impl OverlayContext {
|
|||
self.end_dpi_aware_transform();
|
||||
}
|
||||
|
||||
/// Used by the Pen and Path tools to outline the path of the shape.
|
||||
pub fn outline_vector(&mut self, vector_data: &VectorData, transform: DAffine2) {
|
||||
self.start_dpi_aware_transform();
|
||||
|
||||
|
|
@ -465,6 +468,7 @@ impl OverlayContext {
|
|||
self.end_dpi_aware_transform();
|
||||
}
|
||||
|
||||
/// Used by the Pen tool in order to show how the bezier curve would look like.
|
||||
pub fn outline_bezier(&mut self, bezier: Bezier, transform: DAffine2) {
|
||||
self.start_dpi_aware_transform();
|
||||
|
||||
|
|
@ -493,7 +497,7 @@ impl OverlayContext {
|
|||
self.end_dpi_aware_transform();
|
||||
}
|
||||
|
||||
pub fn outline(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2) {
|
||||
fn push_path(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2) {
|
||||
self.start_dpi_aware_transform();
|
||||
|
||||
self.render_context.begin_path();
|
||||
|
|
@ -540,10 +544,63 @@ impl OverlayContext {
|
|||
}
|
||||
}
|
||||
|
||||
self.end_dpi_aware_transform();
|
||||
}
|
||||
|
||||
/// Used by the Select tool to outline a path selected or hovered.
|
||||
pub fn outline(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2) {
|
||||
self.push_path(subpaths, transform);
|
||||
|
||||
self.render_context.set_stroke_style_str(COLOR_OVERLAY_BLUE);
|
||||
self.render_context.stroke();
|
||||
}
|
||||
|
||||
self.end_dpi_aware_transform();
|
||||
/// Fills the area inside the path. Assumes `color` is in gamma space.
|
||||
/// Used by the Pen tool to show the path being closed.
|
||||
pub fn fill_path(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2, color: &str) {
|
||||
self.push_path(subpaths, transform);
|
||||
|
||||
self.render_context.set_fill_style_str(color);
|
||||
self.render_context.fill();
|
||||
}
|
||||
|
||||
/// Fills the area inside the path with a pattern. Assumes `color` is in gamma space.
|
||||
/// Used by the fill tool to show the area to be filled.
|
||||
pub fn fill_path_pattern(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2, color: &Color) {
|
||||
const PATTERN_WIDTH: usize = 4;
|
||||
const PATTERN_HEIGHT: usize = 4;
|
||||
|
||||
let pattern_canvas = OffscreenCanvas::new(PATTERN_WIDTH as u32, PATTERN_HEIGHT as u32).unwrap();
|
||||
let pattern_context: OffscreenCanvasRenderingContext2d = pattern_canvas
|
||||
.get_context("2d")
|
||||
.ok()
|
||||
.flatten()
|
||||
.expect("Failed to get canvas context")
|
||||
.dyn_into()
|
||||
.expect("Context should be a canvas 2d context");
|
||||
|
||||
// 4x4 pixels, 4 components (RGBA) per pixel
|
||||
let mut data = [0_u8; 4 * PATTERN_WIDTH * PATTERN_HEIGHT];
|
||||
|
||||
// ┌▄▄┬──┬──┬──┐
|
||||
// ├▀▀┼──┼──┼──┤
|
||||
// ├──┼──┼▄▄┼──┤
|
||||
// ├──┼──┼▀▀┼──┤
|
||||
// └──┴──┴──┴──┘
|
||||
let pixels = [(0, 0), (2, 2)];
|
||||
for &(x, y) in &pixels {
|
||||
let index = (x + y * PATTERN_WIDTH as usize) * 4;
|
||||
data[index..index + 4].copy_from_slice(&color.to_rgba8_srgb());
|
||||
}
|
||||
|
||||
let image_data = web_sys::ImageData::new_with_u8_clamped_array_and_sh(wasm_bindgen::Clamped(&mut data), PATTERN_WIDTH as u32, PATTERN_HEIGHT as u32).unwrap();
|
||||
pattern_context.put_image_data(&image_data, 0., 0.).unwrap();
|
||||
let pattern = self.render_context.create_pattern_with_offscreen_canvas(&pattern_canvas, "repeat").unwrap().unwrap();
|
||||
|
||||
self.push_path(subpaths, transform);
|
||||
|
||||
self.render_context.set_fill_style_canvas_pattern(&pattern);
|
||||
self.render_context.fill();
|
||||
}
|
||||
|
||||
pub fn get_width(&self, text: &str) -> f64 {
|
||||
|
|
|
|||
|
|
@ -218,7 +218,7 @@ impl MessageHandler<ToolMessage, ToolMessageData<'_>> for ToolMessageHandler {
|
|||
let document_data = &mut self.tool_state.document_tool_data;
|
||||
document_data.primary_color = color;
|
||||
|
||||
self.tool_state.document_tool_data.update_working_colors(responses); // TODO: Make this an event
|
||||
document_data.update_working_colors(responses); // TODO: Make this an event
|
||||
}
|
||||
ToolMessage::SelectRandomPrimaryColor => {
|
||||
// Select a random primary color (rgba) based on an UUID
|
||||
|
|
|
|||
|
|
@ -1,18 +1,23 @@
|
|||
use super::tool_prelude::*;
|
||||
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils::NodeGraphLayer;
|
||||
use graphene_core::vector::style::Fill;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct FillTool {
|
||||
fsm_state: FillToolFsmState,
|
||||
}
|
||||
|
||||
#[impl_message(Message, ToolMessage, Fill)]
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Hash, serde::Serialize, serde::Deserialize, specta::Type)]
|
||||
#[derive(PartialEq, Clone, Debug, Hash, serde::Serialize, serde::Deserialize, specta::Type)]
|
||||
pub enum FillToolMessage {
|
||||
// Standard messages
|
||||
Abort,
|
||||
WorkingColorChanged,
|
||||
Overlays(OverlayContext),
|
||||
|
||||
// Tool-specific messages
|
||||
PointerMove,
|
||||
PointerUp,
|
||||
FillPrimaryColor,
|
||||
FillSecondaryColor,
|
||||
|
|
@ -45,8 +50,10 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for FillToo
|
|||
FillToolFsmState::Ready => actions!(FillToolMessageDiscriminant;
|
||||
FillPrimaryColor,
|
||||
FillSecondaryColor,
|
||||
PointerMove,
|
||||
),
|
||||
FillToolFsmState::Filling => actions!(FillToolMessageDiscriminant;
|
||||
PointerMove,
|
||||
PointerUp,
|
||||
Abort,
|
||||
),
|
||||
|
|
@ -58,6 +65,8 @@ impl ToolTransition for FillTool {
|
|||
fn event_to_message_map(&self) -> EventToMessageMap {
|
||||
EventToMessageMap {
|
||||
tool_abort: Some(FillToolMessage::Abort.into()),
|
||||
working_color_changed: Some(FillToolMessage::WorkingColorChanged.into()),
|
||||
overlay_provider: Some(|overlay_context| FillToolMessage::Overlays(overlay_context).into()),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
|
@ -82,6 +91,23 @@ impl Fsm for FillToolFsmState {
|
|||
|
||||
let ToolMessage::Fill(event) = event else { return self };
|
||||
match (self, event) {
|
||||
(_, FillToolMessage::Overlays(mut overlay_context)) => {
|
||||
// Choose the working color to preview
|
||||
let use_secondary = input.keyboard.get(Key::Shift as usize);
|
||||
let preview_color = if use_secondary { global_tool_data.secondary_color } else { global_tool_data.primary_color };
|
||||
|
||||
// Get the layer the user is hovering over
|
||||
if let Some(layer) = document.click(input) {
|
||||
overlay_context.fill_path_pattern(document.metadata().layer_outline(layer), document.metadata().transform_to_viewport(layer), &preview_color);
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
(_, FillToolMessage::PointerMove | FillToolMessage::WorkingColorChanged) => {
|
||||
// Generate the hover outline
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
self
|
||||
}
|
||||
(FillToolFsmState::Ready, color_event) => {
|
||||
let Some(layer_identifier) = document.click(input) else {
|
||||
return self;
|
||||
|
|
|
|||
|
|
@ -1030,7 +1030,7 @@ impl Fsm for PathToolFsmState {
|
|||
|
||||
match self {
|
||||
Self::Drawing { selection_shape } => {
|
||||
let mut fill_color = graphene_std::Color::from_rgb_str(crate::consts::COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap())
|
||||
let mut fill_color = graphene_std::Color::from_rgb_str(COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap())
|
||||
.unwrap()
|
||||
.with_alpha(0.05)
|
||||
.to_rgba_hex_srgb();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use super::tool_prelude::*;
|
||||
use crate::consts::{DEFAULT_STROKE_WIDTH, HIDE_HANDLE_DISTANCE, LINE_ROTATE_SNAP_ANGLE};
|
||||
use crate::consts::{COLOR_OVERLAY_BLUE, DEFAULT_STROKE_WIDTH, HIDE_HANDLE_DISTANCE, LINE_ROTATE_SNAP_ANGLE};
|
||||
use crate::messages::input_mapper::utility_types::input_mouse::MouseKeys;
|
||||
use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type;
|
||||
use crate::messages::portfolio::document::overlays::utility_functions::path_overlays;
|
||||
|
|
@ -15,7 +15,7 @@ use bezier_rs::{Bezier, BezierHandles};
|
|||
use graph_craft::document::NodeId;
|
||||
use graphene_core::Color;
|
||||
use graphene_core::vector::{PointId, VectorModificationType};
|
||||
use graphene_std::vector::{HandleId, ManipulatorPointId, NoHashBuilder, SegmentId, VectorData};
|
||||
use graphene_std::vector::{HandleId, ManipulatorPointId, NoHashBuilder, SegmentId, StrokeId, VectorData};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct PenTool {
|
||||
|
|
@ -1614,6 +1614,54 @@ impl Fsm for PenToolFsmState {
|
|||
overlay_context.manipulator_anchor(next_anchor, false, None);
|
||||
}
|
||||
|
||||
// Display a filled overlay of the shape if the new point closes the path
|
||||
if let Some(latest_point) = tool_data.latest_point() {
|
||||
let handle_start = latest_point.handle_start;
|
||||
let handle_end = tool_data.handle_end.unwrap_or(tool_data.next_handle_start);
|
||||
let next_point = tool_data.next_point;
|
||||
let start = latest_point.id;
|
||||
|
||||
if let Some(layer) = layer {
|
||||
let mut vector_data = document.network_interface.compute_modified_vector(layer).unwrap();
|
||||
|
||||
let closest_point = vector_data.extendable_points(preferences.vector_meshes).filter(|&id| id != start).find(|&id| {
|
||||
vector_data.point_domain.position_from_id(id).map_or(false, |pos| {
|
||||
let dist_sq = transform.transform_point2(pos).distance_squared(transform.transform_point2(next_point));
|
||||
dist_sq < crate::consts::SNAP_POINT_TOLERANCE.powi(2)
|
||||
})
|
||||
});
|
||||
|
||||
// We have the point. Join the 2 vertices and check if any path is closed.
|
||||
if let Some(end) = closest_point {
|
||||
let segment_id = SegmentId::generate();
|
||||
vector_data.push(segment_id, start, end, BezierHandles::Cubic { handle_start, handle_end }, StrokeId::ZERO);
|
||||
|
||||
let grouped_segments = vector_data.auto_join_paths();
|
||||
let closed_paths = grouped_segments.iter().filter(|path| path.is_closed() && path.contains(segment_id));
|
||||
|
||||
let subpaths: Vec<_> = closed_paths
|
||||
.filter_map(|path| {
|
||||
let segments = path.edges.iter().filter_map(|edge| {
|
||||
vector_data
|
||||
.segment_domain
|
||||
.iter()
|
||||
.find(|(id, _, _, _)| id == &edge.id)
|
||||
.map(|(_, start, end, bezier)| if start == edge.start { (bezier, start, end) } else { (bezier.reversed(), end, start) })
|
||||
});
|
||||
vector_data.subpath_from_segments_ignore_discontinuities(segments)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut fill_color = graphene_std::Color::from_rgb_str(COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap())
|
||||
.unwrap()
|
||||
.with_alpha(0.05)
|
||||
.to_rgba_hex_srgb();
|
||||
fill_color.insert(0, '#');
|
||||
overlay_context.fill_path(subpaths.iter(), transform, fill_color.as_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw the overlays that visualize current snapping
|
||||
tool_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context);
|
||||
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
use super::tool_prelude::*;
|
||||
use crate::consts::{
|
||||
COLOR_OVERLAY_GREEN, COLOR_OVERLAY_RED, COMPASS_ROSE_HOVER_RING_DIAMETER, DRAG_DIRECTION_MODE_DETERMINATION_THRESHOLD, RESIZE_HANDLE_SIZE, ROTATE_INCREMENT, SELECTION_DRAG_ANGLE,
|
||||
SELECTION_TOLERANCE,
|
||||
COLOR_OVERLAY_BLUE, COLOR_OVERLAY_GREEN, COLOR_OVERLAY_RED, COMPASS_ROSE_HOVER_RING_DIAMETER, DRAG_DIRECTION_MODE_DETERMINATION_THRESHOLD, RESIZE_HANDLE_SIZE, ROTATE_INCREMENT,
|
||||
SELECTION_DRAG_ANGLE, SELECTION_TOLERANCE,
|
||||
};
|
||||
use crate::messages::input_mapper::utility_types::input_mouse::ViewportPosition;
|
||||
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
|
||||
|
|
@ -759,7 +759,7 @@ impl Fsm for SelectToolFsmState {
|
|||
}
|
||||
|
||||
// Update the selection box
|
||||
let mut fill_color = graphene_std::Color::from_rgb_str(crate::consts::COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap())
|
||||
let mut fill_color = graphene_std::Color::from_rgb_str(COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap())
|
||||
.unwrap()
|
||||
.with_alpha(0.05)
|
||||
.to_rgba_hex_srgb();
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
#![allow(clippy::too_many_arguments)]
|
||||
|
||||
use super::tool_prelude::*;
|
||||
use crate::consts::{COLOR_OVERLAY_RED, DRAG_THRESHOLD};
|
||||
use crate::consts::{COLOR_OVERLAY_BLUE, COLOR_OVERLAY_RED, DRAG_THRESHOLD};
|
||||
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
|
||||
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
|
|
@ -456,7 +456,7 @@ impl Fsm for TextToolFsmState {
|
|||
font_cache,
|
||||
..
|
||||
} = transition_data;
|
||||
let fill_color = graphene_std::Color::from_rgb_str(crate::consts::COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap())
|
||||
let fill_color = graphene_std::Color::from_rgb_str(COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap())
|
||||
.unwrap()
|
||||
.with_alpha(0.05)
|
||||
.to_rgba_hex_srgb();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue