Files
Graphite/editor/src/viewport_tools/snapping.rs
0HyperCube d5236f485a 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 commit ddcc23865a.

* 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:

commit dbc19912ca
Author: 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

commit 9b36e6a909
Author: Keavon Chambers <keavon@keavon.com>
Date:   Sat Jan 15 17:24:58 2022 -0800

    Fix all remaining Vue/TS errors flagged in Volar

commit 2cc39cdb37
Author: Keavon Chambers <keavon@keavon.com>
Date:   Sat Jan 15 12:55:51 2022 -0800

    Tweak whitespace around `use` statements and other lint fixes

commit fa390c3875
Author: Keavon Chambers <keavon@keavon.com>
Date:   Sat Jan 15 06:35:06 2022 -0800

    Change canvas artboard background color to be 1 shade lighter

commit ea2d003484
Author: 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

commit 9b6cbb5f50
Author: 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

commit a535f5c1c1
Author: Keavon Chambers <keavon@keavon.com>
Date:   Wed Jan 12 16:17:40 2022 -0800

    Sort messages and message handlers

commit e70858884d
Author: Keavon Chambers <keavon@keavon.com>
Date:   Wed Jan 12 14:16:13 2022 -0800

    Standardize FrontendMessage message names

commit 0b4934b4f3
Author: Keavon Chambers <keavon@keavon.com>
Date:   Wed Jan 12 12:45:07 2022 -0800

    Rename document_file.rs to document_message_handler.rs

commit ec7bf4a771
Author: Keavon Chambers <keavon@keavon.com>
Date:   Wed Jan 12 12:19:14 2022 -0800

    Rename DocumentsMessage to PortfolioMessage

commit 0991312ba0
Author: Keavon Chambers <keavon@keavon.com>
Date:   Wed Jan 12 11:44:49 2022 -0800

    Rename document_message_handler.rs to portfolio_message_handler.rs

commit c76c92e500
Author: 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:

    commit 095d577a49
    Author: Keavon Chambers <keavon@keavon.com>
    Date:   Mon Jan 10 18:06:12 2022 -0800

        Fix NumberInput clamping regression with undefined bounds

    commit 9f54a376c4
    Author: 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>

    commit 61432de480
    Author: 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

commit 574028375b
Author: Keavon Chambers <keavon@keavon.com>
Date:   Wed Jan 12 06:14:32 2022 -0800

    Add `npm start` script alias for `npm run serve`

commit 75e8fc6614
Author: Keavon Chambers <keavon@keavon.com>
Date:   Wed Jan 12 05:17:56 2022 -0800

    Switch VS Code's Vue extension from Vetur to Volar

commit 389b445ef1
Author: Keavon Chambers <keavon@keavon.com>
Date:   Wed Jan 12 03:56:28 2022 -0800

    Remove Charcoal references from the code for now

commit 095d577a49
Author: Keavon Chambers <keavon@keavon.com>
Date:   Mon Jan 10 18:06:12 2022 -0800

    Fix NumberInput clamping regression with undefined bounds

commit 9f54a376c4
Author: 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>

commit 61432de480
Author: 0HyperCube <78500760+0HyperCube@users.noreply.github.com>
Date:   Sat Jan 8 21:06:15 2022 +0000

    Fix rotation input (#472)

commit 3eeac79f5a
Author: 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>

commit a2c2f7fc9d
Author: 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>

commit 54e9121115
Author: 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>

commit 11f15bd6af
Author: 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>

commit 05e8a98f46
Author: Keavon Chambers <keavon@keavon.com>
Date:   Fri Jan 7 23:13:33 2022 -0800

    Redesign the Layer Tree UI (#468)

commit 8e3d237228
Author: 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>

commit c1c7192948
Author: 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>

commit f4707f9b74
Author: 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

commit 0219f060b8
Author: 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>
2022-01-20 20:35:40 +00:00

190 lines
8.2 KiB
Rust

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::layers::style::{self, Stroke};
use graphene::{LayerId, Operation};
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<'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
.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())
.unzip(),
);
}
}
/// 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(
&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
.iter()
.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())
.unzip();
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 },
);
Self::update_overlays(&mut self.overlay_paths, responses, viewport_bounds, (positions, distances), clamped_closest_distance);
clamped_closest_distance
} else {
DVec2::ZERO
}
} else {
DVec2::ZERO
}
}
/// Handles snapping of a viewport position, returning another viewport position.
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 {
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_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 },
);
Self::update_overlays(&mut self.overlay_paths, responses, viewport_bounds, (positions, distances), clamped_closest_distance);
position_viewport + clamped_closest_distance
} else {
position_viewport
}
} else {
position_viewport
}
}
/// 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;
}
}