mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-12-23 10:11:54 +00:00
Path Tool: Implement anchor point dragging (#451)
* #82 path-tool: WIP selecting control point working * Fix bug where duplication with Ctrl+D doesn't properly duplicate (#423) * bug fix: duplication didn't properly duplicate * cargo fmt * changed the formatting slightly for readability * Small cleanups, changed color of handles upon selection * Fix changes from merge * Remove duplicate anchor points on top of one another * Fix possible issues with thumbnails not being updated from Graphene operations * path-tool: attempt to move control points on click * Add dragging for control points * Editing shape anchors functional. Handles next. * Comment cleanup & slight cleanup of closest_anchor(..) * Removing conflict with master * Tiny code tweaks Co-authored-by: Keavon Chambers <keavon@keavon.com> Co-authored-by: caleb <56044292+caleb-ad@users.noreply.github.com> Co-authored-by: otdavies <oliver@psyfer.io> Co-authored-by: Dennis <dennis@kobert.dev>
This commit is contained in:
parent
05e8a98f46
commit
11f15bd6af
6 changed files with 351 additions and 71 deletions
|
|
@ -55,8 +55,15 @@ pub enum VectorManipulatorSegment {
|
|||
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
pub struct VectorManipulatorShape {
|
||||
/// The path to the layer
|
||||
pub layer_path: Vec<LayerId>,
|
||||
/// The outline of the shape
|
||||
pub path: kurbo::BezPath,
|
||||
/// The control points / manipulator handles
|
||||
pub segments: Vec<VectorManipulatorSegment>,
|
||||
/// The compound Bezier curve is closed
|
||||
pub closed: bool,
|
||||
/// The transformation matrix to apply
|
||||
pub transform: DAffine2,
|
||||
}
|
||||
|
||||
|
|
@ -266,9 +273,11 @@ impl DocumentMessageHandler {
|
|||
.collect::<Vec<VectorManipulatorSegment>>();
|
||||
|
||||
Some(VectorManipulatorShape {
|
||||
layer_path: path_to_shape.to_vec(),
|
||||
path,
|
||||
segments,
|
||||
transform: viewport_transform,
|
||||
closed: shape.closed,
|
||||
})
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -186,6 +186,10 @@ impl Default for Mapping {
|
|||
entry! {action=LineMessage::Abort, key_down=Rmb},
|
||||
entry! {action=LineMessage::Abort, key_down=KeyEscape},
|
||||
entry! {action=LineMessage::Redraw{center: KeyAlt, lock_angle: KeyControl, snap_angle: KeyShift}, triggers=[KeyAlt, KeyShift, KeyControl]},
|
||||
// Path
|
||||
entry! {action=PathMessage::DragStart, key_down=Lmb},
|
||||
entry! {action=PathMessage::PointerMove, message=InputMapperMessage::PointerMove},
|
||||
entry! {action=PathMessage::DragStop, key_up=Lmb},
|
||||
// Pen
|
||||
entry! {action=PenMessage::PointerMove, message=InputMapperMessage::PointerMove},
|
||||
entry! {action=PenMessage::DragStart, key_down=Lmb},
|
||||
|
|
|
|||
|
|
@ -13,9 +13,12 @@ use glam::{DAffine2, DVec2};
|
|||
use graphene::color::Color;
|
||||
use graphene::layers::style;
|
||||
use graphene::layers::style::Fill;
|
||||
use graphene::layers::style::PathStyle;
|
||||
use graphene::layers::style::Stroke;
|
||||
use graphene::Operation;
|
||||
use kurbo::BezPath;
|
||||
use kurbo::PathEl;
|
||||
use kurbo::Vec2;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Default)]
|
||||
|
|
@ -27,6 +30,10 @@ pub struct Path {
|
|||
#[impl_message(Message, ToolMessage, Path)]
|
||||
#[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)]
|
||||
pub enum PathMessage {
|
||||
DragStart,
|
||||
PointerMove,
|
||||
DragStop,
|
||||
|
||||
// Standard messages
|
||||
Abort,
|
||||
DocumentIsDirty,
|
||||
|
|
@ -47,10 +54,12 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Path {
|
|||
}
|
||||
}
|
||||
|
||||
// Different actions depending on state may be wanted:
|
||||
fn actions(&self) -> ActionList {
|
||||
use PathToolFsmState::*;
|
||||
match self.fsm_state {
|
||||
Ready => actions!(PathMessageDiscriminant;),
|
||||
Ready => actions!(PathMessageDiscriminant; DragStart),
|
||||
Dragging => actions!(PathMessageDiscriminant; DragStop, PointerMove),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -58,6 +67,7 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Path {
|
|||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum PathToolFsmState {
|
||||
Ready,
|
||||
Dragging,
|
||||
}
|
||||
|
||||
impl Default for PathToolFsmState {
|
||||
|
|
@ -72,9 +82,19 @@ struct PathToolData {
|
|||
handle_marker_pool: Vec<Vec<LayerId>>,
|
||||
anchor_handle_line_pool: Vec<Vec<LayerId>>,
|
||||
shape_outline_pool: Vec<Vec<LayerId>>,
|
||||
selected_shapes: Vec<VectorManipulatorShape>,
|
||||
selection: PathToolSelection,
|
||||
}
|
||||
|
||||
impl PathToolData {}
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct PathToolSelection {
|
||||
closest_layer_path: Vec<LayerId>,
|
||||
closest_shape_id: usize,
|
||||
overlay_path: Vec<LayerId>,
|
||||
bez_path_elements: Vec<kurbo::PathEl>,
|
||||
bez_segment_id: usize,
|
||||
}
|
||||
|
||||
impl Fsm for PathToolFsmState {
|
||||
type ToolData = PathToolData;
|
||||
|
|
@ -85,7 +105,7 @@ impl Fsm for PathToolFsmState {
|
|||
document: &DocumentMessageHandler,
|
||||
_tool_data: &DocumentToolData,
|
||||
data: &mut Self::ToolData,
|
||||
_input: &InputPreprocessor,
|
||||
input: &InputPreprocessor,
|
||||
responses: &mut VecDeque<Message>,
|
||||
) -> Self {
|
||||
if let ToolMessage::Path(event) = event {
|
||||
|
|
@ -134,60 +154,52 @@ impl Fsm for PathToolFsmState {
|
|||
);
|
||||
shape_i += 1;
|
||||
|
||||
for segment in &shape_to_draw.segments {
|
||||
// TODO: We draw each anchor point twice because segment has it on both ends, fix this
|
||||
let (anchors, handles, anchor_handle_lines) = match segment {
|
||||
VectorManipulatorSegment::Line(a1, a2) => (vec![*a1, *a2], vec![], vec![]),
|
||||
VectorManipulatorSegment::Quad(a1, h1, a2) => (vec![*a1, *a2], vec![*h1], vec![(*h1, *a1)]),
|
||||
VectorManipulatorSegment::Cubic(a1, h1, h2, a2) => (vec![*a1, *a2], vec![*h1, *h2], vec![(*h1, *a1), (*h2, *a2)]),
|
||||
};
|
||||
let segment = shape_manipulator_points(shape_to_draw);
|
||||
|
||||
// Draw the line connecting the anchor with handle for cubic and quadratic bezier segments
|
||||
for anchor_handle_line in anchor_handle_lines {
|
||||
let marker = data.anchor_handle_line_pool[line_i].clone();
|
||||
// Draw the line connecting the anchor with handle for cubic and quadratic bezier segments
|
||||
for anchor_handle_line in segment.anchor_handle_lines {
|
||||
let marker = &data.anchor_handle_line_pool[line_i];
|
||||
|
||||
let line_vector = anchor_handle_line.0 - anchor_handle_line.1;
|
||||
let line_vector = anchor_handle_line.0 - anchor_handle_line.1;
|
||||
|
||||
let scale = DVec2::splat(line_vector.length());
|
||||
let angle = -line_vector.angle_between(DVec2::X);
|
||||
let translation = (anchor_handle_line.1 + BIAS).round() + DVec2::splat(0.5);
|
||||
let transform = DAffine2::from_scale_angle_translation(scale, angle, translation).to_cols_array();
|
||||
let scale = DVec2::splat(line_vector.length());
|
||||
let angle = -line_vector.angle_between(DVec2::X);
|
||||
let translation = (anchor_handle_line.1 + BIAS).round() + DVec2::splat(0.5);
|
||||
let transform = DAffine2::from_scale_angle_translation(scale, angle, translation).to_cols_array();
|
||||
|
||||
responses.push_back(DocumentMessage::Overlay(Operation::SetLayerTransformInViewport { path: marker.clone(), transform }.into()).into());
|
||||
responses.push_back(DocumentMessage::Overlay(Operation::SetLayerVisibility { path: marker, visible: true }.into()).into());
|
||||
responses.push_back(DocumentMessage::Overlay(Operation::SetLayerTransformInViewport { path: marker.clone(), transform }.into()).into());
|
||||
responses.push_back(DocumentMessage::Overlay(Operation::SetLayerVisibility { path: marker.clone(), visible: true }.into()).into());
|
||||
|
||||
line_i += 1;
|
||||
}
|
||||
line_i += 1;
|
||||
}
|
||||
|
||||
// Draw the draggable square points on the end of every line segment or bezier curve segment
|
||||
for anchor in anchors {
|
||||
let marker = data.anchor_marker_pool[anchor_i].clone();
|
||||
// Draw the draggable square points on the end of every line segment or bezier curve segment
|
||||
for anchor in segment.anchors {
|
||||
let scale = DVec2::splat(VECTOR_MANIPULATOR_ANCHOR_MARKER_SIZE);
|
||||
let angle = 0.;
|
||||
let translation = (anchor - (scale / 2.) + BIAS).round();
|
||||
let transform = DAffine2::from_scale_angle_translation(scale, angle, translation).to_cols_array();
|
||||
|
||||
let scale = DVec2::splat(VECTOR_MANIPULATOR_ANCHOR_MARKER_SIZE);
|
||||
let angle = 0.;
|
||||
let translation = (anchor - (scale / 2.) + BIAS).round();
|
||||
let transform = DAffine2::from_scale_angle_translation(scale, angle, translation).to_cols_array();
|
||||
let marker = &data.anchor_marker_pool[anchor_i];
|
||||
responses.push_back(DocumentMessage::Overlay(Operation::SetLayerTransformInViewport { path: marker.clone(), transform }.into()).into());
|
||||
responses.push_back(DocumentMessage::Overlay(Operation::SetLayerVisibility { path: marker.clone(), visible: true }.into()).into());
|
||||
|
||||
responses.push_back(DocumentMessage::Overlay(Operation::SetLayerTransformInViewport { path: marker.clone(), transform }.into()).into());
|
||||
responses.push_back(DocumentMessage::Overlay(Operation::SetLayerVisibility { path: marker, visible: true }.into()).into());
|
||||
anchor_i += 1;
|
||||
}
|
||||
|
||||
anchor_i += 1;
|
||||
}
|
||||
// Draw the draggable handle for cubic and quadratic bezier segments
|
||||
for handle in segment.handles {
|
||||
let marker = &data.handle_marker_pool[handle_i];
|
||||
|
||||
// Draw the draggable handle for cubic and quadratic bezier segments
|
||||
for handle in handles {
|
||||
let marker = data.handle_marker_pool[handle_i].clone();
|
||||
let scale = DVec2::splat(VECTOR_MANIPULATOR_ANCHOR_MARKER_SIZE);
|
||||
let angle = 0.;
|
||||
let translation = (handle - (scale / 2.) + BIAS).round();
|
||||
let transform = DAffine2::from_scale_angle_translation(scale, angle, translation).to_cols_array();
|
||||
|
||||
let scale = DVec2::splat(VECTOR_MANIPULATOR_ANCHOR_MARKER_SIZE);
|
||||
let angle = 0.;
|
||||
let translation = (handle - (scale / 2.) + BIAS).round();
|
||||
let transform = DAffine2::from_scale_angle_translation(scale, angle, translation).to_cols_array();
|
||||
responses.push_back(DocumentMessage::Overlay(Operation::SetLayerTransformInViewport { path: marker.clone(), transform }.into()).into());
|
||||
responses.push_back(DocumentMessage::Overlay(Operation::SetLayerVisibility { path: marker.clone(), visible: true }.into()).into());
|
||||
|
||||
responses.push_back(DocumentMessage::Overlay(Operation::SetLayerTransformInViewport { path: marker.clone(), transform }.into()).into());
|
||||
responses.push_back(DocumentMessage::Overlay(Operation::SetLayerVisibility { path: marker, visible: true }.into()).into());
|
||||
|
||||
handle_i += 1;
|
||||
}
|
||||
handle_i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -211,6 +223,170 @@ impl Fsm for PathToolFsmState {
|
|||
|
||||
self
|
||||
}
|
||||
(_, DragStart) => {
|
||||
// todo: DRY refactor (this arm is very similar to the (_, RedrawOverlay) arm)
|
||||
|
||||
let mouse_pos = input.mouse.position;
|
||||
let mut points = Vec::new();
|
||||
|
||||
let (mut anchor_i, mut handle_i, _line_i, _shape_i) = (0, 0, 0, 0);
|
||||
let shapes_to_draw = document.selected_visible_layers_vector_points();
|
||||
let (total_anchors, total_handles, _total_anchor_handle_lines) = calculate_total_overlays_per_type(&shapes_to_draw);
|
||||
grow_overlay_pool_entries(&mut data.anchor_marker_pool, total_anchors, add_anchor_marker, responses);
|
||||
grow_overlay_pool_entries(&mut data.handle_marker_pool, total_handles, add_handle_marker, responses);
|
||||
|
||||
#[derive(Debug)]
|
||||
enum PointType {
|
||||
Anchor { anchor_i: usize, layer_path: Vec<LayerId>, shape_offset: usize },
|
||||
Handle { handle_i: usize, layer_path: Vec<LayerId>, shape_offset: usize },
|
||||
}
|
||||
#[derive(Debug)]
|
||||
struct Point {
|
||||
point_type: PointType,
|
||||
mouse_proximity: f64,
|
||||
}
|
||||
|
||||
impl Point {
|
||||
fn new(_position: DVec2, point_type: PointType, mouse_proximity: f64) -> Self {
|
||||
Self { point_type, mouse_proximity }
|
||||
}
|
||||
}
|
||||
|
||||
// TODO simplify the following block
|
||||
let select_threshold = 6.;
|
||||
let select_threshold_squared = select_threshold * select_threshold;
|
||||
|
||||
for (shape_offset, shape_to_draw) in shapes_to_draw.iter().enumerate() {
|
||||
let segment = shape_manipulator_points(shape_to_draw);
|
||||
|
||||
for anchor in segment.anchors {
|
||||
let d2 = mouse_pos.distance_squared(anchor);
|
||||
if d2 < select_threshold_squared {
|
||||
points.push(Point::new(
|
||||
anchor,
|
||||
PointType::Anchor {
|
||||
anchor_i,
|
||||
layer_path: shape_to_draw.layer_path.clone(),
|
||||
shape_offset,
|
||||
},
|
||||
d2,
|
||||
));
|
||||
}
|
||||
anchor_i += 1;
|
||||
}
|
||||
|
||||
for (_, handle) in segment.handles.into_iter().enumerate() {
|
||||
let d2 = mouse_pos.distance_squared(handle);
|
||||
if d2 < select_threshold_squared {
|
||||
points.push(Point::new(
|
||||
handle,
|
||||
PointType::Handle {
|
||||
handle_i,
|
||||
layer_path: shape_to_draw.layer_path.clone(),
|
||||
shape_offset,
|
||||
},
|
||||
d2,
|
||||
));
|
||||
}
|
||||
handle_i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
points.sort_by(|a, b| a.mouse_proximity.partial_cmp(&b.mouse_proximity).unwrap_or(std::cmp::Ordering::Equal));
|
||||
let closest_point_within_click_threshold = points.first();
|
||||
|
||||
if let Some(point) = closest_point_within_click_threshold {
|
||||
let path = match point.point_type {
|
||||
PointType::Anchor {
|
||||
anchor_i,
|
||||
ref layer_path,
|
||||
shape_offset,
|
||||
} => {
|
||||
data.selected_shapes = shapes_to_draw;
|
||||
let shape = &data.selected_shapes[shape_offset];
|
||||
let path = shape.path.clone();
|
||||
let bez: Vec<PathEl> = (&path).into_iter().collect();
|
||||
let transformed = shape.transform.inverse().transform_point2(input.mouse.position);
|
||||
data.selection.bez_segment_id = closest_anchor(&bez, Vec2::new(transformed.x, transformed.y));
|
||||
data.selection.bez_path_elements = bez;
|
||||
data.selection.closest_layer_path = layer_path.clone();
|
||||
data.selection.closest_shape_id = shape_offset;
|
||||
data.anchor_marker_pool[anchor_i].clone()
|
||||
}
|
||||
PointType::Handle {
|
||||
handle_i,
|
||||
ref layer_path,
|
||||
shape_offset,
|
||||
} => {
|
||||
// TODO make this work for the handles, right now just selects the anchors
|
||||
data.selected_shapes = shapes_to_draw;
|
||||
let shape = &data.selected_shapes[shape_offset];
|
||||
let path = shape.path.clone();
|
||||
let bez: Vec<PathEl> = (&path).into_iter().collect();
|
||||
let transformed = shape.transform.inverse().transform_point2(input.mouse.position);
|
||||
data.selection.bez_segment_id = closest_anchor(&bez, Vec2::new(transformed.x, transformed.y));
|
||||
data.selection.bez_path_elements = bez;
|
||||
data.selection.closest_layer_path = layer_path.clone();
|
||||
data.selection.closest_shape_id = shape_offset;
|
||||
data.handle_marker_pool[handle_i].clone()
|
||||
}
|
||||
};
|
||||
|
||||
data.selection.overlay_path = path;
|
||||
responses.push_back(
|
||||
DocumentMessage::Overlay(
|
||||
Operation::SetLayerFill {
|
||||
path: data.selection.overlay_path.clone(),
|
||||
color: COLOR_ACCENT,
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
Dragging
|
||||
} else {
|
||||
Ready
|
||||
}
|
||||
}
|
||||
(Dragging, PointerMove) => {
|
||||
let shape = &data.selected_shapes[data.selection.closest_shape_id];
|
||||
let transformed = shape.transform.inverse().transform_point2(input.mouse.position);
|
||||
let delta: Vec2 = Vec2::new(transformed.x, transformed.y);
|
||||
let replacement = match &data.selection.bez_path_elements[data.selection.bez_segment_id] {
|
||||
PathEl::MoveTo(_) => PathEl::MoveTo(delta.to_point()),
|
||||
PathEl::LineTo(_) => PathEl::LineTo(delta.to_point()),
|
||||
PathEl::QuadTo(a1, _) => PathEl::QuadTo(*a1, delta.to_point()),
|
||||
PathEl::CurveTo(a1, a2, _) => PathEl::CurveTo(*a1, *a2, delta.to_point()),
|
||||
PathEl::ClosePath => unreachable!(),
|
||||
};
|
||||
data.selection.bez_path_elements[data.selection.bez_segment_id] = replacement;
|
||||
|
||||
responses.push_back(
|
||||
Operation::SetShapePathInViewport {
|
||||
path: data.selection.closest_layer_path.clone(),
|
||||
bez_path: data.selection.bez_path_elements.clone().into_iter().collect(),
|
||||
transform: shape.transform.to_cols_array(),
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
|
||||
Dragging
|
||||
}
|
||||
(_, PointerMove) => self,
|
||||
(_, DragStop) => {
|
||||
let style = PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 2.0)), Some(Fill::new(Color::WHITE)));
|
||||
responses.push_back(
|
||||
DocumentMessage::Overlay(
|
||||
Operation::SetLayerStyle {
|
||||
path: data.selection.overlay_path.clone(),
|
||||
style,
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
Ready
|
||||
}
|
||||
(_, Abort) => {
|
||||
// Destory the overlay layer pools
|
||||
while let Some(layer) = data.anchor_marker_pool.pop() {
|
||||
|
|
@ -297,26 +473,76 @@ impl Fsm for PathToolFsmState {
|
|||
},
|
||||
]),
|
||||
]),
|
||||
PathToolFsmState::Dragging => HintData(vec![]),
|
||||
};
|
||||
|
||||
responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into());
|
||||
}
|
||||
}
|
||||
|
||||
fn calculate_total_overlays_per_type(shapes_to_draw: &[VectorManipulatorShape]) -> (usize, usize, usize) {
|
||||
struct VectorManipulatorTypes {
|
||||
anchors: Vec<glam::DVec2>,
|
||||
handles: Vec<glam::DVec2>,
|
||||
anchor_handle_lines: Vec<(glam::DVec2, glam::DVec2)>,
|
||||
}
|
||||
|
||||
fn shape_manipulator_points(shape: &VectorManipulatorShape) -> VectorManipulatorTypes {
|
||||
// TODO: Performance can be improved by using three iterators (calling `.iter()` for each of the three) instead of a vector, achievable with some file restructuring
|
||||
let initial_counts = calculate_shape_overlays_per_type(shape);
|
||||
let mut result = VectorManipulatorTypes {
|
||||
anchors: Vec::with_capacity(initial_counts.0),
|
||||
handles: Vec::with_capacity(initial_counts.1),
|
||||
anchor_handle_lines: Vec::with_capacity(initial_counts.2),
|
||||
};
|
||||
|
||||
for (i, segment) in shape.segments.iter().enumerate() {
|
||||
// An open shape needs an extra point, which is part of the first segment (when `i` is 0)
|
||||
let include_start_and_end = !shape.closed && i == 0;
|
||||
|
||||
match segment {
|
||||
VectorManipulatorSegment::Line(a1, a2) => {
|
||||
result.anchors.extend(if include_start_and_end { vec![*a1, *a2] } else { vec![*a2] });
|
||||
}
|
||||
VectorManipulatorSegment::Quad(a1, h1, a2) => {
|
||||
result.anchors.extend(if include_start_and_end { vec![*a1, *a2] } else { vec![*a2] });
|
||||
result.handles.extend(vec![*h1]);
|
||||
result.anchor_handle_lines.extend(vec![(*h1, *a1)]);
|
||||
}
|
||||
VectorManipulatorSegment::Cubic(a1, h1, h2, a2) => {
|
||||
result.anchors.extend(if include_start_and_end { vec![*a1, *a2] } else { vec![*a2] });
|
||||
result.handles.extend(vec![*h1, *h2]);
|
||||
result.anchor_handle_lines.extend(vec![(*h1, *a1), (*h2, *a2)]);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn calculate_total_overlays_per_type(shapes: &[VectorManipulatorShape]) -> (usize, usize, usize) {
|
||||
shapes.iter().fold((0, 0, 0), |acc, shape| {
|
||||
let counts = calculate_shape_overlays_per_type(shape);
|
||||
(acc.0 + counts.0, acc.1 + counts.1, acc.2 + counts.2)
|
||||
})
|
||||
}
|
||||
|
||||
fn calculate_shape_overlays_per_type(shape: &VectorManipulatorShape) -> (usize, usize, usize) {
|
||||
let (mut total_anchors, mut total_handles, mut total_anchor_handle_lines) = (0, 0, 0);
|
||||
|
||||
for shape_to_draw in shapes_to_draw {
|
||||
for segment in &shape_to_draw.segments {
|
||||
let (anchors, handles, anchor_handle_lines) = match segment {
|
||||
VectorManipulatorSegment::Line(_, _) => (2, 0, 0),
|
||||
VectorManipulatorSegment::Quad(_, _, _) => (2, 1, 1),
|
||||
VectorManipulatorSegment::Cubic(_, _, _, _) => (2, 2, 2),
|
||||
};
|
||||
total_anchors += anchors;
|
||||
total_handles += handles;
|
||||
total_anchor_handle_lines += anchor_handle_lines;
|
||||
}
|
||||
for segment in &shape.segments {
|
||||
let (anchors, handles, anchor_handle_lines) = match segment {
|
||||
VectorManipulatorSegment::Line(_, _) => (1, 0, 0),
|
||||
VectorManipulatorSegment::Quad(_, _, _) => (1, 1, 1),
|
||||
VectorManipulatorSegment::Cubic(_, _, _, _) => (1, 2, 2),
|
||||
};
|
||||
total_anchors += anchors;
|
||||
total_handles += handles;
|
||||
total_anchor_handle_lines += anchor_handle_lines;
|
||||
}
|
||||
|
||||
// A non-closed shape does not reuse the start and end point, so there is one extra
|
||||
if !shape.closed {
|
||||
total_anchors += 1;
|
||||
}
|
||||
|
||||
(total_anchors, total_handles, total_anchor_handle_lines)
|
||||
|
|
@ -383,8 +609,33 @@ fn add_shape_outline(responses: &mut VecDeque<Message>) -> Vec<LayerId> {
|
|||
path: layer_path.clone(),
|
||||
bez_path: BezPath::default(),
|
||||
style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 1.0)), Some(Fill::none())),
|
||||
closed: false,
|
||||
};
|
||||
responses.push_back(DocumentMessage::Overlay(operation.into()).into());
|
||||
|
||||
layer_path
|
||||
}
|
||||
|
||||
// Brute force comparison to determine which path element we want to select
|
||||
fn closest_anchor(bez: &[kurbo::PathEl], pos: kurbo::Vec2) -> usize {
|
||||
let mut closest: usize = 0;
|
||||
let mut closest_distance: f64 = f64::MAX;
|
||||
for (i, el) in bez.iter().enumerate() {
|
||||
let p = match el {
|
||||
kurbo::PathEl::MoveTo(p) => Some(p.to_vec2()),
|
||||
kurbo::PathEl::LineTo(p) => Some(p.to_vec2()),
|
||||
kurbo::PathEl::QuadTo(_, p) => Some(p.to_vec2()),
|
||||
kurbo::PathEl::CurveTo(_, _, p) => Some(p.to_vec2()),
|
||||
kurbo::PathEl::ClosePath => None,
|
||||
};
|
||||
if p.is_none() {
|
||||
continue;
|
||||
}
|
||||
let distance_squared = (p.unwrap() - pos).hypot2();
|
||||
if distance_squared < closest_distance {
|
||||
closest_distance = distance_squared;
|
||||
closest = i;
|
||||
}
|
||||
}
|
||||
closest
|
||||
}
|
||||
|
|
|
|||
|
|
@ -448,8 +448,8 @@ impl Document {
|
|||
|
||||
Some([vec![DocumentChanged, CreatedLayer { path: path.clone() }], update_thumbnails_upstream(path)].concat())
|
||||
}
|
||||
Operation::AddOverlayShape { path, style, bez_path } => {
|
||||
let mut shape = Shape::from_bez_path(bez_path.clone(), *style, false);
|
||||
Operation::AddOverlayShape { path, style, bez_path, closed } => {
|
||||
let mut shape = Shape::from_bez_path(bez_path.clone(), *style, *closed);
|
||||
shape.render_index = -1;
|
||||
|
||||
let layer = Layer::new(LayerDataType::Shape(shape), DAffine2::IDENTITY.to_cols_array());
|
||||
|
|
@ -546,14 +546,14 @@ impl Document {
|
|||
self.set_layer(path, Layer::new(LayerDataType::Folder(Folder::default()), DAffine2::IDENTITY.to_cols_array()), -1)?;
|
||||
self.mark_as_dirty(path)?;
|
||||
|
||||
Some(vec![DocumentChanged, CreatedLayer { path: path.clone() }])
|
||||
Some([vec![DocumentChanged, CreatedLayer { path: path.clone() }], update_thumbnails_upstream(path)].concat())
|
||||
}
|
||||
Operation::TransformLayer { path, transform } => {
|
||||
let layer = self.layer_mut(path).unwrap();
|
||||
let transform = DAffine2::from_cols_array(transform) * layer.transform;
|
||||
layer.transform = transform;
|
||||
self.mark_as_dirty(path)?;
|
||||
Some(vec![DocumentChanged])
|
||||
Some([vec![DocumentChanged], update_thumbnails_upstream(path)].concat())
|
||||
}
|
||||
Operation::TransformLayerInViewport { path, transform } => {
|
||||
let transform = DAffine2::from_cols_array(transform);
|
||||
|
|
@ -567,6 +567,17 @@ impl Document {
|
|||
self.mark_as_dirty(path)?;
|
||||
Some([vec![DocumentChanged], update_thumbnails_upstream(path)].concat())
|
||||
}
|
||||
Operation::SetShapePath { path, bez_path } => {
|
||||
self.mark_as_dirty(path)?;
|
||||
|
||||
match &mut self.layer_mut(path)?.data {
|
||||
LayerDataType::Shape(shape) => {
|
||||
shape.path = bez_path.clone();
|
||||
}
|
||||
LayerDataType::Folder(_) => (),
|
||||
}
|
||||
Some(vec![DocumentChanged, LayerChanged { path: path.clone() }])
|
||||
}
|
||||
Operation::SetShapePathInViewport { path, bez_path, transform } => {
|
||||
let transform = DAffine2::from_cols_array(transform);
|
||||
self.set_transform_relative_to_viewport(path, transform)?;
|
||||
|
|
@ -578,7 +589,7 @@ impl Document {
|
|||
}
|
||||
LayerDataType::Folder(_) => (),
|
||||
}
|
||||
Some(vec![DocumentChanged, LayerChanged { path: path.clone() }])
|
||||
Some([vec![DocumentChanged, LayerChanged { path: path.clone() }], update_thumbnails_upstream(path)].concat())
|
||||
}
|
||||
Operation::TransformLayerInScope { path, transform, scope } => {
|
||||
let transform = DAffine2::from_cols_array(transform);
|
||||
|
|
@ -632,7 +643,7 @@ impl Document {
|
|||
_ => return Err(DocumentError::NotAShape),
|
||||
}
|
||||
self.mark_as_dirty(path)?;
|
||||
Some(vec![DocumentChanged, LayerChanged { path: path.clone() }])
|
||||
Some([vec![DocumentChanged, LayerChanged { path: path.clone() }], update_thumbnails_upstream(path)].concat())
|
||||
}
|
||||
Operation::SetLayerFill { path, color } => {
|
||||
let layer = self.layer_mut(path)?;
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ pub struct Shape {
|
|||
pub path: BezPath,
|
||||
pub style: style::PathStyle,
|
||||
pub render_index: i32,
|
||||
pub solid: bool,
|
||||
pub closed: bool,
|
||||
}
|
||||
|
||||
impl LayerData for Shape {
|
||||
|
|
@ -62,7 +62,7 @@ impl LayerData for Shape {
|
|||
}
|
||||
|
||||
fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>) {
|
||||
if intersect_quad_bez_path(quad, &self.path, self.solid) {
|
||||
if intersect_quad_bez_path(quad, &self.path, self.closed) {
|
||||
intersections.push(path.clone());
|
||||
}
|
||||
}
|
||||
|
|
@ -78,12 +78,12 @@ impl Shape {
|
|||
transforms.iter().skip(start).cloned().reduce(|a, b| a * b).unwrap_or(DAffine2::IDENTITY)
|
||||
}
|
||||
|
||||
pub fn from_bez_path(bez_path: BezPath, style: PathStyle, solid: bool) -> Self {
|
||||
pub fn from_bez_path(bez_path: BezPath, style: PathStyle, closed: bool) -> Self {
|
||||
Self {
|
||||
path: bez_path,
|
||||
style,
|
||||
render_index: 1,
|
||||
solid,
|
||||
closed,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -111,7 +111,7 @@ impl Shape {
|
|||
path,
|
||||
style,
|
||||
render_index: 1,
|
||||
solid: true,
|
||||
closed: true,
|
||||
}
|
||||
}
|
||||
pub fn rectangle(style: PathStyle) -> Self {
|
||||
|
|
@ -119,7 +119,7 @@ impl Shape {
|
|||
path: kurbo::Rect::new(0., 0., 1., 1.).to_path(0.01),
|
||||
style,
|
||||
render_index: 1,
|
||||
solid: true,
|
||||
closed: true,
|
||||
}
|
||||
}
|
||||
pub fn ellipse(style: PathStyle) -> Self {
|
||||
|
|
@ -127,7 +127,7 @@ impl Shape {
|
|||
path: kurbo::Ellipse::from_rect(kurbo::Rect::new(0., 0., 1., 1.)).to_path(0.01),
|
||||
style,
|
||||
render_index: 1,
|
||||
solid: true,
|
||||
closed: true,
|
||||
}
|
||||
}
|
||||
pub fn line(style: PathStyle) -> Self {
|
||||
|
|
@ -135,7 +135,7 @@ impl Shape {
|
|||
path: kurbo::Line::new((0., 0.), (1., 0.)).to_path(0.01),
|
||||
style,
|
||||
render_index: 1,
|
||||
solid: true,
|
||||
closed: false,
|
||||
}
|
||||
}
|
||||
pub fn poly_line(points: Vec<impl Into<glam::DVec2>>, style: PathStyle) -> Self {
|
||||
|
|
@ -150,7 +150,7 @@ impl Shape {
|
|||
path,
|
||||
style,
|
||||
render_index: 0,
|
||||
solid: false,
|
||||
closed: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ pub enum Operation {
|
|||
path: Vec<LayerId>,
|
||||
bez_path: kurbo::BezPath,
|
||||
style: style::PathStyle,
|
||||
closed: bool,
|
||||
},
|
||||
DeleteLayer {
|
||||
path: Vec<LayerId>,
|
||||
|
|
@ -96,6 +97,10 @@ pub enum Operation {
|
|||
path: Vec<LayerId>,
|
||||
transform: [f64; 6],
|
||||
},
|
||||
SetShapePath {
|
||||
path: Vec<LayerId>,
|
||||
bez_path: kurbo::BezPath,
|
||||
},
|
||||
SetShapePathInViewport {
|
||||
path: Vec<LayerId>,
|
||||
bez_path: kurbo::BezPath,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue