mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-25 00:58:12 +08:00
Add alignment overlays (#462)
* Add alignment overlays * Made snapping / overlays not render extra times, made snapping distance larger * Revert snap tolerance change until user testing can be done * WIP conversion to messages * Revert "WIP conversion to messages" This reverts commitddcc23865a. * Overlay document always has (0,0) in top left * Fix AA on overlay lines * Merge branch 'master' into alignment-overlays * Squashed commit of the following: commitdbc19912caAuthor: asyncth <97792158+asyncth@users.noreply.github.com> Date: Sun Jan 16 12:57:03 2022 +0500 Set the mouse cursor in the canvas based on the current tool and its state (#480) * Add FrontendMouseCursor and DisplayMouseCursor * Add update_cursor method to the Fsm trait and implement it for all tools * Rename DisplayMouseCursor to UpdateMouseCursor * Add 'To CSS Cursor Property' transform decorator and change the mouse cursor in the canvas based on the current tool and its state * Implement update_cursor for Navigate tool properly * Keep the cursor when dragging outside of the canvas * Change the mouse cursor to 'zoom-in' when LMB dragging on canvas with Navigate tool * Rename FrontendMouseCursor to MouseCursorIcon * Rename 'event' to 'e' and replace v-on with @ * Change the definition of the MouseCursorIcon type in TS * Replace switch with dictionary look-up * Move the definition of MouseCursorIcon closer to where it's used commit9b36e6a909Author: Keavon Chambers <keavon@keavon.com> Date: Sat Jan 15 17:24:58 2022 -0800 Fix all remaining Vue/TS errors flagged in Volar commit2cc39cdb37Author: Keavon Chambers <keavon@keavon.com> Date: Sat Jan 15 12:55:51 2022 -0800 Tweak whitespace around `use` statements and other lint fixes commitfa390c3875Author: Keavon Chambers <keavon@keavon.com> Date: Sat Jan 15 06:35:06 2022 -0800 Change canvas artboard background color to be 1 shade lighter commitea2d003484Author: Keavon Chambers <keavon@keavon.com> Date: Fri Jan 14 20:54:38 2022 -0800 Modify all message enum data to use named struct values, not tuples (#479) * Massively reorganize and clean up the whole Rust codebase * Modify all message enum data to use named struct values, not tuples commit9b6cbb5f50Author: Keavon Chambers <keavon@keavon.com> Date: Fri Jan 14 14:58:08 2022 -0800 Massively reorganize and clean up the whole Rust codebase (#478) * Massively reorganize and clean up the whole Rust codebase * Additional changes during code review commita535f5c1c1Author: Keavon Chambers <keavon@keavon.com> Date: Wed Jan 12 16:17:40 2022 -0800 Sort messages and message handlers commite70858884dAuthor: Keavon Chambers <keavon@keavon.com> Date: Wed Jan 12 14:16:13 2022 -0800 Standardize FrontendMessage message names commit0b4934b4f3Author: Keavon Chambers <keavon@keavon.com> Date: Wed Jan 12 12:45:07 2022 -0800 Rename document_file.rs to document_message_handler.rs commitec7bf4a771Author: Keavon Chambers <keavon@keavon.com> Date: Wed Jan 12 12:19:14 2022 -0800 Rename DocumentsMessage to PortfolioMessage commit0991312ba0Author: Keavon Chambers <keavon@keavon.com> Date: Wed Jan 12 11:44:49 2022 -0800 Rename document_message_handler.rs to portfolio_message_handler.rs commitc76c92e500Author: 0HyperCube <78500760+0HyperCube@users.noreply.github.com> Date: Wed Jan 12 19:05:55 2022 +0000 Migrate to using MoveSelectedLayersTo (#469) * migrate to using MoveSelectedLayersTo * Fix dragging a selected layer with multiple selected layers * Fix CreatedLayer overriding selection * Fix MoveSelectedLayersTo behaviour * Squashed commit of the following: commit095d577a49Author: Keavon Chambers <keavon@keavon.com> Date: Mon Jan 10 18:06:12 2022 -0800 Fix NumberInput clamping regression with undefined bounds commit9f54a376c4Author: mfish33 <32677537+mfish33@users.noreply.github.com> Date: Sun Jan 9 15:52:55 2022 -0800 Fix bounds with artboards for zoom-to-fit and scrollbar scaling (#473) * - document load keeps postition - zoom to fit - scrollbars use artboard dimensions * - review comments - svg export uses all artboard bounds Co-authored-by: Keavon Chambers <keavon@keavon.com> commit61432de480Author: 0HyperCube <78500760+0HyperCube@users.noreply.github.com> Date: Sat Jan 8 21:06:15 2022 +0000 Fix rotation input (#472) * Fix insert with no nesting at end of panel * Deselect other layers on paste * Resolve logging commit574028375bAuthor: Keavon Chambers <keavon@keavon.com> Date: Wed Jan 12 06:14:32 2022 -0800 Add `npm start` script alias for `npm run serve` commit75e8fc6614Author: Keavon Chambers <keavon@keavon.com> Date: Wed Jan 12 05:17:56 2022 -0800 Switch VS Code's Vue extension from Vetur to Volar commit389b445ef1Author: Keavon Chambers <keavon@keavon.com> Date: Wed Jan 12 03:56:28 2022 -0800 Remove Charcoal references from the code for now commit095d577a49Author: Keavon Chambers <keavon@keavon.com> Date: Mon Jan 10 18:06:12 2022 -0800 Fix NumberInput clamping regression with undefined bounds commit9f54a376c4Author: mfish33 <32677537+mfish33@users.noreply.github.com> Date: Sun Jan 9 15:52:55 2022 -0800 Fix bounds with artboards for zoom-to-fit and scrollbar scaling (#473) * - document load keeps postition - zoom to fit - scrollbars use artboard dimensions * - review comments - svg export uses all artboard bounds Co-authored-by: Keavon Chambers <keavon@keavon.com> commit61432de480Author: 0HyperCube <78500760+0HyperCube@users.noreply.github.com> Date: Sat Jan 8 21:06:15 2022 +0000 Fix rotation input (#472) commit3eeac79f5aAuthor: 0HyperCube <78500760+0HyperCube@users.noreply.github.com> Date: Sat Jan 8 16:30:03 2022 +0000 Enhance the Navigate Tool zoom behavior (#461) * Snap zoom * Navigate zoom from centre * Ctrl to snap zoom in navigate * Use ctrl for global snap rotate * Fix the rotation input on snap rotate * Update hint to use ctrl * Fix mouse centre on drag * Click to zoom in * Clean up centre zoom * Update user input hints; tweak some variable names for clarity and standardization Co-authored-by: Keavon Chambers <keavon@keavon.com> commita2c2f7fc9dAuthor: 0HyperCube <78500760+0HyperCube@users.noreply.github.com> Date: Sat Jan 8 16:02:02 2022 +0000 Add support for resizing workspace panels (#443) * Resize panels * Removing move_selection test pending #444 resolved * Bind event listners and cursor to the document * Fix flex grow on document being reset when drawing * Call onresize when the boundry is dragged * Add min panel size * Add explicit function return types * Dispatch resize event * Lock pointer instead of setting cursor on document Co-authored-by: otdavies <oliver@psyfer.io> Co-authored-by: Keavon Chambers <keavon@keavon.com> commit54e9121115Author: mfish33 <32677537+mfish33@users.noreply.github.com> Date: Sat Jan 8 07:50:08 2022 -0800 Implement artboards and document version enforcement (#466) * - graphite document artboard implementation - autosave document load hitch fix - Autosave will delete saved files when graphite document version changes * formating * - top left 0,0 - fixed hitch on first document - vue calls first render * Revert * Merge branch 'master' into artboards * Small bug fixes and code review tweaks Co-authored-by: Oliver Davies <oliver@psyfer.io> Co-authored-by: Keavon Chambers <keavon@keavon.com> commit11f15bd6afAuthor: Leonard Pauli <leonardpauli@me.com> Date: Sat Jan 8 14:25:24 2022 +0100 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> commit05e8a98f46Author: Keavon Chambers <keavon@keavon.com> Date: Fri Jan 7 23:13:33 2022 -0800 Redesign the Layer Tree UI (#468) commit8e3d237228Author: Oliver Davies <oliver@psyfer.io> Date: Fri Jan 7 15:53:12 2022 -0800 Ungroup layers (#465) * WIP handling corner cases, like ungrouping subfolders * Resolved hanging * Fix recursive ungrouping * Functional, corner case free Ungroup. Small Undo issue & warnings * Update layertree upon undo * Also update layerdata upon redo * Add some polish * Resolved TODOs * Oops didn't save all after rename, ha. Co-authored-by: Dennis <dennis@kobert.dev> commitc1c7192948Author: TrueDoctor <dennis@kobert.dev> Date: Fri Jan 7 04:15:08 2022 +0100 Tidy up path handling in document_file (#464) * Tidy up path handling in document_file + Improve #455 * Cargo Clippy lints * Rename to_vec to map_to_vec Co-authored-by: Oliver Davies <oliver@psyfer.io> commitf4707f9b74Author: mfish33 <32677537+mfish33@users.noreply.github.com> Date: Thu Jan 6 18:45:37 2022 -0800 Better decimal rounding in the NumberInput widget (#457) * better decimal rounding in NumberInput * - created function to reuse - used math instead of string manipulation * updated comment to be correct * updated comment * updated comment commit0219f060b8Author: Keavon Chambers <keavon@keavon.com> Date: Wed Jan 5 05:40:32 2022 -0800 Fix build script to use correct branch name * Fix indentation * Overlays fade in * Fix formatting * Add consts for snap visible and opacity * Tweak constants for improved UX, rejigger imports * Fix AA bounding box * Snap only visible layers * Add some comments Co-authored-by: otdavies <oliver@psyfer.io> Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
committed by
Keavon Chambers
co-authored by
otdavies
Keavon Chambers
parent
b50bee9cf8
commit
d5236f485a
@@ -1,25 +1,105 @@
|
||||
use crate::consts::SNAP_TOLERANCE;
|
||||
use crate::consts::{COLOR_ACCENT, SNAP_OVERLAY_FADE_DISTANCE, SNAP_OVERLAY_UNSNAPPED_OPACITY, SNAP_TOLERANCE};
|
||||
use crate::document::DocumentMessageHandler;
|
||||
use crate::message_prelude::*;
|
||||
|
||||
use graphene::LayerId;
|
||||
use graphene::layers::style::{self, Stroke};
|
||||
use graphene::{LayerId, Operation};
|
||||
|
||||
use glam::DVec2;
|
||||
use glam::{DAffine2, DVec2};
|
||||
use std::f64::consts::PI;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct SnapHandler {
|
||||
snap_targets: Option<(Vec<f64>, Vec<f64>)>,
|
||||
overlay_paths: Vec<Vec<LayerId>>,
|
||||
}
|
||||
|
||||
impl SnapHandler {
|
||||
/// Updates the snapping overlays with the specified distances.
|
||||
/// `positions_and_distances` is a tuple of `position` and `distance` iterators, respectively, each with `(x, y)` values.
|
||||
fn update_overlays(
|
||||
overlay_paths: &mut Vec<Vec<LayerId>>,
|
||||
responses: &mut VecDeque<Message>,
|
||||
viewport_bounds: DVec2,
|
||||
(positions_and_distances): (impl Iterator<Item = (f64, f64)>, impl Iterator<Item = (f64, f64)>),
|
||||
closest_distance: DVec2,
|
||||
) {
|
||||
/// Draws an alignment line overlay with the correct transform and fade opacity, reusing lines from the pool if available.
|
||||
fn add_overlay_line(responses: &mut VecDeque<Message>, transform: [f64; 6], opacity: f64, index: usize, overlay_paths: &mut Vec<Vec<LayerId>>) {
|
||||
// If there isn't one in the pool to ruse, add a new alignment line to the pool with the intended transform
|
||||
let layer_path = if index >= overlay_paths.len() {
|
||||
let layer_path = vec![generate_uuid()];
|
||||
responses.push_back(
|
||||
DocumentMessage::Overlays(
|
||||
Operation::AddOverlayLine {
|
||||
path: layer_path.clone(),
|
||||
transform,
|
||||
style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 1.0)), None),
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
overlay_paths.push(layer_path.clone());
|
||||
layer_path
|
||||
}
|
||||
// Otherwise, reuse an overlay line from the pool and update its new transform
|
||||
else {
|
||||
let layer_path = overlay_paths[index].clone();
|
||||
responses.push_back(DocumentMessage::Overlays(Operation::SetLayerTransform { path: layer_path.clone(), transform }.into()).into());
|
||||
layer_path
|
||||
};
|
||||
|
||||
// Then set its opacity to the fade amount
|
||||
responses.push_back(DocumentMessage::Overlays(Operation::SetLayerOpacity { path: layer_path, opacity }.into()).into());
|
||||
}
|
||||
|
||||
let (positions, distances) = positions_and_distances;
|
||||
let mut index = 0;
|
||||
|
||||
// Draw the vertical alignment lines
|
||||
for (x_target, distance) in positions.filter(|(_pos, dist)| dist.abs() < SNAP_OVERLAY_FADE_DISTANCE) {
|
||||
let transform = DAffine2::from_scale_angle_translation(DVec2::new(viewport_bounds.y, 1.), PI / 2., DVec2::new((x_target).round() - 0.5, 0.)).to_cols_array();
|
||||
|
||||
let opacity = if closest_distance.x == distance {
|
||||
1.
|
||||
} else {
|
||||
SNAP_OVERLAY_UNSNAPPED_OPACITY - distance.abs() / (SNAP_OVERLAY_FADE_DISTANCE / SNAP_OVERLAY_UNSNAPPED_OPACITY)
|
||||
};
|
||||
|
||||
add_overlay_line(responses, transform, opacity, index, overlay_paths);
|
||||
index += 1;
|
||||
}
|
||||
// Draw the horizontal alignment lines
|
||||
for (y_target, distance) in distances.filter(|(_pos, dist)| dist.abs() < SNAP_OVERLAY_FADE_DISTANCE) {
|
||||
let transform = DAffine2::from_scale_angle_translation(DVec2::new(viewport_bounds.x, 1.), 0., DVec2::new(0., (y_target).round() - 0.5)).to_cols_array();
|
||||
|
||||
let opacity = if closest_distance.y == distance {
|
||||
1.
|
||||
} else {
|
||||
SNAP_OVERLAY_UNSNAPPED_OPACITY - distance.abs() / (SNAP_OVERLAY_FADE_DISTANCE / SNAP_OVERLAY_UNSNAPPED_OPACITY)
|
||||
};
|
||||
|
||||
add_overlay_line(responses, transform, opacity, index, overlay_paths);
|
||||
index += 1;
|
||||
}
|
||||
Self::remove_unused_overlays(overlay_paths, responses, index);
|
||||
}
|
||||
|
||||
/// Remove overlays from the pool beyond a given index. Pool entries up through that index will be kept.
|
||||
fn remove_unused_overlays(overlay_paths: &mut Vec<Vec<LayerId>>, responses: &mut VecDeque<Message>, remove_after_index: usize) {
|
||||
while overlay_paths.len() > remove_after_index {
|
||||
responses.push_back(DocumentMessage::Overlays(Operation::DeleteLayer { path: overlay_paths.pop().unwrap() }.into()).into());
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets a list of snap targets for the X and Y axes in Viewport coords for the target layers (usually all layers or all non-selected layers.)
|
||||
/// This should be called at the start of a drag.
|
||||
pub fn start_snap(&mut self, document_message_handler: &DocumentMessageHandler, target_layers: Vec<&[LayerId]>, ignore_layers: &[Vec<LayerId>]) {
|
||||
pub fn start_snap<'a>(&mut self, document_message_handler: &DocumentMessageHandler, target_layers: impl Iterator<Item = &'a [LayerId]>) {
|
||||
if document_message_handler.snapping_enabled {
|
||||
// Could be made into sorted Vec or a HashSet for more performant lookups.
|
||||
self.snap_targets = Some(
|
||||
target_layers
|
||||
.iter()
|
||||
.filter(|path| !ignore_layers.iter().any(|layer| layer.as_slice() == **path))
|
||||
.filter_map(|path| document_message_handler.graphene_document.viewport_bounding_box(path).ok()?)
|
||||
.flat_map(|[bound1, bound2]| [bound1, bound2, ((bound1 + bound2) / 2.)])
|
||||
.map(|vec| vec.into())
|
||||
@@ -30,7 +110,14 @@ impl SnapHandler {
|
||||
|
||||
/// Finds the closest snap from an array of layers to the specified snap targets in viewport coords.
|
||||
/// Returns 0 for each axis that there is no snap less than the snap tolerance.
|
||||
pub fn snap_layers(&self, document_message_handler: &DocumentMessageHandler, selected_layers: &[Vec<LayerId>], mouse_delta: DVec2) -> DVec2 {
|
||||
pub fn snap_layers(
|
||||
&mut self,
|
||||
responses: &mut VecDeque<Message>,
|
||||
document_message_handler: &DocumentMessageHandler,
|
||||
selected_layers: &[Vec<LayerId>],
|
||||
viewport_bounds: DVec2,
|
||||
mouse_delta: DVec2,
|
||||
) -> DVec2 {
|
||||
if document_message_handler.snapping_enabled {
|
||||
if let Some((targets_x, targets_y)) = &self.snap_targets {
|
||||
let (snap_x, snap_y): (Vec<f64>, Vec<f64>) = selected_layers
|
||||
@@ -40,24 +127,23 @@ impl SnapHandler {
|
||||
.map(|vec| vec.into())
|
||||
.unzip();
|
||||
|
||||
let closest_move = DVec2::new(
|
||||
targets_x
|
||||
.iter()
|
||||
.flat_map(|target| snap_x.iter().map(move |snap| target - mouse_delta.x - snap))
|
||||
.min_by(|a, b| a.abs().partial_cmp(&b.abs()).expect("Could not compare document bounds."))
|
||||
.unwrap_or(0.),
|
||||
targets_y
|
||||
.iter()
|
||||
.flat_map(|target| snap_y.iter().map(move |snap| target - mouse_delta.y - snap))
|
||||
.min_by(|a, b| a.abs().partial_cmp(&b.abs()).expect("Could not compare document bounds."))
|
||||
.unwrap_or(0.),
|
||||
let positions = targets_x.iter().flat_map(|&target| snap_x.iter().map(move |&snap| (target, target - mouse_delta.x - snap)));
|
||||
let distances = targets_y.iter().flat_map(|&target| snap_y.iter().map(move |&snap| (target, target - mouse_delta.y - snap)));
|
||||
|
||||
let min_positions = positions.clone().min_by(|a, b| a.1.abs().partial_cmp(&b.1.abs()).expect("Could not compare position."));
|
||||
let min_distances = distances.clone().min_by(|a, b| a.1.abs().partial_cmp(&b.1.abs()).expect("Could not compare position."));
|
||||
|
||||
let closest_distance = DVec2::new(min_positions.map_or(0., |(_pos, dist)| dist), min_distances.map_or(0., |(_pos, dist)| dist));
|
||||
|
||||
// Clamp, do not move, if above snap tolerance
|
||||
let clamped_closest_distance = DVec2::new(
|
||||
if closest_distance.x.abs() > SNAP_TOLERANCE { 0. } else { closest_distance.x },
|
||||
if closest_distance.y.abs() > SNAP_TOLERANCE { 0. } else { closest_distance.y },
|
||||
);
|
||||
|
||||
// Clamp, do not move if over snap tolerance
|
||||
DVec2::new(
|
||||
if closest_move.x.abs() > SNAP_TOLERANCE { 0. } else { closest_move.x },
|
||||
if closest_move.y.abs() > SNAP_TOLERANCE { 0. } else { closest_move.y },
|
||||
)
|
||||
Self::update_overlays(&mut self.overlay_paths, responses, viewport_bounds, (positions, distances), clamped_closest_distance);
|
||||
|
||||
clamped_closest_distance
|
||||
} else {
|
||||
DVec2::ZERO
|
||||
}
|
||||
@@ -67,30 +153,26 @@ impl SnapHandler {
|
||||
}
|
||||
|
||||
/// Handles snapping of a viewport position, returning another viewport position.
|
||||
pub fn snap_position(&self, document_message_handler: &DocumentMessageHandler, position_viewport: DVec2) -> DVec2 {
|
||||
pub fn snap_position(&mut self, responses: &mut VecDeque<Message>, viewport_bounds: DVec2, document_message_handler: &DocumentMessageHandler, position_viewport: DVec2) -> DVec2 {
|
||||
if document_message_handler.snapping_enabled {
|
||||
if let Some((targets_x, targets_y)) = &self.snap_targets {
|
||||
// For each list of snap targets, find the shortest distance to move the point to that target.
|
||||
let closest_move = DVec2::new(
|
||||
targets_x
|
||||
.iter()
|
||||
.map(|x| (x - position_viewport.x))
|
||||
.min_by(|a, b| a.abs().partial_cmp(&b.abs()).expect("Could not compare document bounds."))
|
||||
.unwrap_or(0.),
|
||||
targets_y
|
||||
.iter()
|
||||
.map(|y| (y - position_viewport.y))
|
||||
.min_by(|a, b| a.abs().partial_cmp(&b.abs()).expect("Could not compare document bounds."))
|
||||
.unwrap_or(0.),
|
||||
);
|
||||
let positions = targets_x.iter().map(|&x| (x, x - position_viewport.x));
|
||||
let distances = targets_y.iter().map(|&y| (y, y - position_viewport.y));
|
||||
|
||||
let min_positions = positions.clone().min_by(|a, b| a.1.abs().partial_cmp(&b.1.abs()).expect("Could not compare position."));
|
||||
let min_distances = distances.clone().min_by(|a, b| a.1.abs().partial_cmp(&b.1.abs()).expect("Could not compare position."));
|
||||
|
||||
let closest_distance = DVec2::new(min_positions.map_or(0., |(_pos, dist)| dist), min_distances.map_or(0., |(_pos, dist)| dist));
|
||||
|
||||
// Do not move if over snap tolerance
|
||||
let clamped_closest_move = DVec2::new(
|
||||
if closest_move.x.abs() > SNAP_TOLERANCE { 0. } else { closest_move.x },
|
||||
if closest_move.y.abs() > SNAP_TOLERANCE { 0. } else { closest_move.y },
|
||||
let clamped_closest_distance = DVec2::new(
|
||||
if closest_distance.x.abs() > SNAP_TOLERANCE { 0. } else { closest_distance.x },
|
||||
if closest_distance.y.abs() > SNAP_TOLERANCE { 0. } else { closest_distance.y },
|
||||
);
|
||||
|
||||
position_viewport + clamped_closest_move
|
||||
Self::update_overlays(&mut self.overlay_paths, responses, viewport_bounds, (positions, distances), clamped_closest_distance);
|
||||
|
||||
position_viewport + clamped_closest_distance
|
||||
} else {
|
||||
position_viewport
|
||||
}
|
||||
@@ -99,8 +181,9 @@ impl SnapHandler {
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes snap target data. Call this when snapping is done.
|
||||
pub fn cleanup(&mut self) {
|
||||
/// Removes snap target data and overlays. Call this when snapping is done.
|
||||
pub fn cleanup(&mut self, responses: &mut VecDeque<Message>) {
|
||||
Self::remove_unused_overlays(&mut self.overlay_paths, responses, 0);
|
||||
self.snap_targets = None;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user