mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-24 15:28:11 +08:00
Restructure the entire editor codebase to consistently match the message hierarchy
Closes #744
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
pub mod overlay_renderer;
|
||||
pub mod path_outline;
|
||||
pub mod resize;
|
||||
pub mod shape_editor;
|
||||
pub mod snapping;
|
||||
pub mod transformation_cage;
|
||||
@@ -0,0 +1,314 @@
|
||||
use crate::application::generate_uuid;
|
||||
use crate::consts::VIEWPORT_GRID_ROUNDING_BIAS;
|
||||
use crate::consts::{COLOR_ACCENT, MANIPULATOR_GROUP_MARKER_SIZE, PATH_OUTLINE_WEIGHT};
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
use graphene::color::Color;
|
||||
use graphene::document::Document;
|
||||
use graphene::layers::style::{self, Fill, Stroke};
|
||||
use graphene::layers::vector::consts::ManipulatorType;
|
||||
use graphene::layers::vector::manipulator_group::ManipulatorGroup;
|
||||
use graphene::layers::vector::manipulator_point::ManipulatorPoint;
|
||||
use graphene::layers::vector::subpath::Subpath;
|
||||
use graphene::{LayerId, Operation};
|
||||
|
||||
use glam::{DAffine2, DVec2};
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
|
||||
/// [ManipulatorGroupOverlay]s is the collection of overlays that make up an [ManipulatorGroup] visible in the editor.
|
||||
type ManipulatorGroupOverlays = [Option<Vec<LayerId>>; 5];
|
||||
type ManipulatorId = u64;
|
||||
|
||||
const POINT_STROKE_WEIGHT: f64 = 2.;
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct OverlayRenderer {
|
||||
shape_overlay_cache: HashMap<LayerId, Vec<LayerId>>,
|
||||
manipulator_group_overlay_cache: HashMap<(LayerId, ManipulatorId), ManipulatorGroupOverlays>,
|
||||
}
|
||||
|
||||
impl OverlayRenderer {
|
||||
pub fn new() -> Self {
|
||||
OverlayRenderer {
|
||||
manipulator_group_overlay_cache: HashMap::new(),
|
||||
shape_overlay_cache: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_subpath_overlays(&mut self, document: &Document, layer_path: Vec<LayerId>, responses: &mut VecDeque<Message>) {
|
||||
let transform = document.generate_transform_relative_to_viewport(&layer_path).ok().unwrap();
|
||||
if let Ok(layer) = document.layer(&layer_path) {
|
||||
let layer_id = layer_path.last().unwrap();
|
||||
self.layer_overlay_visibility(document, layer_path.clone(), true, responses);
|
||||
|
||||
if let Some(shape) = layer.as_subpath() {
|
||||
let outline_cache = self.shape_overlay_cache.get(layer_id);
|
||||
log::trace!("Overlay: Outline cache {:?}", &outline_cache);
|
||||
|
||||
// Create an outline if we do not have a cached one
|
||||
if outline_cache == None {
|
||||
let outline_path = self.create_shape_outline_overlay(shape.clone(), responses);
|
||||
self.shape_overlay_cache.insert(*layer_id, outline_path.clone());
|
||||
Self::place_outline_overlays(outline_path.clone(), &transform, responses);
|
||||
log::trace!("Overlay: Creating new outline {:?}", &outline_path);
|
||||
} else if let Some(outline_path) = outline_cache {
|
||||
log::trace!("Overlay: Updating overlays for {:?} owning layer: {:?}", outline_path, layer_id);
|
||||
Self::modify_outline_overlays(outline_path.clone(), shape.clone(), responses);
|
||||
Self::place_outline_overlays(outline_path.clone(), &transform, responses);
|
||||
}
|
||||
|
||||
// Create, place, and style the manipulator overlays
|
||||
for (manipulator_group_id, manipulator_group) in shape.manipulator_groups().enumerate() {
|
||||
let manipulator_group_cache = self.manipulator_group_overlay_cache.get_mut(&(*layer_id, *manipulator_group_id));
|
||||
|
||||
// If cached update placement and style
|
||||
if let Some(manipulator_group_overlays) = manipulator_group_cache {
|
||||
log::trace!("Overlay: Updating detail overlays for {:?}", manipulator_group_overlays);
|
||||
Self::place_manipulator_group_overlays(manipulator_group, manipulator_group_overlays, &transform, responses);
|
||||
Self::style_overlays(manipulator_group, manipulator_group_overlays, responses);
|
||||
} else {
|
||||
// Create if not cached
|
||||
let mut manipulator_group_overlays = [
|
||||
Some(self.create_anchor_overlay(responses)),
|
||||
Self::create_handle_overlay_if_exists(&manipulator_group.points[ManipulatorType::InHandle], responses),
|
||||
Self::create_handle_overlay_if_exists(&manipulator_group.points[ManipulatorType::OutHandle], responses),
|
||||
Self::create_handle_line_overlay_if_exists(&manipulator_group.points[ManipulatorType::InHandle], responses),
|
||||
Self::create_handle_line_overlay_if_exists(&manipulator_group.points[ManipulatorType::OutHandle], responses),
|
||||
];
|
||||
Self::place_manipulator_group_overlays(manipulator_group, &mut manipulator_group_overlays, &transform, responses);
|
||||
Self::style_overlays(manipulator_group, &manipulator_group_overlays, responses);
|
||||
self.manipulator_group_overlay_cache.insert((*layer_id, *manipulator_group_id), manipulator_group_overlays);
|
||||
}
|
||||
}
|
||||
// TODO Handle removing shapes from cache so we don't memory leak
|
||||
// Eventually will get replaced with am immediate mode renderer for overlays
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear_subpath_overlays(&mut self, document: &Document, layer_path: Vec<LayerId>, responses: &mut VecDeque<Message>) {
|
||||
let layer_id = layer_path.last().unwrap();
|
||||
|
||||
// Remove the shape outline overlays
|
||||
if let Some(overlay_path) = self.shape_overlay_cache.get(layer_id) {
|
||||
Self::remove_outline_overlays(overlay_path.clone(), responses)
|
||||
}
|
||||
self.shape_overlay_cache.remove(layer_id);
|
||||
|
||||
// Remove the ManipulatorGroup overlays
|
||||
if let Ok(layer) = document.layer(&layer_path) {
|
||||
if let Some(shape) = layer.as_subpath() {
|
||||
for (id, _) in shape.manipulator_groups().enumerate() {
|
||||
if let Some(manipulator_group_overlays) = self.manipulator_group_overlay_cache.get(&(*layer_id, *id)) {
|
||||
Self::remove_manipulator_group_overlays(manipulator_group_overlays, responses);
|
||||
self.manipulator_group_overlay_cache.remove(&(*layer_id, *id));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn layer_overlay_visibility(&mut self, document: &Document, layer_path: Vec<LayerId>, visibility: bool, responses: &mut VecDeque<Message>) {
|
||||
let layer_id = layer_path.last().unwrap();
|
||||
|
||||
// Hide the shape outline overlays
|
||||
if let Some(overlay_path) = self.shape_overlay_cache.get(layer_id) {
|
||||
Self::set_outline_overlay_visibility(overlay_path.clone(), visibility, responses);
|
||||
}
|
||||
|
||||
// Hide the manipulator group overlays
|
||||
if let Ok(layer) = document.layer(&layer_path) {
|
||||
if let Some(shape) = layer.as_subpath() {
|
||||
for (id, _) in shape.manipulator_groups().enumerate() {
|
||||
if let Some(manipulator_group_overlays) = self.manipulator_group_overlay_cache.get(&(*layer_id, *id)) {
|
||||
Self::set_manipulator_group_overlay_visibility(manipulator_group_overlays, visibility, responses);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Create the kurbo shape that matches the selected viewport shape.
|
||||
fn create_shape_outline_overlay(&self, subpath: Subpath, responses: &mut VecDeque<Message>) -> Vec<LayerId> {
|
||||
let layer_path = vec![generate_uuid()];
|
||||
let operation = Operation::AddShape {
|
||||
path: layer_path.clone(),
|
||||
subpath,
|
||||
style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, PATH_OUTLINE_WEIGHT)), Fill::None),
|
||||
insert_index: -1,
|
||||
transform: DAffine2::IDENTITY.to_cols_array(),
|
||||
};
|
||||
responses.push_back(DocumentMessage::Overlays(operation.into()).into());
|
||||
|
||||
layer_path
|
||||
}
|
||||
|
||||
/// Create a single anchor overlay and return its layer ID.
|
||||
fn create_anchor_overlay(&self, responses: &mut VecDeque<Message>) -> Vec<LayerId> {
|
||||
let layer_path = vec![generate_uuid()];
|
||||
let operation = Operation::AddRect {
|
||||
path: layer_path.clone(),
|
||||
transform: DAffine2::IDENTITY.to_cols_array(),
|
||||
style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 2.0)), Fill::solid(Color::WHITE)),
|
||||
insert_index: -1,
|
||||
};
|
||||
responses.push_back(DocumentMessage::Overlays(operation.into()).into());
|
||||
layer_path
|
||||
}
|
||||
|
||||
/// Create a single handle overlay and return its layer ID.
|
||||
fn create_handle_overlay(responses: &mut VecDeque<Message>) -> Vec<LayerId> {
|
||||
let layer_path = vec![generate_uuid()];
|
||||
let operation = Operation::AddEllipse {
|
||||
path: layer_path.clone(),
|
||||
transform: DAffine2::IDENTITY.to_cols_array(),
|
||||
style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 2.0)), Fill::solid(Color::WHITE)),
|
||||
insert_index: -1,
|
||||
};
|
||||
responses.push_back(DocumentMessage::Overlays(operation.into()).into());
|
||||
layer_path
|
||||
}
|
||||
|
||||
/// Create a single handle overlay and return its layer id if it exists.
|
||||
fn create_handle_overlay_if_exists(handle: &Option<ManipulatorPoint>, responses: &mut VecDeque<Message>) -> Option<Vec<LayerId>> {
|
||||
handle.as_ref().map(|_| Self::create_handle_overlay(responses))
|
||||
}
|
||||
|
||||
/// Create the shape outline overlay and return its layer ID.
|
||||
fn create_handle_line_overlay(responses: &mut VecDeque<Message>) -> Vec<LayerId> {
|
||||
let layer_path = vec![generate_uuid()];
|
||||
let operation = Operation::AddLine {
|
||||
path: layer_path.clone(),
|
||||
transform: DAffine2::IDENTITY.to_cols_array(),
|
||||
style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 1.0)), Fill::None),
|
||||
insert_index: -1,
|
||||
};
|
||||
responses.push_front(DocumentMessage::Overlays(operation.into()).into());
|
||||
layer_path
|
||||
}
|
||||
|
||||
/// Create the shape outline overlay and return its layer ID.
|
||||
fn create_handle_line_overlay_if_exists(handle: &Option<ManipulatorPoint>, responses: &mut VecDeque<Message>) -> Option<Vec<LayerId>> {
|
||||
handle.as_ref().map(|_| Self::create_handle_line_overlay(responses))
|
||||
}
|
||||
|
||||
fn place_outline_overlays(outline_path: Vec<LayerId>, parent_transform: &DAffine2, responses: &mut VecDeque<Message>) {
|
||||
let transform_message = Self::overlay_transform_message(outline_path, parent_transform.to_cols_array());
|
||||
responses.push_back(transform_message);
|
||||
}
|
||||
|
||||
fn modify_outline_overlays(outline_path: Vec<LayerId>, subpath: Subpath, responses: &mut VecDeque<Message>) {
|
||||
let outline_modify_message = Self::overlay_modify_message(outline_path, subpath);
|
||||
responses.push_back(outline_modify_message);
|
||||
}
|
||||
|
||||
/// Updates the position of the overlays based on the [Subpath] points.
|
||||
fn place_manipulator_group_overlays(manipulator_group: &ManipulatorGroup, overlays: &mut ManipulatorGroupOverlays, parent_transform: &DAffine2, responses: &mut VecDeque<Message>) {
|
||||
if let Some(manipulator_point) = &manipulator_group.points[ManipulatorType::Anchor] {
|
||||
// Helper function to keep things DRY (don't-repeat-yourself)
|
||||
let mut place_handle_and_line = |handle: &ManipulatorPoint, line_source: &mut Option<Vec<LayerId>>, marker_source: &mut Option<Vec<LayerId>>| {
|
||||
let line_overlay = line_source.take().unwrap_or_else(|| Self::create_handle_line_overlay(responses));
|
||||
let line_vector = parent_transform.transform_point2(manipulator_point.position) - parent_transform.transform_point2(handle.position);
|
||||
let scale = DVec2::splat(line_vector.length());
|
||||
let angle = -line_vector.angle_between(DVec2::X);
|
||||
let translation = (parent_transform.transform_point2(handle.position) + VIEWPORT_GRID_ROUNDING_BIAS).round() + DVec2::splat(0.5);
|
||||
let transform = DAffine2::from_scale_angle_translation(scale, angle, translation).to_cols_array();
|
||||
responses.push_back(Self::overlay_transform_message(line_overlay.clone(), transform));
|
||||
*line_source = Some(line_overlay);
|
||||
|
||||
let marker_overlay = marker_source.take().unwrap_or_else(|| Self::create_handle_overlay(responses));
|
||||
let scale = DVec2::splat(MANIPULATOR_GROUP_MARKER_SIZE);
|
||||
let angle = 0.;
|
||||
let translation = (parent_transform.transform_point2(handle.position) - (scale / 2.) + VIEWPORT_GRID_ROUNDING_BIAS).round();
|
||||
let transform = DAffine2::from_scale_angle_translation(scale, angle, translation).to_cols_array();
|
||||
responses.push_back(Self::overlay_transform_message(marker_overlay.clone(), transform));
|
||||
*marker_source = Some(marker_overlay);
|
||||
};
|
||||
|
||||
// Place the handle overlays
|
||||
let [_, h1, h2] = &manipulator_group.points;
|
||||
let [a, b, c, line1, line2] = overlays;
|
||||
let markers = [a, b, c];
|
||||
if let Some(handle) = &h1 {
|
||||
place_handle_and_line(handle, line1, markers[handle.manipulator_type as usize]);
|
||||
}
|
||||
if let Some(handle) = &h2 {
|
||||
place_handle_and_line(handle, line2, markers[handle.manipulator_type as usize]);
|
||||
}
|
||||
|
||||
// Place the anchor point overlay
|
||||
if let Some(anchor_overlay) = &overlays[ManipulatorType::Anchor as usize] {
|
||||
let scale = DVec2::splat(MANIPULATOR_GROUP_MARKER_SIZE);
|
||||
let angle = 0.;
|
||||
let translation = (parent_transform.transform_point2(manipulator_point.position) - (scale / 2.) + VIEWPORT_GRID_ROUNDING_BIAS).round();
|
||||
let transform = DAffine2::from_scale_angle_translation(scale, angle, translation).to_cols_array();
|
||||
|
||||
let message = Self::overlay_transform_message(anchor_overlay.clone(), transform);
|
||||
responses.push_back(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes the manipulator overlays from the overlay document.
|
||||
fn remove_manipulator_group_overlays(overlay_paths: &ManipulatorGroupOverlays, responses: &mut VecDeque<Message>) {
|
||||
overlay_paths.iter().flatten().for_each(|layer_id| {
|
||||
log::trace!("Overlay: Sending delete message for: {:?}", layer_id);
|
||||
responses.push_back(DocumentMessage::Overlays(Operation::DeleteLayer { path: layer_id.clone() }.into()).into());
|
||||
});
|
||||
}
|
||||
|
||||
fn remove_outline_overlays(overlay_path: Vec<LayerId>, responses: &mut VecDeque<Message>) {
|
||||
responses.push_back(DocumentMessage::Overlays(Operation::DeleteLayer { path: overlay_path }.into()).into());
|
||||
}
|
||||
|
||||
/// Sets the visibility of the handles overlay.
|
||||
fn set_manipulator_group_overlay_visibility(manipulator_group_overlays: &ManipulatorGroupOverlays, visibility: bool, responses: &mut VecDeque<Message>) {
|
||||
manipulator_group_overlays.iter().flatten().for_each(|layer_id| {
|
||||
responses.push_back(Self::overlay_visibility_message(layer_id.clone(), visibility));
|
||||
});
|
||||
}
|
||||
|
||||
fn set_outline_overlay_visibility(overlay_path: Vec<LayerId>, visibility: bool, responses: &mut VecDeque<Message>) {
|
||||
responses.push_back(Self::overlay_visibility_message(overlay_path, visibility));
|
||||
}
|
||||
|
||||
/// Create a visibility message for an overlay.
|
||||
fn overlay_visibility_message(layer_path: Vec<LayerId>, visibility: bool) -> Message {
|
||||
DocumentMessage::Overlays(
|
||||
Operation::SetLayerVisibility {
|
||||
path: layer_path,
|
||||
visible: visibility,
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.into()
|
||||
}
|
||||
|
||||
/// Create a transform message for an overlay.
|
||||
fn overlay_transform_message(layer_path: Vec<LayerId>, transform: [f64; 6]) -> Message {
|
||||
DocumentMessage::Overlays(Operation::SetLayerTransformInViewport { path: layer_path, transform }.into()).into()
|
||||
}
|
||||
|
||||
/// Create an update message for an overlay.
|
||||
fn overlay_modify_message(layer_path: Vec<LayerId>, subpath: Subpath) -> Message {
|
||||
DocumentMessage::Overlays(Operation::SetShapePath { path: layer_path, subpath }.into()).into()
|
||||
}
|
||||
|
||||
/// Sets the overlay style for this point.
|
||||
fn style_overlays(manipulator_group: &ManipulatorGroup, overlays: &ManipulatorGroupOverlays, responses: &mut VecDeque<Message>) {
|
||||
// TODO Move the style definitions out of the Subpath, should be looked up from a stylesheet or similar
|
||||
let selected_style = style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, POINT_STROKE_WEIGHT + 1.0)), Fill::solid(COLOR_ACCENT));
|
||||
let deselected_style = style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, POINT_STROKE_WEIGHT)), Fill::solid(Color::WHITE));
|
||||
|
||||
// Update if the manipulator points are shown as selected
|
||||
// Here the index is important, even though overlays[..] has five elements we only care about the first three
|
||||
for (index, point) in manipulator_group.points.iter().enumerate() {
|
||||
if let Some(point) = point {
|
||||
if let Some(overlay) = &overlays[index] {
|
||||
let style = if point.editor_state.is_selected { selected_style.clone() } else { deselected_style.clone() };
|
||||
responses.push_back(DocumentMessage::Overlays(Operation::SetLayerStyle { path: overlay.clone(), style }.into()).into());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
use crate::application::generate_uuid;
|
||||
use crate::consts::{COLOR_ACCENT, PATH_OUTLINE_WEIGHT, SELECTION_TOLERANCE};
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
use graphene::intersection::Quad;
|
||||
use graphene::layers::layer_info::LayerDataType;
|
||||
use graphene::layers::style::{self, Fill, Stroke};
|
||||
use graphene::layers::text_layer::FontCache;
|
||||
use graphene::layers::vector::subpath::Subpath;
|
||||
use graphene::{LayerId, Operation};
|
||||
|
||||
use glam::{DAffine2, DVec2};
|
||||
use std::collections::VecDeque;
|
||||
|
||||
/// Manages the overlay used by the select tool for outlining selected shapes and when hovering over a non selected shape.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct PathOutline {
|
||||
hovered_layer_path: Option<Vec<LayerId>>,
|
||||
hovered_overlay_path: Option<Vec<LayerId>>,
|
||||
selected_overlay_paths: Vec<Vec<LayerId>>,
|
||||
}
|
||||
|
||||
impl PathOutline {
|
||||
/// Creates an outline of a layer either with a pre-existing overlay or by generating a new one
|
||||
fn create_outline(
|
||||
document_layer_path: Vec<LayerId>,
|
||||
overlay_path: Option<Vec<LayerId>>,
|
||||
document: &DocumentMessageHandler,
|
||||
responses: &mut VecDeque<Message>,
|
||||
font_cache: &FontCache,
|
||||
) -> Option<Vec<LayerId>> {
|
||||
// Get layer data
|
||||
let document_layer = document.graphene_document.layer(&document_layer_path).ok()?;
|
||||
|
||||
// TODO Purge this area of BezPath and Kurbo
|
||||
// Get the bezpath from the shape or text
|
||||
let subpath = match &document_layer.data {
|
||||
LayerDataType::Shape(layer_shape) => Some(layer_shape.shape.clone()),
|
||||
LayerDataType::Text(text) => Some(text.to_subpath_nonmut(font_cache)),
|
||||
_ => document_layer.aabb_for_transform(DAffine2::IDENTITY, font_cache).map(|[p1, p2]| Subpath::new_rect(p1, p2)),
|
||||
}?;
|
||||
|
||||
// Generate a new overlay layer if necessary
|
||||
let overlay = match overlay_path {
|
||||
Some(path) => path,
|
||||
None => {
|
||||
let overlay_path = vec![generate_uuid()];
|
||||
let operation = Operation::AddShape {
|
||||
path: overlay_path.clone(),
|
||||
subpath: Default::default(),
|
||||
style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, PATH_OUTLINE_WEIGHT)), Fill::None),
|
||||
insert_index: -1,
|
||||
transform: DAffine2::IDENTITY.to_cols_array(),
|
||||
};
|
||||
|
||||
responses.push_back(DocumentMessage::Overlays(operation.into()).into());
|
||||
|
||||
overlay_path
|
||||
}
|
||||
};
|
||||
|
||||
// Update the shape bezpath
|
||||
let operation = Operation::SetShapePath { path: overlay.clone(), subpath };
|
||||
responses.push_back(DocumentMessage::Overlays(operation.into()).into());
|
||||
|
||||
// Update the transform to match the document
|
||||
let operation = Operation::SetLayerTransform {
|
||||
path: overlay.clone(),
|
||||
transform: document.graphene_document.multiply_transforms(&document_layer_path).unwrap().to_cols_array(),
|
||||
};
|
||||
responses.push_back(DocumentMessage::Overlays(operation.into()).into());
|
||||
|
||||
Some(overlay)
|
||||
}
|
||||
|
||||
/// Removes the hovered overlay and deletes path references
|
||||
pub fn clear_hovered(&mut self, responses: &mut VecDeque<Message>) {
|
||||
if let Some(path) = self.hovered_overlay_path.take() {
|
||||
let operation = Operation::DeleteLayer { path };
|
||||
responses.push_back(DocumentMessage::Overlays(operation.into()).into());
|
||||
}
|
||||
self.hovered_layer_path = None;
|
||||
}
|
||||
|
||||
/// Performs an intersect test and generates a hovered overlay if necessary
|
||||
pub fn intersect_test_hovered(&mut self, input: &InputPreprocessorMessageHandler, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>, font_cache: &FontCache) {
|
||||
// Get the layer the user is hovering over
|
||||
let tolerance = DVec2::splat(SELECTION_TOLERANCE);
|
||||
let quad = Quad::from_box([input.mouse.position - tolerance, input.mouse.position + tolerance]);
|
||||
let mut intersection = document.graphene_document.intersects_quad_root(quad, font_cache);
|
||||
|
||||
// If the user is hovering over a layer they have not already selected, then update outline
|
||||
if let Some(path) = intersection.pop() {
|
||||
if !document.selected_visible_layers().any(|visible| visible == path.as_slice()) {
|
||||
// Updates the overlay, generating a new one if necessary
|
||||
self.hovered_overlay_path = Self::create_outline(path.clone(), self.hovered_overlay_path.take(), document, responses, font_cache);
|
||||
if self.hovered_overlay_path.is_none() {
|
||||
self.clear_hovered(responses);
|
||||
}
|
||||
|
||||
self.hovered_layer_path = Some(path);
|
||||
} else {
|
||||
self.clear_hovered(responses);
|
||||
}
|
||||
} else {
|
||||
self.clear_hovered(responses);
|
||||
}
|
||||
}
|
||||
|
||||
/// Clears overlays for the selected paths and removes references
|
||||
pub fn clear_selected(&mut self, responses: &mut VecDeque<Message>) {
|
||||
while let Some(path) = self.selected_overlay_paths.pop() {
|
||||
let operation = Operation::DeleteLayer { path };
|
||||
responses.push_back(DocumentMessage::Overlays(operation.into()).into());
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the selected overlays, generating or removing overlays if necessary
|
||||
pub fn update_selected<'a>(&mut self, selected: impl Iterator<Item = &'a [LayerId]>, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>, font_cache: &FontCache) {
|
||||
let mut old_overlay_paths = std::mem::take(&mut self.selected_overlay_paths);
|
||||
|
||||
for document_layer_path in selected {
|
||||
if let Some(overlay_path) = Self::create_outline(document_layer_path.to_vec(), old_overlay_paths.pop(), document, responses, font_cache) {
|
||||
self.selected_overlay_paths.push(overlay_path);
|
||||
}
|
||||
}
|
||||
for path in old_overlay_paths {
|
||||
let operation = Operation::DeleteLayer { path };
|
||||
responses.push_back(DocumentMessage::Overlays(operation.into()).into());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
use crate::messages::input_mapper::utility_types::input_keyboard::Key;
|
||||
use crate::messages::input_mapper::utility_types::input_mouse::ViewportPosition;
|
||||
use crate::messages::prelude::*;
|
||||
use crate::messages::tool::common_functionality::snapping::SnapManager;
|
||||
|
||||
use graphene::layers::text_layer::FontCache;
|
||||
use graphene::LayerId;
|
||||
use graphene::Operation;
|
||||
|
||||
use glam::{DAffine2, DVec2, Vec2Swizzles};
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Resize {
|
||||
pub drag_start: ViewportPosition,
|
||||
pub path: Option<Vec<LayerId>>,
|
||||
snap_manager: SnapManager,
|
||||
}
|
||||
|
||||
impl Resize {
|
||||
/// Starts a resize, assigning the snap targets and snapping the starting position.
|
||||
pub fn start(&mut self, responses: &mut VecDeque<Message>, document: &DocumentMessageHandler, mouse_position: DVec2, font_cache: &FontCache) {
|
||||
self.snap_manager.start_snap(document, document.bounding_boxes(None, None, font_cache), true, true);
|
||||
self.snap_manager.add_all_document_handles(document, &[], &[], &[]);
|
||||
self.drag_start = self.snap_manager.snap_position(responses, document, mouse_position);
|
||||
}
|
||||
|
||||
pub fn calculate_transform(
|
||||
&mut self,
|
||||
responses: &mut VecDeque<Message>,
|
||||
document: &DocumentMessageHandler,
|
||||
center: Key,
|
||||
lock_ratio: Key,
|
||||
ipp: &InputPreprocessorMessageHandler,
|
||||
) -> Option<Message> {
|
||||
if let Some(path) = &self.path {
|
||||
let mut start = self.drag_start;
|
||||
|
||||
let stop = self.snap_manager.snap_position(responses, document, ipp.mouse.position);
|
||||
|
||||
let mut size = stop - start;
|
||||
if ipp.keyboard.get(lock_ratio as usize) {
|
||||
size = size.abs().max(size.abs().yx()) * size.signum();
|
||||
}
|
||||
if ipp.keyboard.get(center as usize) {
|
||||
start -= size;
|
||||
size *= 2.;
|
||||
}
|
||||
|
||||
Some(
|
||||
Operation::SetLayerTransformInViewport {
|
||||
path: path.to_vec(),
|
||||
transform: DAffine2::from_scale_angle_translation(size, 0., start).to_cols_array(),
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cleanup(&mut self, responses: &mut VecDeque<Message>) {
|
||||
self.snap_manager.cleanup(responses);
|
||||
self.path = None;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
use graphene::layers::vector::consts::ManipulatorType;
|
||||
use graphene::layers::vector::manipulator_group::ManipulatorGroup;
|
||||
use graphene::layers::vector::manipulator_point::ManipulatorPoint;
|
||||
use graphene::layers::vector::subpath::Subpath;
|
||||
use graphene::{LayerId, Operation};
|
||||
|
||||
use glam::DVec2;
|
||||
use graphene::document::Document;
|
||||
use std::collections::VecDeque;
|
||||
|
||||
/// ShapeEditor is the container for all of the layer paths that are represented as [Subpath]s and provides
|
||||
/// functionality required to query and create the [Subpath] / [ManipulatorGroup]s / [ManipulatorPoint]s.
|
||||
///
|
||||
/// Overview:
|
||||
/// ```text
|
||||
/// ShapeEditor
|
||||
/// |
|
||||
/// selected_layers <- Paths to selected layers that may contain Subpaths
|
||||
/// / | \
|
||||
/// Subpath ... Subpath <- Reference from layer paths, one Subpath per layer (for now, will eventually be a CompoundPath)
|
||||
/// / | \
|
||||
/// ManipulatorGroup ... ManipulatorGroup <- Subpath contains many ManipulatorGroups
|
||||
/// ```
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct ShapeEditor {
|
||||
// The layers we can select and edit manipulators (anchors and handles) from
|
||||
selected_layers: Vec<Vec<LayerId>>,
|
||||
}
|
||||
|
||||
// TODO Consider keeping a list of selected manipulators to minimize traversals of the layers
|
||||
impl ShapeEditor {
|
||||
/// Select the first point within the selection threshold.
|
||||
/// Returns the points if found, None otherwise.
|
||||
pub fn select_point(
|
||||
&self,
|
||||
document: &Document,
|
||||
mouse_position: DVec2,
|
||||
select_threshold: f64,
|
||||
add_to_selection: bool,
|
||||
responses: &mut VecDeque<Message>,
|
||||
) -> Option<Vec<(&[LayerId], u64, ManipulatorType)>> {
|
||||
if self.selected_layers.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Some((shape_layer_path, manipulator_group_id, manipulator_point_index)) = self.find_nearest_point_indices(document, mouse_position, select_threshold) {
|
||||
log::trace!("Selecting... manipulator group ID: {}, manipulator point index: {}", manipulator_group_id, manipulator_point_index);
|
||||
|
||||
// If the point we're selecting has already been selected
|
||||
// we can assume this point exists.. since we did just click on it hence the unwrap
|
||||
let is_point_selected = self.shape(document, shape_layer_path).unwrap().manipulator_groups().by_id(manipulator_group_id).unwrap().points[manipulator_point_index]
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.editor_state
|
||||
.is_selected;
|
||||
|
||||
let point_position = self.shape(document, shape_layer_path).unwrap().manipulator_groups().by_id(manipulator_group_id).unwrap().points[manipulator_point_index]
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.position;
|
||||
|
||||
// The currently selected points (which are then modified to reflect the selection)
|
||||
let mut points = self
|
||||
.selected_layers()
|
||||
.iter()
|
||||
.filter_map(|path| document.layer(path).ok().map(|layer| (path, layer)))
|
||||
.filter_map(|(path, shape)| shape.as_subpath().map(|subpath| (path, subpath)))
|
||||
.flat_map(|(path, shape)| {
|
||||
shape
|
||||
.manipulator_groups()
|
||||
.enumerate()
|
||||
.filter(|(_id, manipulator_group)| manipulator_group.is_anchor_selected())
|
||||
.flat_map(|(id, manipulator_group)| manipulator_group.selected_points().map(move |point| (id, point.manipulator_type)))
|
||||
.map(|(anchor, manipulator_point)| (path.as_slice(), *anchor, manipulator_point))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Should we select or deselect the point?
|
||||
let should_select = if is_point_selected { !add_to_selection } else { true };
|
||||
|
||||
// This is selecting the manipulator only for now, next to generalize to points
|
||||
if should_select {
|
||||
let add = add_to_selection || is_point_selected;
|
||||
let point = (manipulator_group_id, ManipulatorType::from_index(manipulator_point_index));
|
||||
// Clear all point in other selected shapes
|
||||
if !add {
|
||||
responses.push_back(DocumentMessage::DeselectAllManipulatorPoints.into());
|
||||
points = vec![(shape_layer_path, point.0, point.1)];
|
||||
} else {
|
||||
points.push((shape_layer_path, point.0, point.1));
|
||||
}
|
||||
responses.push_back(
|
||||
Operation::SelectManipulatorPoints {
|
||||
layer_path: shape_layer_path.to_vec(),
|
||||
point_ids: vec![point],
|
||||
add,
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
// Snap the selected point to the cursor
|
||||
if let Ok(viewspace) = document.generate_transform_relative_to_viewport(shape_layer_path) {
|
||||
self.move_selected_points(mouse_position - viewspace.transform_point2(point_position), mouse_position, responses)
|
||||
}
|
||||
} else {
|
||||
responses.push_back(
|
||||
Operation::DeselectManipulatorPoints {
|
||||
layer_path: shape_layer_path.to_vec(),
|
||||
point_ids: vec![(manipulator_group_id, ManipulatorType::from_index(manipulator_point_index))],
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
points.retain(|x| *x != (shape_layer_path, manipulator_group_id, ManipulatorType::from_index(manipulator_point_index)))
|
||||
}
|
||||
|
||||
return Some(points);
|
||||
}
|
||||
|
||||
// Deselect all points if no nearby point
|
||||
responses.push_back(DocumentMessage::DeselectAllManipulatorPoints.into());
|
||||
None
|
||||
}
|
||||
|
||||
/// A wrapper for `find_nearest_point_indices()` and returns a [ManipulatorPoint].
|
||||
pub fn find_nearest_point<'a>(&'a self, document: &'a Document, mouse_position: DVec2, select_threshold: f64) -> Option<&'a ManipulatorPoint> {
|
||||
let (shape_layer_path, manipulator_group_id, manipulator_point_index) = self.find_nearest_point_indices(document, mouse_position, select_threshold)?;
|
||||
let selected_shape = self.shape(document, shape_layer_path).unwrap();
|
||||
if let Some(manipulator_group) = selected_shape.manipulator_groups().by_id(manipulator_group_id) {
|
||||
return manipulator_group.points[manipulator_point_index].as_ref();
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Set the shapes we consider for selection, we will choose draggable manipulators from these shapes.
|
||||
pub fn set_selected_layers(&mut self, target_layers: Vec<Vec<LayerId>>) {
|
||||
self.selected_layers = target_layers;
|
||||
}
|
||||
|
||||
pub fn selected_layers(&self) -> &Vec<Vec<LayerId>> {
|
||||
&self.selected_layers
|
||||
}
|
||||
|
||||
pub fn selected_layers_ref(&self) -> Vec<&[LayerId]> {
|
||||
self.selected_layers.iter().map(|l| l.as_slice()).collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
/// Clear all of the shapes we can modify.
|
||||
pub fn clear_selected_layers(&mut self) {
|
||||
self.selected_layers.clear();
|
||||
}
|
||||
|
||||
pub fn has_selected_layers(&self) -> bool {
|
||||
!self.selected_layers.is_empty()
|
||||
}
|
||||
|
||||
/// Provide the currently selected manipulators by reference.
|
||||
pub fn selected_manipulator_groups<'a>(&'a self, document: &'a Document) -> impl Iterator<Item = &'a ManipulatorGroup> {
|
||||
self.iter(document).flat_map(|shape| shape.selected_manipulator_groups())
|
||||
}
|
||||
|
||||
/// A mutable iterator of all the manipulators, regardless of selection.
|
||||
pub fn manipulator_groups<'a>(&'a self, document: &'a Document) -> impl Iterator<Item = &'a ManipulatorGroup> {
|
||||
self.iter(document).flat_map(|shape| shape.manipulator_groups().iter())
|
||||
}
|
||||
|
||||
/// Provide the currently selected points by reference.
|
||||
pub fn selected_points<'a>(&'a self, document: &'a Document) -> impl Iterator<Item = &'a ManipulatorPoint> {
|
||||
self.selected_manipulator_groups(document).flat_map(|manipulator_group| manipulator_group.selected_points())
|
||||
}
|
||||
|
||||
/// Move the selected points by dragging the mouse.
|
||||
pub fn move_selected_points(&self, delta: DVec2, absolute_position: DVec2, responses: &mut VecDeque<Message>) {
|
||||
for layer_path in &self.selected_layers {
|
||||
responses.push_back(
|
||||
DocumentMessage::MoveSelectedManipulatorPoints {
|
||||
layer_path: layer_path.clone(),
|
||||
delta: (delta.x, delta.y),
|
||||
absolute_position: (absolute_position.x, absolute_position.y),
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Dissolve the selected points.
|
||||
pub fn delete_selected_points(&self, responses: &mut VecDeque<Message>) {
|
||||
responses.push_back(DocumentMessage::DeleteSelectedManipulatorPoints.into());
|
||||
}
|
||||
|
||||
/// Toggle if the handles should mirror angle across the anchor position.
|
||||
pub fn toggle_handle_mirroring_on_selected(&self, toggle_angle: bool, toggle_distance: bool, responses: &mut VecDeque<Message>) {
|
||||
for layer_path in &self.selected_layers {
|
||||
responses.push_back(
|
||||
DocumentMessage::ToggleSelectedHandleMirroring {
|
||||
layer_path: layer_path.clone(),
|
||||
toggle_angle,
|
||||
toggle_distance,
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Deselect all manipulators from the shapes that the manipulation handler has created.
|
||||
pub fn deselect_all_points(&self, responses: &mut VecDeque<Message>) {
|
||||
responses.push_back(DocumentMessage::DeselectAllManipulatorPoints.into());
|
||||
}
|
||||
|
||||
/// Iterate over the shapes.
|
||||
pub fn iter<'a>(&'a self, document: &'a Document) -> impl Iterator<Item = &'a Subpath> + 'a {
|
||||
self.selected_layers.iter().flat_map(|layer_id| document.layer(layer_id)).filter_map(|shape| shape.as_subpath())
|
||||
}
|
||||
|
||||
/// Find a [ManipulatorPoint] that is within the selection threshold and return the layer path, an index to the [ManipulatorGroup], and an enum index for [ManipulatorPoint].
|
||||
/// Return value is an `Option` of the tuple representing `(layer path, ManipulatorGroup ID, ManipulatorType enum index)`.
|
||||
fn find_nearest_point_indices(&self, document: &Document, mouse_position: DVec2, select_threshold: f64) -> Option<(&[LayerId], u64, usize)> {
|
||||
if self.selected_layers.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let select_threshold_squared = select_threshold * select_threshold;
|
||||
// Find the closest control point among all elements of shapes_to_modify
|
||||
for layer in self.selected_layers.iter() {
|
||||
if let Some((manipulator_id, manipulator_point_index, distance_squared)) = self.closest_point_in_layer(document, layer, mouse_position) {
|
||||
// Choose the first point under the threshold
|
||||
if distance_squared < select_threshold_squared {
|
||||
log::trace!("Selecting... manipulator ID: {}, manipulator point index: {}", manipulator_id, manipulator_point_index);
|
||||
return Some((layer, manipulator_id, manipulator_point_index));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
// TODO Use quadtree or some equivalent spatial acceleration structure to improve this to O(log(n))
|
||||
/// Find the closest manipulator, manipulator point, and distance so we can select path elements.
|
||||
/// Brute force comparison to determine which manipulator (handle or anchor) we want to select taking O(n) time.
|
||||
/// Return value is an `Option` of the tuple representing `(manipulator ID, manipulator point index, distance squared)`.
|
||||
fn closest_point_in_layer(&self, document: &Document, layer_path: &[LayerId], pos: glam::DVec2) -> Option<(u64, usize, f64)> {
|
||||
let mut closest_distance_squared: f64 = f64::MAX; // Not ideal
|
||||
let mut result: Option<(u64, usize, f64)> = None;
|
||||
|
||||
if let Some(shape) = document.layer(layer_path).ok()?.as_subpath() {
|
||||
let viewspace = document.generate_transform_relative_to_viewport(layer_path).ok()?;
|
||||
for (manipulator_id, manipulator) in shape.manipulator_groups().enumerate() {
|
||||
let manipulator_point_index = manipulator.closest_point(&viewspace, pos);
|
||||
if let Some(point) = &manipulator.points[manipulator_point_index] {
|
||||
if point.editor_state.can_be_selected {
|
||||
let distance_squared = viewspace.transform_point2(point.position).distance_squared(pos);
|
||||
if distance_squared < closest_distance_squared {
|
||||
closest_distance_squared = distance_squared;
|
||||
result = Some((*manipulator_id, manipulator_point_index, distance_squared));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn shape<'a>(&'a self, document: &'a Document, layer_id: &[u64]) -> Option<&'a Subpath> {
|
||||
document.layer(layer_id).ok()?.as_subpath()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,319 @@
|
||||
use crate::application::generate_uuid;
|
||||
use crate::consts::{
|
||||
COLOR_ACCENT, SNAP_AXIS_OVERLAY_FADE_DISTANCE, SNAP_AXIS_TOLERANCE, SNAP_AXIS_UNSNAPPED_OPACITY, SNAP_POINT_OVERLAY_FADE_FAR, SNAP_POINT_OVERLAY_FADE_NEAR, SNAP_POINT_SIZE, SNAP_POINT_TOLERANCE,
|
||||
SNAP_POINT_UNSNAPPED_OPACITY,
|
||||
};
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
use graphene::layers::layer_info::{Layer, LayerDataType};
|
||||
use graphene::layers::style::{self, Stroke};
|
||||
use graphene::layers::vector::consts::ManipulatorType;
|
||||
use graphene::{LayerId, Operation};
|
||||
|
||||
use glam::{DAffine2, DVec2};
|
||||
use std::f64::consts::PI;
|
||||
|
||||
// Handles snap overlays
|
||||
#[derive(Debug, Clone, Default)]
|
||||
struct SnapOverlays {
|
||||
axis_overlay_paths: Vec<Vec<LayerId>>,
|
||||
point_overlay_paths: Vec<Vec<LayerId>>,
|
||||
axis_index: usize,
|
||||
point_index: usize,
|
||||
}
|
||||
|
||||
impl SnapOverlays {
|
||||
/// Draws an overlay (axis or point) with the correct transform and fade opacity, reusing lines from the pool if available.
|
||||
fn add_overlay(is_axis: bool, responses: &mut VecDeque<Message>, transform: [f64; 6], opacity: Option<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(
|
||||
if is_axis {
|
||||
Operation::AddLine {
|
||||
path: layer_path.clone(),
|
||||
transform,
|
||||
style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 1.0)), style::Fill::None),
|
||||
insert_index: -1,
|
||||
}
|
||||
} else {
|
||||
Operation::AddEllipse {
|
||||
path: layer_path.clone(),
|
||||
transform,
|
||||
style: style::PathStyle::new(None, style::Fill::Solid(COLOR_ACCENT)),
|
||||
insert_index: -1,
|
||||
}
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
overlay_paths.push(layer_path.clone());
|
||||
layer_path
|
||||
}
|
||||
// Otherwise, reuse an overlay 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
|
||||
if let Some(opacity) = opacity {
|
||||
responses.push_back(DocumentMessage::Overlays(Operation::SetLayerOpacity { path: layer_path, opacity }.into()).into());
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw the alignment lines for an axis
|
||||
/// Note: horizontal refers to the overlay line being horizontal and the snap being along the Y axis
|
||||
fn draw_alignment_lines(&mut self, is_horizontal: bool, distances: impl Iterator<Item = (DVec2, DVec2, f64)>, responses: &mut VecDeque<Message>, closest_distance: DVec2) {
|
||||
for (target, goal, distance) in distances.filter(|(_target, _pos, dist)| dist.abs() < SNAP_AXIS_OVERLAY_FADE_DISTANCE) {
|
||||
let offset = if is_horizontal { target.y } else { target.x }.round() - 0.5;
|
||||
let offset_other = if is_horizontal { target.x } else { target.y }.round() - 0.5;
|
||||
let goal_axis = if is_horizontal { goal.x } else { goal.y }.round() - 0.5;
|
||||
|
||||
let scale = DVec2::new(offset_other - goal_axis, 1.);
|
||||
let angle = if is_horizontal { 0. } else { PI / 2. };
|
||||
let translation = if is_horizontal { DVec2::new(goal_axis, offset) } else { DVec2::new(offset, goal_axis) };
|
||||
|
||||
let transform = DAffine2::from_scale_angle_translation(scale, angle, translation).to_cols_array();
|
||||
let closest = if is_horizontal { closest_distance.y } else { closest_distance.x };
|
||||
|
||||
let opacity = if (closest - distance).abs() < 1. {
|
||||
1.
|
||||
} else {
|
||||
SNAP_AXIS_UNSNAPPED_OPACITY - distance.abs() / (SNAP_AXIS_OVERLAY_FADE_DISTANCE / SNAP_AXIS_UNSNAPPED_OPACITY)
|
||||
};
|
||||
|
||||
// Add line
|
||||
Self::add_overlay(true, responses, transform, Some(opacity), self.axis_index, &mut self.axis_overlay_paths);
|
||||
self.axis_index += 1;
|
||||
|
||||
let size = DVec2::splat(SNAP_POINT_SIZE);
|
||||
|
||||
// Add point at target
|
||||
let transform = DAffine2::from_scale_angle_translation(size, 0., target - size / 2.).to_cols_array();
|
||||
Self::add_overlay(false, responses, transform, Some(opacity), self.point_index, &mut self.point_overlay_paths);
|
||||
self.point_index += 1;
|
||||
|
||||
// Add point along line but towards goal
|
||||
let translation = if is_horizontal { DVec2::new(goal.x, target.y) } else { DVec2::new(target.x, goal.y) };
|
||||
let transform = DAffine2::from_scale_angle_translation(size, 0., translation - size / 2.).to_cols_array();
|
||||
Self::add_overlay(false, responses, transform, Some(opacity), self.point_index, &mut self.point_overlay_paths);
|
||||
self.point_index += 1
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw the snap points
|
||||
fn draw_snap_points(&mut self, distances: impl Iterator<Item = (DVec2, DVec2, f64)>, responses: &mut VecDeque<Message>, closest_distance: DVec2) {
|
||||
for (target, offset, distance) in distances.filter(|(_pos, _offset, dist)| dist.abs() < SNAP_POINT_OVERLAY_FADE_FAR) {
|
||||
let active = (closest_distance - offset).length_squared() < 1.;
|
||||
|
||||
if active {
|
||||
continue;
|
||||
}
|
||||
|
||||
let opacity = (1. - (distance - SNAP_POINT_OVERLAY_FADE_NEAR) / (SNAP_POINT_OVERLAY_FADE_FAR - SNAP_POINT_OVERLAY_FADE_NEAR)).min(1.) / SNAP_POINT_UNSNAPPED_OPACITY;
|
||||
|
||||
let size = DVec2::splat(SNAP_POINT_SIZE);
|
||||
let transform = DAffine2::from_scale_angle_translation(size, 0., target - size / 2.).to_cols_array();
|
||||
Self::add_overlay(false, responses, transform, Some(opacity), self.point_index, &mut self.point_overlay_paths);
|
||||
self.point_index += 1
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the snapping overlays with the specified distances.
|
||||
/// `positions_and_distances` is a tuple of `x`, `y` & `point` iterators,, each with `(position, goal, distance)` values.
|
||||
fn update_overlays<X, Y, P>(&mut self, responses: &mut VecDeque<Message>, positions_and_distances: (X, Y, P), closest_distance: DVec2)
|
||||
where
|
||||
X: Iterator<Item = (DVec2, DVec2, f64)>,
|
||||
Y: Iterator<Item = (DVec2, DVec2, f64)>,
|
||||
P: Iterator<Item = (DVec2, DVec2, f64)>,
|
||||
{
|
||||
self.axis_index = 0;
|
||||
self.point_index = 0;
|
||||
|
||||
let (x, y, points) = positions_and_distances;
|
||||
self.draw_alignment_lines(true, y, responses, closest_distance);
|
||||
self.draw_alignment_lines(false, x, responses, closest_distance);
|
||||
self.draw_snap_points(points, responses, closest_distance);
|
||||
|
||||
Self::remove_unused_overlays(&mut self.axis_overlay_paths, responses, self.axis_index);
|
||||
Self::remove_unused_overlays(&mut self.point_overlay_paths, responses, self.point_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());
|
||||
}
|
||||
}
|
||||
|
||||
/// Deletes all overlays
|
||||
fn cleanup(&mut self, responses: &mut VecDeque<Message>) {
|
||||
Self::remove_unused_overlays(&mut self.axis_overlay_paths, responses, 0);
|
||||
Self::remove_unused_overlays(&mut self.point_overlay_paths, responses, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles snapping and snap overlays
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct SnapManager {
|
||||
point_targets: Option<Vec<DVec2>>,
|
||||
bound_targets: Option<Vec<DVec2>>,
|
||||
snap_overlays: SnapOverlays,
|
||||
snap_x: bool,
|
||||
snap_y: bool,
|
||||
}
|
||||
|
||||
impl SnapManager {
|
||||
/// Computes the necessary translation to the layer to snap it (as well as updating necessary overlays)
|
||||
fn calculate_snap<R>(&mut self, targets: R, responses: &mut VecDeque<Message>) -> DVec2
|
||||
where
|
||||
R: Iterator<Item = DVec2> + Clone,
|
||||
{
|
||||
let empty = Vec::new();
|
||||
let snap_points = self.snap_x && self.snap_y;
|
||||
|
||||
let axis = self.bound_targets.as_ref().unwrap_or(&empty);
|
||||
let points = if snap_points { self.point_targets.as_ref().unwrap_or(&empty) } else { &empty };
|
||||
|
||||
let x_axis = if self.snap_x { axis } else { &empty }
|
||||
.iter()
|
||||
.flat_map(|&pos| targets.clone().map(move |goal| (pos, goal, (pos - goal).x)));
|
||||
let y_axis = if self.snap_y { axis } else { &empty }
|
||||
.iter()
|
||||
.flat_map(|&pos| targets.clone().map(move |goal| (pos, goal, (pos - goal).y)));
|
||||
let points = points.iter().flat_map(|&pos| targets.clone().map(move |goal| (pos, pos - goal, (pos - goal).length())));
|
||||
|
||||
let min_x = x_axis.clone().min_by(|a, b| a.2.abs().partial_cmp(&b.2.abs()).expect("Could not compare position."));
|
||||
let min_y = y_axis.clone().min_by(|a, b| a.2.abs().partial_cmp(&b.2.abs()).expect("Could not compare position."));
|
||||
let min_points = points.clone().min_by(|a, b| a.2.abs().partial_cmp(&b.2.abs()).expect("Could not compare position."));
|
||||
|
||||
// Snap to a point if possible
|
||||
let clamped_closest_distance = if let Some(min_points) = min_points.filter(|&(_, _, dist)| dist <= SNAP_POINT_TOLERANCE) {
|
||||
min_points.1
|
||||
} else {
|
||||
// Do not move if over snap tolerance
|
||||
let closest_distance = DVec2::new(min_x.unwrap_or_default().2, min_y.unwrap_or_default().2);
|
||||
DVec2::new(
|
||||
if closest_distance.x.abs() > SNAP_AXIS_TOLERANCE { 0. } else { closest_distance.x },
|
||||
if closest_distance.y.abs() > SNAP_AXIS_TOLERANCE { 0. } else { closest_distance.y },
|
||||
)
|
||||
};
|
||||
|
||||
self.snap_overlays.update_overlays(responses, (x_axis, y_axis, points), clamped_closest_distance);
|
||||
|
||||
clamped_closest_distance
|
||||
}
|
||||
|
||||
/// Gets a list of snap targets for the X and Y axes (if specified) 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, bounding_boxes: impl Iterator<Item = [DVec2; 2]>, snap_x: bool, snap_y: bool) {
|
||||
if document_message_handler.snapping_enabled {
|
||||
self.snap_x = snap_x;
|
||||
self.snap_y = snap_y;
|
||||
|
||||
// Could be made into sorted Vec or a HashSet for more performant lookups.
|
||||
self.bound_targets = Some(bounding_boxes.flat_map(expand_bounds).collect());
|
||||
self.point_targets = None;
|
||||
}
|
||||
}
|
||||
|
||||
/// Add arbitrary snapping points
|
||||
///
|
||||
/// This should be called after start_snap
|
||||
pub fn add_snap_points(&mut self, document_message_handler: &DocumentMessageHandler, snap_points: impl Iterator<Item = DVec2>) {
|
||||
if document_message_handler.snapping_enabled {
|
||||
if let Some(targets) = &mut self.point_targets {
|
||||
targets.extend(snap_points);
|
||||
} else {
|
||||
self.point_targets = Some(snap_points.collect());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Add the [ManipulatorGroup]s (optionally including handles) of the specified shape layer to the snapping points
|
||||
///
|
||||
/// This should be called after start_snap
|
||||
pub fn add_snap_path(&mut self, document_message_handler: &DocumentMessageHandler, layer: &Layer, path: &[LayerId], include_handles: bool, ignore_points: &[(&[LayerId], u64, ManipulatorType)]) {
|
||||
if let LayerDataType::Shape(shape_layer) = &layer.data {
|
||||
let transform = document_message_handler.graphene_document.multiply_transforms(path).unwrap();
|
||||
let snap_points = shape_layer
|
||||
.shape
|
||||
.manipulator_groups()
|
||||
.enumerate()
|
||||
.flat_map(|(id, shape)| {
|
||||
if include_handles {
|
||||
[
|
||||
(*id, &shape.points[ManipulatorType::Anchor]),
|
||||
(*id, &shape.points[ManipulatorType::InHandle]),
|
||||
(*id, &shape.points[ManipulatorType::OutHandle]),
|
||||
]
|
||||
} else {
|
||||
[(*id, &shape.points[ManipulatorType::Anchor]), (0, &None), (0, &None)]
|
||||
}
|
||||
})
|
||||
.filter_map(|(id, point)| point.as_ref().map(|val| (id, val)))
|
||||
.filter(|(id, point)| !ignore_points.contains(&(path, *id, point.manipulator_type)))
|
||||
.map(|(_id, point)| DVec2::new(point.position.x, point.position.y))
|
||||
.map(|pos| transform.transform_point2(pos));
|
||||
self.add_snap_points(document_message_handler, snap_points);
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds all of the shape handles in the document, including bézier handles of the points specified
|
||||
pub fn add_all_document_handles(
|
||||
&mut self,
|
||||
document_message_handler: &DocumentMessageHandler,
|
||||
include_handles: &[&[LayerId]],
|
||||
exclude: &[&[LayerId]],
|
||||
ignore_points: &[(&[LayerId], u64, ManipulatorType)],
|
||||
) {
|
||||
for path in document_message_handler.all_layers() {
|
||||
if !exclude.contains(&path) {
|
||||
let layer = document_message_handler.graphene_document.layer(path).expect("Could not get layer for snapping");
|
||||
self.add_snap_path(document_message_handler, layer, path, include_handles.contains(&path), ignore_points);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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, snap_anchors: Vec<DVec2>, mouse_delta: DVec2) -> DVec2 {
|
||||
if document_message_handler.snapping_enabled {
|
||||
self.calculate_snap(snap_anchors.iter().map(move |&snap| mouse_delta + snap), responses)
|
||||
} else {
|
||||
DVec2::ZERO
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles snapping of a viewport position, returning another viewport position.
|
||||
pub fn snap_position(&mut self, responses: &mut VecDeque<Message>, document_message_handler: &DocumentMessageHandler, position_viewport: DVec2) -> DVec2 {
|
||||
if document_message_handler.snapping_enabled {
|
||||
self.calculate_snap([position_viewport].into_iter(), responses) + 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.snap_overlays.cleanup(responses);
|
||||
self.bound_targets = None;
|
||||
self.point_targets = None;
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts a bounding box into a set of points for snapping
|
||||
///
|
||||
/// Puts a point in the middle of each edge (top, bottom, left, right)
|
||||
pub fn expand_bounds([bound1, bound2]: [DVec2; 2]) -> [DVec2; 4] {
|
||||
[
|
||||
DVec2::new((bound1.x + bound2.x) / 2., bound1.y),
|
||||
DVec2::new((bound1.x + bound2.x) / 2., bound2.y),
|
||||
DVec2::new(bound1.x, (bound1.y + bound2.y) / 2.),
|
||||
DVec2::new(bound2.x, (bound1.y + bound2.y) / 2.),
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
use crate::application::generate_uuid;
|
||||
use crate::consts::{BOUNDS_ROTATE_THRESHOLD, BOUNDS_SELECT_THRESHOLD, COLOR_ACCENT, MANIPULATOR_GROUP_MARKER_SIZE, SELECTION_DRAG_ANGLE};
|
||||
use crate::messages::frontend::utility_types::MouseCursorIcon;
|
||||
use crate::messages::portfolio::document::utility_types::transformation::OriginalTransforms;
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
use graphene::color::Color;
|
||||
use graphene::layers::style::{self, Fill, Stroke};
|
||||
use graphene::LayerId;
|
||||
use graphene::Operation;
|
||||
|
||||
use glam::{DAffine2, DVec2};
|
||||
|
||||
/// Contains the edges that are being dragged along with the original bounds.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct SelectedEdges {
|
||||
bounds: [DVec2; 2],
|
||||
top: bool,
|
||||
bottom: bool,
|
||||
left: bool,
|
||||
right: bool,
|
||||
// Aspect ratio in the form of width/height, so x:1 = width:height
|
||||
aspect_ratio: f64,
|
||||
}
|
||||
|
||||
impl SelectedEdges {
|
||||
pub fn new(top: bool, bottom: bool, left: bool, right: bool, bounds: [DVec2; 2]) -> Self {
|
||||
let size = (bounds[0] - bounds[1]).abs();
|
||||
let aspect_ratio = size.x / size.y;
|
||||
Self {
|
||||
top,
|
||||
bottom,
|
||||
left,
|
||||
right,
|
||||
bounds,
|
||||
aspect_ratio,
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate the pivot for the operation (the opposite point to the edge dragged)
|
||||
pub fn calculate_pivot(&self) -> DVec2 {
|
||||
let min = self.bounds[0];
|
||||
let max = self.bounds[1];
|
||||
|
||||
let x = if self.left {
|
||||
max.x
|
||||
} else if self.right {
|
||||
min.x
|
||||
} else {
|
||||
(min.x + max.x) / 2.
|
||||
};
|
||||
|
||||
let y = if self.top {
|
||||
max.y
|
||||
} else if self.bottom {
|
||||
min.y
|
||||
} else {
|
||||
(min.y + max.y) / 2.
|
||||
};
|
||||
|
||||
DVec2::new(x, y)
|
||||
}
|
||||
|
||||
/// Computes the new bounds with the given mouse move and modifier keys
|
||||
pub fn new_size(&self, mouse: DVec2, transform: DAffine2, center: bool, constrain: bool) -> (DVec2, DVec2) {
|
||||
let mouse = transform.inverse().transform_point2(mouse);
|
||||
|
||||
let mut min = self.bounds[0];
|
||||
let mut max = self.bounds[1];
|
||||
if self.top {
|
||||
min.y = mouse.y;
|
||||
} else if self.bottom {
|
||||
max.y = mouse.y;
|
||||
}
|
||||
if self.left {
|
||||
let delta = min.x - mouse.x;
|
||||
min.x = mouse.x;
|
||||
max.x += delta;
|
||||
} else if self.right {
|
||||
max.x = mouse.x;
|
||||
}
|
||||
|
||||
let mut size = max - min;
|
||||
if constrain {
|
||||
size = match ((self.top || self.bottom), (self.left || self.right)) {
|
||||
(true, true) => DVec2::new(size.x, size.x / self.aspect_ratio).abs().max(DVec2::new(size.y * self.aspect_ratio, size.y).abs()) * size.signum(),
|
||||
(true, false) => DVec2::new(size.y * self.aspect_ratio, size.y),
|
||||
(false, true) => DVec2::new(size.x, size.x / self.aspect_ratio),
|
||||
_ => size,
|
||||
};
|
||||
}
|
||||
if center {
|
||||
if self.left || self.right {
|
||||
size.x *= 2.;
|
||||
}
|
||||
|
||||
if self.bottom || self.top {
|
||||
size.y *= 2.;
|
||||
}
|
||||
}
|
||||
|
||||
(min, size)
|
||||
}
|
||||
|
||||
/// Offsets the transformation pivot in order to scale from the center
|
||||
fn offset_pivot(&self, center: bool, size: DVec2) -> DVec2 {
|
||||
let mut offset = DVec2::ZERO;
|
||||
|
||||
if !center {
|
||||
return offset;
|
||||
}
|
||||
|
||||
if self.right {
|
||||
offset.x -= size.x / 2.;
|
||||
}
|
||||
if self.left {
|
||||
offset.x += size.x / 2.;
|
||||
}
|
||||
if self.bottom {
|
||||
offset.y -= size.y / 2.;
|
||||
}
|
||||
if self.top {
|
||||
offset.y += size.y / 2.;
|
||||
}
|
||||
offset
|
||||
}
|
||||
|
||||
/// Moves the position to account for centering (only necessary with absolute transforms - e.g. with artboards)
|
||||
pub fn center_position(&self, mut position: DVec2, size: DVec2) -> DVec2 {
|
||||
if self.right {
|
||||
position.x -= size.x / 2.;
|
||||
}
|
||||
if self.bottom {
|
||||
position.y -= size.y / 2.;
|
||||
}
|
||||
|
||||
position
|
||||
}
|
||||
|
||||
/// Calculates the required scaling to resize the bounding box
|
||||
pub fn bounds_to_scale_transform(&self, center: bool, size: DVec2) -> DAffine2 {
|
||||
DAffine2::from_translation(self.offset_pivot(center, size)) * DAffine2::from_scale(size / (self.bounds[1] - self.bounds[0]))
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a viewport relative bounding box overlay with no transform handles
|
||||
pub fn add_bounding_box(responses: &mut VecDeque<Message>) -> Vec<LayerId> {
|
||||
let path = vec![generate_uuid()];
|
||||
|
||||
let operation = Operation::AddRect {
|
||||
path: path.clone(),
|
||||
transform: DAffine2::ZERO.to_cols_array(),
|
||||
style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 1.0)), Fill::None),
|
||||
insert_index: -1,
|
||||
};
|
||||
responses.push_back(DocumentMessage::Overlays(operation.into()).into());
|
||||
|
||||
path
|
||||
}
|
||||
|
||||
/// Add the transform handle overlay
|
||||
fn add_transform_handles(responses: &mut VecDeque<Message>) -> [Vec<LayerId>; 8] {
|
||||
const EMPTY_VEC: Vec<LayerId> = Vec::new();
|
||||
let mut transform_handle_paths = [EMPTY_VEC; 8];
|
||||
|
||||
for item in &mut transform_handle_paths {
|
||||
let current_path = vec![generate_uuid()];
|
||||
|
||||
let operation = Operation::AddRect {
|
||||
path: current_path.clone(),
|
||||
transform: DAffine2::ZERO.to_cols_array(),
|
||||
style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 2.0)), Fill::solid(Color::WHITE)),
|
||||
insert_index: -1,
|
||||
};
|
||||
responses.push_back(DocumentMessage::Overlays(operation.into()).into());
|
||||
|
||||
*item = current_path;
|
||||
}
|
||||
|
||||
transform_handle_paths
|
||||
}
|
||||
|
||||
/// Converts a bounding box to a rounded transform (with translation and scale)
|
||||
pub fn transform_from_box(pos1: DVec2, pos2: DVec2, transform: DAffine2) -> DAffine2 {
|
||||
let inverse = transform.inverse();
|
||||
transform
|
||||
* DAffine2::from_scale_angle_translation(
|
||||
inverse.transform_vector2(transform.transform_vector2(pos2 - pos1).round()),
|
||||
0.,
|
||||
inverse.transform_point2(transform.transform_point2(pos1).round() - DVec2::splat(0.5)),
|
||||
)
|
||||
}
|
||||
|
||||
/// Aligns the mouse position to the closest axis
|
||||
pub fn axis_align_drag(axis_align: bool, position: DVec2, start: DVec2) -> DVec2 {
|
||||
if axis_align {
|
||||
let mouse_position = position - start;
|
||||
let snap_resolution = SELECTION_DRAG_ANGLE.to_radians();
|
||||
let angle = -mouse_position.angle_between(DVec2::X);
|
||||
let snapped_angle = (angle / snap_resolution).round() * snap_resolution;
|
||||
DVec2::new(snapped_angle.cos(), snapped_angle.sin()) * mouse_position.length() + start
|
||||
} else {
|
||||
position
|
||||
}
|
||||
}
|
||||
|
||||
/// Contains info on the overlays for the bounding box and transform handles
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct BoundingBoxOverlays {
|
||||
pub bounding_box: Vec<LayerId>,
|
||||
pub transform_handles: [Vec<LayerId>; 8],
|
||||
pub bounds: [DVec2; 2],
|
||||
pub transform: DAffine2,
|
||||
pub selected_edges: Option<SelectedEdges>,
|
||||
pub original_transforms: OriginalTransforms,
|
||||
pub pivot: DVec2,
|
||||
}
|
||||
|
||||
impl BoundingBoxOverlays {
|
||||
#[must_use]
|
||||
pub fn new(responses: &mut VecDeque<Message>) -> Self {
|
||||
Self {
|
||||
bounding_box: add_bounding_box(responses),
|
||||
transform_handles: add_transform_handles(responses),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculates the transformed handle positions based on the bounding box and the transform
|
||||
pub fn evaluate_transform_handle_positions(&self) -> [DVec2; 8] {
|
||||
let (left, top): (f64, f64) = self.bounds[0].into();
|
||||
let (right, bottom): (f64, f64) = self.bounds[1].into();
|
||||
[
|
||||
self.transform.transform_point2(DVec2::new(left, top)),
|
||||
self.transform.transform_point2(DVec2::new(left, (top + bottom) / 2.)),
|
||||
self.transform.transform_point2(DVec2::new(left, bottom)),
|
||||
self.transform.transform_point2(DVec2::new((left + right) / 2., top)),
|
||||
self.transform.transform_point2(DVec2::new((left + right) / 2., bottom)),
|
||||
self.transform.transform_point2(DVec2::new(right, top)),
|
||||
self.transform.transform_point2(DVec2::new(right, (top + bottom) / 2.)),
|
||||
self.transform.transform_point2(DVec2::new(right, bottom)),
|
||||
]
|
||||
}
|
||||
|
||||
/// Update the position of the bounding box and transform handles
|
||||
pub fn transform(&mut self, responses: &mut VecDeque<Message>) {
|
||||
let transform = transform_from_box(self.bounds[0], self.bounds[1], self.transform).to_cols_array();
|
||||
let path = self.bounding_box.clone();
|
||||
responses.push_back(DocumentMessage::Overlays(Operation::SetLayerTransformInViewport { path, transform }.into()).into());
|
||||
|
||||
// Helps push values that end in approximately half, plus or minus some floating point imprecision, towards the same side of the round() function
|
||||
const BIAS: f64 = 0.0001;
|
||||
|
||||
for (position, path) in self.evaluate_transform_handle_positions().into_iter().zip(&self.transform_handles) {
|
||||
let scale = DVec2::splat(MANIPULATOR_GROUP_MARKER_SIZE);
|
||||
let translation = (position - (scale / 2.) - 0.5 + BIAS).round();
|
||||
let transform = DAffine2::from_scale_angle_translation(scale, 0., translation).to_cols_array();
|
||||
let path = path.clone();
|
||||
responses.push_back(DocumentMessage::Overlays(Operation::SetLayerTransformInViewport { path, transform }.into()).into());
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the user has selected the edge for dragging (returns which edge in order top, bottom, left, right)
|
||||
pub fn check_selected_edges(&self, cursor: DVec2) -> Option<(bool, bool, bool, bool)> {
|
||||
let cursor = self.transform.inverse().transform_point2(cursor);
|
||||
let select_threshold = self.transform.inverse().transform_vector2(DVec2::new(0., BOUNDS_SELECT_THRESHOLD)).length();
|
||||
|
||||
let min = self.bounds[0].min(self.bounds[1]);
|
||||
let max = self.bounds[0].max(self.bounds[1]);
|
||||
if min.x - cursor.x < select_threshold && min.y - cursor.y < select_threshold && cursor.x - max.x < select_threshold && cursor.y - max.y < select_threshold {
|
||||
let mut top = (cursor.y - min.y).abs() < select_threshold;
|
||||
let mut bottom = (max.y - cursor.y).abs() < select_threshold;
|
||||
let mut left = (cursor.x - min.x).abs() < select_threshold;
|
||||
let mut right = (max.x - cursor.x).abs() < select_threshold;
|
||||
if cursor.y - min.y + max.y - cursor.y < select_threshold * 2. && (left || right) {
|
||||
top = false;
|
||||
bottom = false;
|
||||
}
|
||||
if cursor.x - min.x + max.x - cursor.x < select_threshold * 2. && (top || bottom) {
|
||||
left = false;
|
||||
right = false;
|
||||
}
|
||||
|
||||
if top || bottom || left || right {
|
||||
return Some((top, bottom, left, right));
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Check if the user is rotating with the bounds
|
||||
pub fn check_rotate(&self, cursor: DVec2) -> bool {
|
||||
let cursor = self.transform.inverse().transform_point2(cursor);
|
||||
let rotate_threshold = self.transform.inverse().transform_vector2(DVec2::new(0., BOUNDS_ROTATE_THRESHOLD)).length();
|
||||
|
||||
let min = self.bounds[0].min(self.bounds[1]);
|
||||
let max = self.bounds[0].max(self.bounds[1]);
|
||||
|
||||
let outside_bounds = (min.x > cursor.x || cursor.x > max.x) || (min.y > cursor.y || cursor.y > max.y);
|
||||
let inside_extended_bounds = min.x - cursor.x < rotate_threshold && min.y - cursor.y < rotate_threshold && cursor.x - max.x < rotate_threshold && cursor.y - max.y < rotate_threshold;
|
||||
|
||||
outside_bounds & inside_extended_bounds
|
||||
}
|
||||
|
||||
/// Gets the required mouse cursor to show resizing bounds or optionally rotation
|
||||
pub fn get_cursor(&self, input: &InputPreprocessorMessageHandler, rotate: bool) -> MouseCursorIcon {
|
||||
if let Some(directions) = self.check_selected_edges(input.mouse.position) {
|
||||
match directions {
|
||||
(true, false, false, false) | (false, true, false, false) => MouseCursorIcon::NSResize,
|
||||
(false, false, true, false) | (false, false, false, true) => MouseCursorIcon::EWResize,
|
||||
(true, false, true, false) | (false, true, false, true) => MouseCursorIcon::NWSEResize,
|
||||
(true, false, false, true) | (false, true, true, false) => MouseCursorIcon::NESWResize,
|
||||
_ => MouseCursorIcon::Default,
|
||||
}
|
||||
} else if rotate && self.check_rotate(input.mouse.position) {
|
||||
MouseCursorIcon::Grabbing
|
||||
} else {
|
||||
MouseCursorIcon::Default
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes the overlays
|
||||
pub fn delete(self, responses: &mut VecDeque<Message>) {
|
||||
responses.push_back(DocumentMessage::Overlays(Operation::DeleteLayer { path: self.bounding_box }.into()).into());
|
||||
responses.extend(
|
||||
self.transform_handles
|
||||
.iter()
|
||||
.map(|path| DocumentMessage::Overlays(Operation::DeleteLayer { path: path.clone() }.into()).into()),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
mod tool_message;
|
||||
mod tool_message_handler;
|
||||
|
||||
pub mod common_functionality;
|
||||
pub mod tool_messages;
|
||||
pub mod utility_types;
|
||||
|
||||
#[doc(inline)]
|
||||
pub use tool_message::{ToolMessage, ToolMessageDiscriminant};
|
||||
#[doc(inline)]
|
||||
pub use tool_message_handler::ToolMessageHandler;
|
||||
@@ -0,0 +1,128 @@
|
||||
use super::utility_types::ToolType;
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
use graphene::color::Color;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[remain::sorted]
|
||||
#[impl_message(Message, Tool)]
|
||||
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum ToolMessage {
|
||||
// Sub-messages
|
||||
#[remain::unsorted]
|
||||
#[child]
|
||||
Select(SelectToolMessage),
|
||||
#[remain::unsorted]
|
||||
#[child]
|
||||
Artboard(ArtboardToolMessage),
|
||||
#[remain::unsorted]
|
||||
#[child]
|
||||
Navigate(NavigateToolMessage),
|
||||
#[remain::unsorted]
|
||||
#[child]
|
||||
Eyedropper(EyedropperToolMessage),
|
||||
#[remain::unsorted]
|
||||
#[child]
|
||||
Fill(FillToolMessage),
|
||||
#[remain::unsorted]
|
||||
#[child]
|
||||
Gradient(GradientToolMessage),
|
||||
|
||||
#[remain::unsorted]
|
||||
#[child]
|
||||
Path(PathToolMessage),
|
||||
#[remain::unsorted]
|
||||
#[child]
|
||||
Pen(PenToolMessage),
|
||||
#[remain::unsorted]
|
||||
#[child]
|
||||
Freehand(FreehandToolMessage),
|
||||
#[remain::unsorted]
|
||||
#[child]
|
||||
Spline(SplineToolMessage),
|
||||
#[remain::unsorted]
|
||||
#[child]
|
||||
Line(LineToolMessage),
|
||||
#[remain::unsorted]
|
||||
#[child]
|
||||
Rectangle(RectangleToolMessage),
|
||||
#[remain::unsorted]
|
||||
#[child]
|
||||
Ellipse(EllipseToolMessage),
|
||||
#[remain::unsorted]
|
||||
#[child]
|
||||
Shape(ShapeToolMessage),
|
||||
#[remain::unsorted]
|
||||
#[child]
|
||||
Text(TextToolMessage),
|
||||
|
||||
// #[remain::unsorted]
|
||||
// #[child]
|
||||
// Brush(BrushToolMessage),
|
||||
// #[remain::unsorted]
|
||||
// #[child]
|
||||
// Heal(HealToolMessage),
|
||||
// #[remain::unsorted]
|
||||
// #[child]
|
||||
// Clone(CloneToolMessage),
|
||||
// #[remain::unsorted]
|
||||
// #[child]
|
||||
// Patch(PatchToolMessage),
|
||||
// #[remain::unsorted]
|
||||
// #[child]
|
||||
// Relight(RelightToolMessage),
|
||||
// #[remain::unsorted]
|
||||
// #[child]
|
||||
// Detail(DetailToolMessage),
|
||||
|
||||
// Messages
|
||||
#[remain::unsorted]
|
||||
ActivateToolSelect,
|
||||
#[remain::unsorted]
|
||||
ActivateToolArtboard,
|
||||
#[remain::unsorted]
|
||||
ActivateToolNavigate,
|
||||
#[remain::unsorted]
|
||||
ActivateToolEyedropper,
|
||||
#[remain::unsorted]
|
||||
ActivateToolText,
|
||||
#[remain::unsorted]
|
||||
ActivateToolFill,
|
||||
#[remain::unsorted]
|
||||
ActivateToolGradient,
|
||||
|
||||
#[remain::unsorted]
|
||||
ActivateToolPath,
|
||||
#[remain::unsorted]
|
||||
ActivateToolPen,
|
||||
#[remain::unsorted]
|
||||
ActivateToolFreehand,
|
||||
#[remain::unsorted]
|
||||
ActivateToolSpline,
|
||||
#[remain::unsorted]
|
||||
ActivateToolLine,
|
||||
#[remain::unsorted]
|
||||
ActivateToolRectangle,
|
||||
#[remain::unsorted]
|
||||
ActivateToolEllipse,
|
||||
#[remain::unsorted]
|
||||
ActivateToolShape,
|
||||
|
||||
ActivateTool {
|
||||
tool_type: ToolType,
|
||||
},
|
||||
DeactivateTools,
|
||||
InitTools,
|
||||
ResetColors,
|
||||
SelectPrimaryColor {
|
||||
color: Color,
|
||||
},
|
||||
SelectRandomPrimaryColor,
|
||||
SelectSecondaryColor {
|
||||
color: Color,
|
||||
},
|
||||
SwapColors,
|
||||
UpdateCursor,
|
||||
UpdateHints,
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
use super::utility_types::{tool_message_to_tool_type, ToolFsmState};
|
||||
use crate::application::generate_uuid;
|
||||
use crate::messages::layout::utility_types::layout_widget::PropertyHolder;
|
||||
use crate::messages::layout::utility_types::misc::LayoutTarget;
|
||||
use crate::messages::prelude::*;
|
||||
use crate::messages::tool::utility_types::ToolType;
|
||||
|
||||
use graphene::color::Color;
|
||||
use graphene::layers::text_layer::FontCache;
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ToolMessageHandler {
|
||||
tool_state: ToolFsmState,
|
||||
}
|
||||
|
||||
impl MessageHandler<ToolMessage, (&DocumentMessageHandler, &InputPreprocessorMessageHandler, &FontCache)> for ToolMessageHandler {
|
||||
#[remain::check]
|
||||
fn process_message(&mut self, message: ToolMessage, data: (&DocumentMessageHandler, &InputPreprocessorMessageHandler, &FontCache), responses: &mut VecDeque<Message>) {
|
||||
let (document, input, font_cache) = data;
|
||||
#[remain::sorted]
|
||||
match message {
|
||||
// Messages
|
||||
#[remain::unsorted]
|
||||
ToolMessage::ActivateToolSelect => responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Select }.into()),
|
||||
#[remain::unsorted]
|
||||
ToolMessage::ActivateToolArtboard => responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Artboard }.into()),
|
||||
#[remain::unsorted]
|
||||
ToolMessage::ActivateToolNavigate => responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Navigate }.into()),
|
||||
#[remain::unsorted]
|
||||
ToolMessage::ActivateToolEyedropper => responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Eyedropper }.into()),
|
||||
#[remain::unsorted]
|
||||
ToolMessage::ActivateToolText => responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Text }.into()),
|
||||
#[remain::unsorted]
|
||||
ToolMessage::ActivateToolFill => responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Fill }.into()),
|
||||
#[remain::unsorted]
|
||||
ToolMessage::ActivateToolGradient => responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Gradient }.into()),
|
||||
|
||||
#[remain::unsorted]
|
||||
ToolMessage::ActivateToolPath => responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Path }.into()),
|
||||
#[remain::unsorted]
|
||||
ToolMessage::ActivateToolPen => responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Pen }.into()),
|
||||
#[remain::unsorted]
|
||||
ToolMessage::ActivateToolFreehand => responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Freehand }.into()),
|
||||
#[remain::unsorted]
|
||||
ToolMessage::ActivateToolSpline => responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Spline }.into()),
|
||||
#[remain::unsorted]
|
||||
ToolMessage::ActivateToolLine => responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Line }.into()),
|
||||
#[remain::unsorted]
|
||||
ToolMessage::ActivateToolRectangle => responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Rectangle }.into()),
|
||||
#[remain::unsorted]
|
||||
ToolMessage::ActivateToolEllipse => responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Ellipse }.into()),
|
||||
#[remain::unsorted]
|
||||
ToolMessage::ActivateToolShape => responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Shape }.into()),
|
||||
|
||||
ToolMessage::ActivateTool { tool_type } => {
|
||||
let tool_data = &mut self.tool_state.tool_data;
|
||||
let document_data = &self.tool_state.document_tool_data;
|
||||
let old_tool = tool_data.active_tool_type;
|
||||
|
||||
// Do nothing if switching to the same tool
|
||||
if tool_type == old_tool {
|
||||
return;
|
||||
}
|
||||
|
||||
// Send the Abort state transition to the tool
|
||||
let mut send_abort_to_tool = |tool_type, update_hints_and_cursor: bool| {
|
||||
if let Some(tool) = tool_data.tools.get_mut(&tool_type) {
|
||||
if let Some(tool_abort_message) = tool.event_to_message_map().tool_abort {
|
||||
tool.process_message(tool_abort_message, (document, document_data, input, font_cache), responses);
|
||||
}
|
||||
|
||||
if update_hints_and_cursor {
|
||||
tool.process_message(ToolMessage::UpdateHints, (document, document_data, input, font_cache), responses);
|
||||
tool.process_message(ToolMessage::UpdateCursor, (document, document_data, input, font_cache), responses);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Send the old and new tools a transition to their FSM Abort states
|
||||
send_abort_to_tool(tool_type, true);
|
||||
send_abort_to_tool(old_tool, false);
|
||||
|
||||
// Unsubscribe old tool from the broadcaster
|
||||
tool_data.tools.get(&tool_type).unwrap().deactivate(responses);
|
||||
|
||||
// Store the new active tool
|
||||
tool_data.active_tool_type = tool_type;
|
||||
|
||||
// Subscribe new tool
|
||||
tool_data.tools.get(&tool_type).unwrap().activate(responses);
|
||||
|
||||
// Send the SelectionChanged message to the active tool, this will ensure the selection is updated
|
||||
responses.push_back(BroadcastEvent::SelectionChanged.into());
|
||||
|
||||
// Send the DocumentIsDirty message to the active tool's sub-tool message handler
|
||||
responses.push_back(BroadcastEvent::DocumentIsDirty.into());
|
||||
|
||||
// Send Properties to the frontend
|
||||
tool_data.tools.get(&tool_type).unwrap().register_properties(responses, LayoutTarget::ToolOptions);
|
||||
|
||||
// Notify the frontend about the new active tool to be displayed
|
||||
tool_data.register_properties(responses, LayoutTarget::ToolShelf);
|
||||
}
|
||||
ToolMessage::DeactivateTools => {
|
||||
let tool_data = &mut self.tool_state.tool_data;
|
||||
tool_data.tools.get(&tool_data.active_tool_type).unwrap().deactivate(responses);
|
||||
}
|
||||
ToolMessage::InitTools => {
|
||||
let tool_data = &mut self.tool_state.tool_data;
|
||||
let document_data = &self.tool_state.document_tool_data;
|
||||
let active_tool = &tool_data.active_tool_type;
|
||||
|
||||
// Subscribe tool to broadcast messages
|
||||
tool_data.tools.get(active_tool).unwrap().activate(responses);
|
||||
|
||||
// Register initial properties
|
||||
tool_data.tools.get(active_tool).unwrap().register_properties(responses, LayoutTarget::ToolOptions);
|
||||
|
||||
// Notify the frontend about the initial active tool
|
||||
tool_data.register_properties(responses, LayoutTarget::ToolShelf);
|
||||
|
||||
// Notify the frontend about the initial working colors
|
||||
document_data.update_working_colors(responses);
|
||||
responses.push_back(FrontendMessage::TriggerRefreshBoundsOfViewports.into());
|
||||
|
||||
// Set initial hints and cursor
|
||||
tool_data
|
||||
.active_tool_mut()
|
||||
.process_message(ToolMessage::UpdateHints, (document, document_data, input, font_cache), responses);
|
||||
tool_data
|
||||
.active_tool_mut()
|
||||
.process_message(ToolMessage::UpdateCursor, (document, document_data, input, font_cache), responses);
|
||||
}
|
||||
ToolMessage::ResetColors => {
|
||||
let document_data = &mut self.tool_state.document_tool_data;
|
||||
|
||||
document_data.primary_color = Color::BLACK;
|
||||
document_data.secondary_color = Color::WHITE;
|
||||
|
||||
document_data.update_working_colors(responses);
|
||||
}
|
||||
ToolMessage::SelectPrimaryColor { color } => {
|
||||
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);
|
||||
}
|
||||
ToolMessage::SelectRandomPrimaryColor => {
|
||||
// Select a random primary color (rgba) based on an UUID
|
||||
let document_data = &mut self.tool_state.document_tool_data;
|
||||
|
||||
let random_number = generate_uuid();
|
||||
let r = (random_number >> 16) as u8;
|
||||
let g = (random_number >> 8) as u8;
|
||||
let b = random_number as u8;
|
||||
let random_color = Color::from_rgba8(r, g, b, 255);
|
||||
document_data.primary_color = random_color;
|
||||
|
||||
document_data.update_working_colors(responses);
|
||||
}
|
||||
ToolMessage::SelectSecondaryColor { color } => {
|
||||
let document_data = &mut self.tool_state.document_tool_data;
|
||||
document_data.secondary_color = color;
|
||||
|
||||
document_data.update_working_colors(responses);
|
||||
}
|
||||
ToolMessage::SwapColors => {
|
||||
let document_data = &mut self.tool_state.document_tool_data;
|
||||
|
||||
std::mem::swap(&mut document_data.primary_color, &mut document_data.secondary_color);
|
||||
|
||||
document_data.update_working_colors(responses);
|
||||
}
|
||||
|
||||
// Sub-messages
|
||||
#[remain::unsorted]
|
||||
tool_message => {
|
||||
let tool_type = match &tool_message {
|
||||
ToolMessage::UpdateCursor | ToolMessage::UpdateHints => self.tool_state.tool_data.active_tool_type,
|
||||
tool_message => tool_message_to_tool_type(tool_message),
|
||||
};
|
||||
let document_data = &self.tool_state.document_tool_data;
|
||||
let tool_data = &mut self.tool_state.tool_data;
|
||||
|
||||
if let Some(tool) = tool_data.tools.get_mut(&tool_type) {
|
||||
if tool_type == tool_data.active_tool_type {
|
||||
tool.process_message(tool_message, (document, document_data, input, font_cache), responses);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn actions(&self) -> ActionList {
|
||||
let mut list = actions!(ToolMessageDiscriminant;
|
||||
ActivateToolSelect,
|
||||
ActivateToolArtboard,
|
||||
ActivateToolNavigate,
|
||||
ActivateToolEyedropper,
|
||||
ActivateToolText,
|
||||
ActivateToolFill,
|
||||
ActivateToolGradient,
|
||||
ActivateToolPath,
|
||||
ActivateToolPen,
|
||||
ActivateToolFreehand,
|
||||
ActivateToolSpline,
|
||||
ActivateToolLine,
|
||||
ActivateToolRectangle,
|
||||
ActivateToolEllipse,
|
||||
ActivateToolShape,
|
||||
SelectRandomPrimaryColor,
|
||||
ResetColors,
|
||||
SwapColors,
|
||||
);
|
||||
list.extend(self.tool_state.tool_data.active_tool().actions());
|
||||
|
||||
list
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,465 @@
|
||||
use crate::application::generate_uuid;
|
||||
use crate::consts::SELECTION_TOLERANCE;
|
||||
use crate::messages::frontend::utility_types::MouseCursorIcon;
|
||||
use crate::messages::input_mapper::utility_types::input_keyboard::{Key, KeysGroup, MouseMotion};
|
||||
use crate::messages::layout::utility_types::layout_widget::PropertyHolder;
|
||||
use crate::messages::portfolio::document::utility_types::misc::TargetDocument;
|
||||
use crate::messages::prelude::*;
|
||||
use crate::messages::tool::common_functionality::snapping::SnapManager;
|
||||
use crate::messages::tool::common_functionality::transformation_cage::*;
|
||||
use crate::messages::tool::utility_types::{EventToMessageMap, Fsm, ToolActionHandlerData, ToolMetadata, ToolTransition, ToolType};
|
||||
use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo};
|
||||
|
||||
use graphene::intersection::Quad;
|
||||
use graphene::LayerId;
|
||||
|
||||
use glam::{DVec2, Vec2Swizzles};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ArtboardTool {
|
||||
fsm_state: ArtboardToolFsmState,
|
||||
data: ArtboardToolData,
|
||||
}
|
||||
|
||||
#[remain::sorted]
|
||||
#[impl_message(Message, ToolMessage, Artboard)]
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Hash, Serialize, Deserialize)]
|
||||
pub enum ArtboardToolMessage {
|
||||
// Standard messages
|
||||
#[remain::unsorted]
|
||||
Abort,
|
||||
#[remain::unsorted]
|
||||
DocumentIsDirty,
|
||||
|
||||
// Tool-specific messages
|
||||
DeleteSelected,
|
||||
PointerDown,
|
||||
PointerMove {
|
||||
constrain_axis_or_aspect: Key,
|
||||
center: Key,
|
||||
},
|
||||
PointerUp,
|
||||
}
|
||||
|
||||
impl ToolMetadata for ArtboardTool {
|
||||
fn icon_name(&self) -> String {
|
||||
"GeneralArtboardTool".into()
|
||||
}
|
||||
fn tooltip(&self) -> String {
|
||||
"Artboard Tool".into()
|
||||
}
|
||||
fn tool_type(&self) -> crate::messages::tool::utility_types::ToolType {
|
||||
ToolType::Artboard
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for ArtboardTool {
|
||||
fn process_message(&mut self, message: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
|
||||
if message == ToolMessage::UpdateHints {
|
||||
self.fsm_state.update_hints(responses);
|
||||
return;
|
||||
}
|
||||
|
||||
if message == ToolMessage::UpdateCursor {
|
||||
responses.push_back(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }.into());
|
||||
return;
|
||||
}
|
||||
|
||||
let new_state = self.fsm_state.transition(message, &mut self.data, data, &(), responses);
|
||||
|
||||
if self.fsm_state != new_state {
|
||||
self.fsm_state = new_state;
|
||||
self.fsm_state.update_hints(responses);
|
||||
}
|
||||
}
|
||||
|
||||
advertise_actions!(ArtboardToolMessageDiscriminant;
|
||||
PointerDown,
|
||||
PointerUp,
|
||||
PointerMove,
|
||||
DeleteSelected,
|
||||
Abort,
|
||||
);
|
||||
}
|
||||
|
||||
impl PropertyHolder for ArtboardTool {}
|
||||
|
||||
impl ToolTransition for ArtboardTool {
|
||||
fn event_to_message_map(&self) -> EventToMessageMap {
|
||||
EventToMessageMap {
|
||||
document_dirty: Some(ArtboardToolMessage::DocumentIsDirty.into()),
|
||||
tool_abort: Some(ArtboardToolMessage::Abort.into()),
|
||||
selection_changed: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum ArtboardToolFsmState {
|
||||
Ready,
|
||||
Drawing,
|
||||
ResizingBounds,
|
||||
Dragging,
|
||||
}
|
||||
|
||||
impl Default for ArtboardToolFsmState {
|
||||
fn default() -> Self {
|
||||
ArtboardToolFsmState::Ready
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct ArtboardToolData {
|
||||
bounding_box_overlays: Option<BoundingBoxOverlays>,
|
||||
selected_board: Option<LayerId>,
|
||||
snap_manager: SnapManager,
|
||||
cursor: MouseCursorIcon,
|
||||
drag_start: DVec2,
|
||||
drag_current: DVec2,
|
||||
}
|
||||
|
||||
impl Fsm for ArtboardToolFsmState {
|
||||
type ToolData = ArtboardToolData;
|
||||
type ToolOptions = ();
|
||||
|
||||
fn transition(
|
||||
self,
|
||||
event: ToolMessage,
|
||||
tool_data: &mut Self::ToolData,
|
||||
(document, _global_tool_data, input, font_cache): ToolActionHandlerData,
|
||||
_tool_options: &Self::ToolOptions,
|
||||
responses: &mut VecDeque<Message>,
|
||||
) -> Self {
|
||||
if let ToolMessage::Artboard(event) = event {
|
||||
match (self, event) {
|
||||
(ArtboardToolFsmState::Ready | ArtboardToolFsmState::ResizingBounds | ArtboardToolFsmState::Dragging, ArtboardToolMessage::DocumentIsDirty) => {
|
||||
match (
|
||||
tool_data.selected_board.map(|path| document.artboard_bounding_box_and_transform(&[path], font_cache)).unwrap_or(None),
|
||||
tool_data.bounding_box_overlays.take(),
|
||||
) {
|
||||
(None, Some(bounding_box_overlays)) => bounding_box_overlays.delete(responses),
|
||||
(Some((bounds, transform)), paths) => {
|
||||
let mut bounding_box_overlays = paths.unwrap_or_else(|| BoundingBoxOverlays::new(responses));
|
||||
|
||||
bounding_box_overlays.bounds = bounds;
|
||||
bounding_box_overlays.transform = transform;
|
||||
|
||||
bounding_box_overlays.transform(responses);
|
||||
|
||||
tool_data.bounding_box_overlays = Some(bounding_box_overlays);
|
||||
|
||||
responses.push_back(OverlaysMessage::Rerender.into());
|
||||
responses.push_back(
|
||||
PropertiesPanelMessage::SetActiveLayers {
|
||||
paths: vec![vec![tool_data.selected_board.unwrap()]],
|
||||
document: TargetDocument::Artboard,
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
self
|
||||
}
|
||||
(ArtboardToolFsmState::Ready, ArtboardToolMessage::PointerDown) => {
|
||||
tool_data.drag_start = input.mouse.position;
|
||||
tool_data.drag_current = input.mouse.position;
|
||||
|
||||
let dragging_bounds = if let Some(bounding_box) = &mut tool_data.bounding_box_overlays {
|
||||
let edges = bounding_box.check_selected_edges(input.mouse.position);
|
||||
|
||||
bounding_box.selected_edges = edges.map(|(top, bottom, left, right)| {
|
||||
let edges = SelectedEdges::new(top, bottom, left, right, bounding_box.bounds);
|
||||
bounding_box.pivot = edges.calculate_pivot();
|
||||
edges
|
||||
});
|
||||
|
||||
edges
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(selected_edges) = dragging_bounds {
|
||||
let snap_x = selected_edges.2 || selected_edges.3;
|
||||
let snap_y = selected_edges.0 || selected_edges.1;
|
||||
|
||||
tool_data
|
||||
.snap_manager
|
||||
.start_snap(document, document.bounding_boxes(None, Some(tool_data.selected_board.unwrap()), font_cache), snap_x, snap_y);
|
||||
tool_data.snap_manager.add_all_document_handles(document, &[], &[], &[]);
|
||||
|
||||
ArtboardToolFsmState::ResizingBounds
|
||||
} else {
|
||||
let tolerance = DVec2::splat(SELECTION_TOLERANCE);
|
||||
let quad = Quad::from_box([input.mouse.position - tolerance, input.mouse.position + tolerance]);
|
||||
let intersection = document.artboard_message_handler.artboards_graphene_document.intersects_quad_root(quad, font_cache);
|
||||
|
||||
responses.push_back(BroadcastEvent::DocumentIsDirty.into());
|
||||
if let Some(intersection) = intersection.last() {
|
||||
tool_data.selected_board = Some(intersection[0]);
|
||||
|
||||
tool_data
|
||||
.snap_manager
|
||||
.start_snap(document, document.bounding_boxes(None, Some(intersection[0]), font_cache), true, true);
|
||||
tool_data.snap_manager.add_all_document_handles(document, &[], &[], &[]);
|
||||
|
||||
responses.push_back(
|
||||
PropertiesPanelMessage::SetActiveLayers {
|
||||
paths: vec![intersection.clone()],
|
||||
document: TargetDocument::Artboard,
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
|
||||
ArtboardToolFsmState::Dragging
|
||||
} else {
|
||||
let id = generate_uuid();
|
||||
tool_data.selected_board = Some(id);
|
||||
|
||||
tool_data.snap_manager.start_snap(document, document.bounding_boxes(None, Some(id), font_cache), true, true);
|
||||
tool_data.snap_manager.add_all_document_handles(document, &[], &[], &[]);
|
||||
|
||||
responses.push_back(
|
||||
ArtboardMessage::AddArtboard {
|
||||
id: Some(id),
|
||||
position: (0., 0.),
|
||||
size: (0., 0.),
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
|
||||
responses.push_back(PropertiesPanelMessage::ClearSelection.into());
|
||||
|
||||
ArtboardToolFsmState::Drawing
|
||||
}
|
||||
}
|
||||
}
|
||||
(ArtboardToolFsmState::ResizingBounds, ArtboardToolMessage::PointerMove { constrain_axis_or_aspect, center }) => {
|
||||
if let Some(bounds) = &tool_data.bounding_box_overlays {
|
||||
if let Some(movement) = &bounds.selected_edges {
|
||||
let from_center = input.keyboard.get(center as usize);
|
||||
let constrain_square = input.keyboard.get(constrain_axis_or_aspect as usize);
|
||||
|
||||
let mouse_position = input.mouse.position;
|
||||
let snapped_mouse_position = tool_data.snap_manager.snap_position(responses, document, mouse_position);
|
||||
|
||||
let (mut position, size) = movement.new_size(snapped_mouse_position, bounds.transform, from_center, constrain_square);
|
||||
if from_center {
|
||||
position = movement.center_position(position, size);
|
||||
}
|
||||
|
||||
responses.push_back(
|
||||
ArtboardMessage::ResizeArtboard {
|
||||
artboard: tool_data.selected_board.unwrap(),
|
||||
position: position.round().into(),
|
||||
size: size.round().into(),
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
|
||||
responses.push_back(BroadcastEvent::DocumentIsDirty.into());
|
||||
}
|
||||
}
|
||||
ArtboardToolFsmState::ResizingBounds
|
||||
}
|
||||
(ArtboardToolFsmState::Dragging, ArtboardToolMessage::PointerMove { constrain_axis_or_aspect, .. }) => {
|
||||
if let Some(bounds) = &tool_data.bounding_box_overlays {
|
||||
let axis_align = input.keyboard.get(constrain_axis_or_aspect as usize);
|
||||
|
||||
let mouse_position = axis_align_drag(axis_align, input.mouse.position, tool_data.drag_start);
|
||||
let mouse_delta = mouse_position - tool_data.drag_current;
|
||||
|
||||
let snap = bounds.evaluate_transform_handle_positions().into_iter().collect();
|
||||
let closest_move = tool_data.snap_manager.snap_layers(responses, document, snap, mouse_delta);
|
||||
|
||||
let size = bounds.bounds[1] - bounds.bounds[0];
|
||||
|
||||
let position = bounds.bounds[0] + bounds.transform.inverse().transform_vector2(mouse_position - tool_data.drag_current + closest_move);
|
||||
|
||||
responses.push_back(
|
||||
ArtboardMessage::ResizeArtboard {
|
||||
artboard: tool_data.selected_board.unwrap(),
|
||||
position: position.round().into(),
|
||||
size: size.round().into(),
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
|
||||
responses.push_back(BroadcastEvent::DocumentIsDirty.into());
|
||||
|
||||
tool_data.drag_current = mouse_position + closest_move;
|
||||
}
|
||||
ArtboardToolFsmState::Dragging
|
||||
}
|
||||
(ArtboardToolFsmState::Drawing, ArtboardToolMessage::PointerMove { constrain_axis_or_aspect, center }) => {
|
||||
let mouse_position = input.mouse.position;
|
||||
let snapped_mouse_position = tool_data.snap_manager.snap_position(responses, document, mouse_position);
|
||||
|
||||
let root_transform = document.graphene_document.root.transform.inverse();
|
||||
|
||||
let mut start = tool_data.drag_start;
|
||||
let mut size = snapped_mouse_position - start;
|
||||
// Constrain axis
|
||||
if input.keyboard.get(constrain_axis_or_aspect as usize) {
|
||||
size = size.abs().max(size.abs().yx()) * size.signum();
|
||||
}
|
||||
// From center
|
||||
if input.keyboard.get(center as usize) {
|
||||
start -= size;
|
||||
size *= 2.;
|
||||
}
|
||||
|
||||
let start = root_transform.transform_point2(start);
|
||||
let size = root_transform.transform_vector2(size);
|
||||
|
||||
responses.push_back(
|
||||
ArtboardMessage::ResizeArtboard {
|
||||
artboard: tool_data.selected_board.unwrap(),
|
||||
position: start.round().into(),
|
||||
size: size.round().into(),
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
|
||||
// Have to put message here instead of when Artboard is created
|
||||
// This might result in a few more calls but it is not reliant on the order of messages
|
||||
responses.push_back(
|
||||
PropertiesPanelMessage::SetActiveLayers {
|
||||
paths: vec![vec![tool_data.selected_board.unwrap()]],
|
||||
document: TargetDocument::Artboard,
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
|
||||
responses.push_back(BroadcastEvent::DocumentIsDirty.into());
|
||||
|
||||
ArtboardToolFsmState::Drawing
|
||||
}
|
||||
(ArtboardToolFsmState::Ready, ArtboardToolMessage::PointerMove { .. }) => {
|
||||
let cursor = tool_data.bounding_box_overlays.as_ref().map_or(MouseCursorIcon::Default, |bounds| bounds.get_cursor(input, false));
|
||||
|
||||
if tool_data.cursor != cursor {
|
||||
tool_data.cursor = cursor;
|
||||
responses.push_back(FrontendMessage::UpdateMouseCursor { cursor }.into());
|
||||
}
|
||||
|
||||
ArtboardToolFsmState::Ready
|
||||
}
|
||||
(ArtboardToolFsmState::ResizingBounds, ArtboardToolMessage::PointerUp) => {
|
||||
tool_data.snap_manager.cleanup(responses);
|
||||
|
||||
if let Some(bounds) = &mut tool_data.bounding_box_overlays {
|
||||
bounds.original_transforms.clear();
|
||||
}
|
||||
|
||||
ArtboardToolFsmState::Ready
|
||||
}
|
||||
(ArtboardToolFsmState::Drawing, ArtboardToolMessage::PointerUp) => {
|
||||
tool_data.snap_manager.cleanup(responses);
|
||||
|
||||
if let Some(bounds) = &mut tool_data.bounding_box_overlays {
|
||||
bounds.original_transforms.clear();
|
||||
}
|
||||
|
||||
responses.push_back(BroadcastEvent::DocumentIsDirty.into());
|
||||
|
||||
ArtboardToolFsmState::Ready
|
||||
}
|
||||
(ArtboardToolFsmState::Dragging, ArtboardToolMessage::PointerUp) => {
|
||||
tool_data.snap_manager.cleanup(responses);
|
||||
|
||||
if let Some(bounds) = &mut tool_data.bounding_box_overlays {
|
||||
bounds.original_transforms.clear();
|
||||
}
|
||||
|
||||
ArtboardToolFsmState::Ready
|
||||
}
|
||||
(_, ArtboardToolMessage::DeleteSelected) => {
|
||||
if let Some(artboard) = tool_data.selected_board.take() {
|
||||
responses.push_back(ArtboardMessage::DeleteArtboard { artboard }.into());
|
||||
responses.push_back(BroadcastEvent::DocumentIsDirty.into());
|
||||
}
|
||||
ArtboardToolFsmState::Ready
|
||||
}
|
||||
(_, ArtboardToolMessage::Abort) => {
|
||||
if let Some(bounding_box_overlays) = tool_data.bounding_box_overlays.take() {
|
||||
bounding_box_overlays.delete(responses);
|
||||
}
|
||||
|
||||
// Register properties when switching back to other tools
|
||||
responses.push_back(
|
||||
PropertiesPanelMessage::SetActiveLayers {
|
||||
paths: document.selected_layers().map(|path| path.to_vec()).collect(),
|
||||
document: TargetDocument::Artwork,
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
|
||||
tool_data.snap_manager.cleanup(responses);
|
||||
ArtboardToolFsmState::Ready
|
||||
}
|
||||
_ => self,
|
||||
}
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
fn update_hints(&self, responses: &mut VecDeque<Message>) {
|
||||
let hint_data = match self {
|
||||
ArtboardToolFsmState::Ready => HintData(vec![
|
||||
HintGroup(vec![HintInfo {
|
||||
key_groups: vec![],
|
||||
key_groups_mac: None,
|
||||
mouse: Some(MouseMotion::LmbDrag),
|
||||
label: String::from("Draw Artboard"),
|
||||
plus: false,
|
||||
}]),
|
||||
HintGroup(vec![HintInfo {
|
||||
key_groups: vec![],
|
||||
key_groups_mac: None,
|
||||
mouse: Some(MouseMotion::LmbDrag),
|
||||
label: String::from("Move Artboard"),
|
||||
plus: false,
|
||||
}]),
|
||||
HintGroup(vec![HintInfo {
|
||||
key_groups: vec![KeysGroup(vec![Key::KeyBackspace])],
|
||||
key_groups_mac: None,
|
||||
mouse: None,
|
||||
label: String::from("Delete Artboard"),
|
||||
plus: false,
|
||||
}]),
|
||||
]),
|
||||
ArtboardToolFsmState::Dragging => HintData(vec![HintGroup(vec![HintInfo {
|
||||
key_groups: vec![KeysGroup(vec![Key::KeyShift])],
|
||||
key_groups_mac: None,
|
||||
mouse: None,
|
||||
label: String::from("Constrain to Axis"),
|
||||
plus: false,
|
||||
}])]),
|
||||
ArtboardToolFsmState::Drawing | ArtboardToolFsmState::ResizingBounds => HintData(vec![HintGroup(vec![
|
||||
HintInfo {
|
||||
key_groups: vec![KeysGroup(vec![Key::KeyShift])],
|
||||
key_groups_mac: None,
|
||||
mouse: None,
|
||||
label: String::from("Constrain Square"),
|
||||
plus: false,
|
||||
},
|
||||
HintInfo {
|
||||
key_groups: vec![KeysGroup(vec![Key::KeyAlt])],
|
||||
key_groups_mac: None,
|
||||
mouse: None,
|
||||
label: String::from("From Center"),
|
||||
plus: false,
|
||||
},
|
||||
])]),
|
||||
};
|
||||
|
||||
responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into());
|
||||
}
|
||||
|
||||
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
|
||||
responses.push_back(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }.into());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
use crate::consts::DRAG_THRESHOLD;
|
||||
use crate::messages::frontend::utility_types::MouseCursorIcon;
|
||||
use crate::messages::input_mapper::utility_types::input_keyboard::{Key, KeysGroup, MouseMotion};
|
||||
use crate::messages::layout::utility_types::layout_widget::PropertyHolder;
|
||||
use crate::messages::prelude::*;
|
||||
use crate::messages::tool::common_functionality::resize::Resize;
|
||||
use crate::messages::tool::utility_types::{EventToMessageMap, Fsm, ToolActionHandlerData, ToolMetadata, ToolTransition, ToolType};
|
||||
use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo};
|
||||
|
||||
use graphene::layers::style;
|
||||
use graphene::Operation;
|
||||
|
||||
use glam::DAffine2;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct EllipseTool {
|
||||
fsm_state: EllipseToolFsmState,
|
||||
data: EllipseToolData,
|
||||
}
|
||||
|
||||
#[remain::sorted]
|
||||
#[impl_message(Message, ToolMessage, Ellipse)]
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Hash, Serialize, Deserialize)]
|
||||
pub enum EllipseToolMessage {
|
||||
// Standard messages
|
||||
#[remain::unsorted]
|
||||
Abort,
|
||||
|
||||
// Tool-specific messages
|
||||
DragStart,
|
||||
DragStop,
|
||||
Resize {
|
||||
center: Key,
|
||||
lock_ratio: Key,
|
||||
},
|
||||
}
|
||||
|
||||
impl ToolMetadata for EllipseTool {
|
||||
fn icon_name(&self) -> String {
|
||||
"VectorEllipseTool".into()
|
||||
}
|
||||
fn tooltip(&self) -> String {
|
||||
"Ellipse Tool".into()
|
||||
}
|
||||
fn tool_type(&self) -> crate::messages::tool::utility_types::ToolType {
|
||||
ToolType::Ellipse
|
||||
}
|
||||
}
|
||||
|
||||
impl PropertyHolder for EllipseTool {}
|
||||
|
||||
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for EllipseTool {
|
||||
fn process_message(&mut self, message: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
|
||||
if message == ToolMessage::UpdateHints {
|
||||
self.fsm_state.update_hints(responses);
|
||||
return;
|
||||
}
|
||||
|
||||
if message == ToolMessage::UpdateCursor {
|
||||
self.fsm_state.update_cursor(responses);
|
||||
return;
|
||||
}
|
||||
|
||||
let new_state = self.fsm_state.transition(message, &mut self.data, data, &(), responses);
|
||||
|
||||
if self.fsm_state != new_state {
|
||||
self.fsm_state = new_state;
|
||||
self.fsm_state.update_hints(responses);
|
||||
self.fsm_state.update_cursor(responses);
|
||||
}
|
||||
}
|
||||
|
||||
fn actions(&self) -> ActionList {
|
||||
use EllipseToolFsmState::*;
|
||||
|
||||
match self.fsm_state {
|
||||
Ready => actions!(EllipseToolMessageDiscriminant;
|
||||
DragStart,
|
||||
),
|
||||
Drawing => actions!(EllipseToolMessageDiscriminant;
|
||||
DragStop,
|
||||
Abort,
|
||||
Resize,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolTransition for EllipseTool {
|
||||
fn event_to_message_map(&self) -> EventToMessageMap {
|
||||
EventToMessageMap {
|
||||
document_dirty: None,
|
||||
tool_abort: Some(EllipseToolMessage::Abort.into()),
|
||||
selection_changed: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum EllipseToolFsmState {
|
||||
Ready,
|
||||
Drawing,
|
||||
}
|
||||
|
||||
impl Default for EllipseToolFsmState {
|
||||
fn default() -> Self {
|
||||
EllipseToolFsmState::Ready
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct EllipseToolData {
|
||||
data: Resize,
|
||||
}
|
||||
|
||||
impl Fsm for EllipseToolFsmState {
|
||||
type ToolData = EllipseToolData;
|
||||
type ToolOptions = ();
|
||||
|
||||
fn transition(
|
||||
self,
|
||||
event: ToolMessage,
|
||||
tool_data: &mut Self::ToolData,
|
||||
(document, global_tool_data, input, font_cache): ToolActionHandlerData,
|
||||
_tool_options: &Self::ToolOptions,
|
||||
responses: &mut VecDeque<Message>,
|
||||
) -> Self {
|
||||
use EllipseToolFsmState::*;
|
||||
use EllipseToolMessage::*;
|
||||
|
||||
let mut shape_data = &mut tool_data.data;
|
||||
|
||||
if let ToolMessage::Ellipse(event) = event {
|
||||
match (self, event) {
|
||||
(Ready, DragStart) => {
|
||||
shape_data.start(responses, document, input.mouse.position, font_cache);
|
||||
responses.push_back(DocumentMessage::StartTransaction.into());
|
||||
shape_data.path = Some(document.get_path_for_new_layer());
|
||||
responses.push_back(DocumentMessage::DeselectAllLayers.into());
|
||||
|
||||
responses.push_back(
|
||||
Operation::AddEllipse {
|
||||
path: shape_data.path.clone().unwrap(),
|
||||
insert_index: -1,
|
||||
transform: DAffine2::ZERO.to_cols_array(),
|
||||
style: style::PathStyle::new(None, style::Fill::solid(global_tool_data.primary_color)),
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
|
||||
Drawing
|
||||
}
|
||||
(state, Resize { center, lock_ratio }) => {
|
||||
if let Some(message) = shape_data.calculate_transform(responses, document, center, lock_ratio, input) {
|
||||
responses.push_back(message);
|
||||
}
|
||||
|
||||
state
|
||||
}
|
||||
(Drawing, DragStop) => {
|
||||
match shape_data.drag_start.distance(input.mouse.position) <= DRAG_THRESHOLD {
|
||||
true => responses.push_back(DocumentMessage::AbortTransaction.into()),
|
||||
false => responses.push_back(DocumentMessage::CommitTransaction.into()),
|
||||
}
|
||||
|
||||
shape_data.cleanup(responses);
|
||||
Ready
|
||||
}
|
||||
(Drawing, Abort) => {
|
||||
responses.push_back(DocumentMessage::AbortTransaction.into());
|
||||
shape_data.cleanup(responses);
|
||||
|
||||
Ready
|
||||
}
|
||||
_ => self,
|
||||
}
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
fn update_hints(&self, responses: &mut VecDeque<Message>) {
|
||||
let hint_data = match self {
|
||||
EllipseToolFsmState::Ready => HintData(vec![HintGroup(vec![
|
||||
HintInfo {
|
||||
key_groups: vec![],
|
||||
key_groups_mac: None,
|
||||
mouse: Some(MouseMotion::LmbDrag),
|
||||
label: String::from("Draw Ellipse"),
|
||||
plus: false,
|
||||
},
|
||||
HintInfo {
|
||||
key_groups: vec![KeysGroup(vec![Key::KeyShift])],
|
||||
key_groups_mac: None,
|
||||
mouse: None,
|
||||
label: String::from("Constrain Circular"),
|
||||
plus: true,
|
||||
},
|
||||
HintInfo {
|
||||
key_groups: vec![KeysGroup(vec![Key::KeyAlt])],
|
||||
key_groups_mac: None,
|
||||
mouse: None,
|
||||
label: String::from("From Center"),
|
||||
plus: true,
|
||||
},
|
||||
])]),
|
||||
EllipseToolFsmState::Drawing => HintData(vec![HintGroup(vec![
|
||||
HintInfo {
|
||||
key_groups: vec![KeysGroup(vec![Key::KeyShift])],
|
||||
key_groups_mac: None,
|
||||
mouse: None,
|
||||
label: String::from("Constrain Circular"),
|
||||
plus: false,
|
||||
},
|
||||
HintInfo {
|
||||
key_groups: vec![KeysGroup(vec![Key::KeyAlt])],
|
||||
key_groups_mac: None,
|
||||
mouse: None,
|
||||
label: String::from("From Center"),
|
||||
plus: false,
|
||||
},
|
||||
])]),
|
||||
};
|
||||
|
||||
responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into());
|
||||
}
|
||||
|
||||
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
|
||||
responses.push_back(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Crosshair }.into());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
use crate::consts::SELECTION_TOLERANCE;
|
||||
use crate::messages::frontend::utility_types::MouseCursorIcon;
|
||||
use crate::messages::input_mapper::utility_types::input_keyboard::MouseMotion;
|
||||
use crate::messages::layout::utility_types::layout_widget::PropertyHolder;
|
||||
use crate::messages::prelude::*;
|
||||
use crate::messages::tool::utility_types::{EventToMessageMap, Fsm, ToolActionHandlerData, ToolMetadata, ToolTransition, ToolType};
|
||||
use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo};
|
||||
|
||||
use graphene::intersection::Quad;
|
||||
use graphene::layers::layer_info::LayerDataType;
|
||||
|
||||
use glam::DVec2;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct EyedropperTool {
|
||||
fsm_state: EyedropperToolFsmState,
|
||||
data: EyedropperToolData,
|
||||
}
|
||||
|
||||
#[remain::sorted]
|
||||
#[impl_message(Message, ToolMessage, Eyedropper)]
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Hash, Serialize, Deserialize)]
|
||||
pub enum EyedropperToolMessage {
|
||||
// Standard messages
|
||||
#[remain::unsorted]
|
||||
Abort,
|
||||
|
||||
// Tool-specific messages
|
||||
LeftMouseDown,
|
||||
RightMouseDown,
|
||||
}
|
||||
|
||||
impl ToolMetadata for EyedropperTool {
|
||||
fn icon_name(&self) -> String {
|
||||
"GeneralEyedropperTool".into()
|
||||
}
|
||||
fn tooltip(&self) -> String {
|
||||
"Eyedropper Tool".into()
|
||||
}
|
||||
fn tool_type(&self) -> crate::messages::tool::utility_types::ToolType {
|
||||
ToolType::Eyedropper
|
||||
}
|
||||
}
|
||||
|
||||
impl PropertyHolder for EyedropperTool {}
|
||||
|
||||
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for EyedropperTool {
|
||||
fn process_message(&mut self, message: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
|
||||
if message == ToolMessage::UpdateHints {
|
||||
self.fsm_state.update_hints(responses);
|
||||
return;
|
||||
}
|
||||
|
||||
if message == ToolMessage::UpdateCursor {
|
||||
self.fsm_state.update_cursor(responses);
|
||||
return;
|
||||
}
|
||||
|
||||
let new_state = self.fsm_state.transition(message, &mut self.data, data, &(), responses);
|
||||
|
||||
if self.fsm_state != new_state {
|
||||
self.fsm_state = new_state;
|
||||
self.fsm_state.update_hints(responses);
|
||||
self.fsm_state.update_cursor(responses);
|
||||
}
|
||||
}
|
||||
|
||||
advertise_actions!(EyedropperToolMessageDiscriminant;
|
||||
LeftMouseDown,
|
||||
RightMouseDown,
|
||||
);
|
||||
}
|
||||
|
||||
impl ToolTransition for EyedropperTool {
|
||||
fn event_to_message_map(&self) -> EventToMessageMap {
|
||||
EventToMessageMap {
|
||||
document_dirty: None,
|
||||
tool_abort: Some(EyedropperToolMessage::Abort.into()),
|
||||
selection_changed: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum EyedropperToolFsmState {
|
||||
Ready,
|
||||
}
|
||||
|
||||
impl Default for EyedropperToolFsmState {
|
||||
fn default() -> Self {
|
||||
EyedropperToolFsmState::Ready
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct EyedropperToolData {}
|
||||
|
||||
impl Fsm for EyedropperToolFsmState {
|
||||
type ToolData = EyedropperToolData;
|
||||
type ToolOptions = ();
|
||||
|
||||
fn transition(
|
||||
self,
|
||||
event: ToolMessage,
|
||||
_tool_data: &mut Self::ToolData,
|
||||
(document, _global_tool_data, input, font_cache): ToolActionHandlerData,
|
||||
_tool_options: &Self::ToolOptions,
|
||||
responses: &mut VecDeque<Message>,
|
||||
) -> Self {
|
||||
use EyedropperToolFsmState::*;
|
||||
use EyedropperToolMessage::*;
|
||||
|
||||
if let ToolMessage::Eyedropper(event) = event {
|
||||
match (self, event) {
|
||||
(Ready, lmb_or_rmb) if lmb_or_rmb == LeftMouseDown || lmb_or_rmb == RightMouseDown => {
|
||||
let mouse_pos = input.mouse.position;
|
||||
let tolerance = DVec2::splat(SELECTION_TOLERANCE);
|
||||
let quad = Quad::from_box([mouse_pos - tolerance, mouse_pos + tolerance]);
|
||||
|
||||
// TODO: Destroy this pyramid
|
||||
if let Some(path) = document.graphene_document.intersects_quad_root(quad, font_cache).last() {
|
||||
if let Ok(layer) = document.graphene_document.layer(path) {
|
||||
if let LayerDataType::Shape(shape) = &layer.data {
|
||||
if shape.style.fill().is_some() {
|
||||
match lmb_or_rmb {
|
||||
EyedropperToolMessage::LeftMouseDown => responses.push_back(ToolMessage::SelectPrimaryColor { color: shape.style.fill().color() }.into()),
|
||||
EyedropperToolMessage::RightMouseDown => responses.push_back(ToolMessage::SelectSecondaryColor { color: shape.style.fill().color() }.into()),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ready
|
||||
}
|
||||
_ => self,
|
||||
}
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
fn update_hints(&self, responses: &mut VecDeque<Message>) {
|
||||
let hint_data = match self {
|
||||
EyedropperToolFsmState::Ready => HintData(vec![HintGroup(vec![
|
||||
HintInfo {
|
||||
key_groups: vec![],
|
||||
key_groups_mac: None,
|
||||
mouse: Some(MouseMotion::Lmb),
|
||||
label: String::from("Sample to Primary"),
|
||||
plus: false,
|
||||
},
|
||||
HintInfo {
|
||||
key_groups: vec![],
|
||||
key_groups_mac: None,
|
||||
mouse: Some(MouseMotion::Rmb),
|
||||
label: String::from("Sample to Secondary"),
|
||||
plus: false,
|
||||
},
|
||||
])]),
|
||||
};
|
||||
|
||||
responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into());
|
||||
}
|
||||
|
||||
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
|
||||
responses.push_back(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }.into());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
use crate::consts::SELECTION_TOLERANCE;
|
||||
use crate::messages::frontend::utility_types::MouseCursorIcon;
|
||||
use crate::messages::input_mapper::utility_types::input_keyboard::MouseMotion;
|
||||
use crate::messages::layout::utility_types::layout_widget::PropertyHolder;
|
||||
use crate::messages::prelude::*;
|
||||
use crate::messages::tool::utility_types::{EventToMessageMap, Fsm, ToolActionHandlerData, ToolMetadata, ToolTransition, ToolType};
|
||||
use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo};
|
||||
|
||||
use graphene::intersection::Quad;
|
||||
use graphene::Operation;
|
||||
|
||||
use glam::DVec2;
|
||||
use graphene::layers::style::Fill;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct FillTool {
|
||||
fsm_state: FillToolFsmState,
|
||||
data: FillToolData,
|
||||
}
|
||||
|
||||
#[remain::sorted]
|
||||
#[impl_message(Message, ToolMessage, Fill)]
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Hash, Serialize, Deserialize)]
|
||||
pub enum FillToolMessage {
|
||||
// Standard messages
|
||||
#[remain::unsorted]
|
||||
Abort,
|
||||
|
||||
// Tool-specific messages
|
||||
LeftMouseDown,
|
||||
RightMouseDown,
|
||||
}
|
||||
|
||||
impl ToolMetadata for FillTool {
|
||||
fn icon_name(&self) -> String {
|
||||
"GeneralFillTool".into()
|
||||
}
|
||||
fn tooltip(&self) -> String {
|
||||
"Fill Tool".into()
|
||||
}
|
||||
fn tool_type(&self) -> crate::messages::tool::utility_types::ToolType {
|
||||
ToolType::Fill
|
||||
}
|
||||
}
|
||||
|
||||
impl PropertyHolder for FillTool {}
|
||||
|
||||
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for FillTool {
|
||||
fn process_message(&mut self, message: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
|
||||
if message == ToolMessage::UpdateHints {
|
||||
self.fsm_state.update_hints(responses);
|
||||
return;
|
||||
}
|
||||
|
||||
if message == ToolMessage::UpdateCursor {
|
||||
self.fsm_state.update_cursor(responses);
|
||||
return;
|
||||
}
|
||||
|
||||
let new_state = self.fsm_state.transition(message, &mut self.data, data, &(), responses);
|
||||
|
||||
if self.fsm_state != new_state {
|
||||
self.fsm_state = new_state;
|
||||
self.fsm_state.update_hints(responses);
|
||||
self.fsm_state.update_cursor(responses);
|
||||
}
|
||||
}
|
||||
|
||||
advertise_actions!(FillToolMessageDiscriminant;
|
||||
LeftMouseDown,
|
||||
RightMouseDown,
|
||||
);
|
||||
}
|
||||
|
||||
impl ToolTransition for FillTool {
|
||||
fn event_to_message_map(&self) -> EventToMessageMap {
|
||||
EventToMessageMap {
|
||||
document_dirty: None,
|
||||
tool_abort: Some(FillToolMessage::Abort.into()),
|
||||
selection_changed: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum FillToolFsmState {
|
||||
Ready,
|
||||
}
|
||||
|
||||
impl Default for FillToolFsmState {
|
||||
fn default() -> Self {
|
||||
FillToolFsmState::Ready
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct FillToolData {}
|
||||
|
||||
impl Fsm for FillToolFsmState {
|
||||
type ToolData = FillToolData;
|
||||
type ToolOptions = ();
|
||||
|
||||
fn transition(
|
||||
self,
|
||||
event: ToolMessage,
|
||||
_tool_data: &mut Self::ToolData,
|
||||
(document, global_tool_data, input, font_cache): ToolActionHandlerData,
|
||||
_tool_options: &Self::ToolOptions,
|
||||
responses: &mut VecDeque<Message>,
|
||||
) -> Self {
|
||||
use FillToolFsmState::*;
|
||||
use FillToolMessage::*;
|
||||
|
||||
if let ToolMessage::Fill(event) = event {
|
||||
match (self, event) {
|
||||
(Ready, lmb_or_rmb) if lmb_or_rmb == LeftMouseDown || lmb_or_rmb == RightMouseDown => {
|
||||
let mouse_pos = input.mouse.position;
|
||||
let tolerance = DVec2::splat(SELECTION_TOLERANCE);
|
||||
let quad = Quad::from_box([mouse_pos - tolerance, mouse_pos + tolerance]);
|
||||
|
||||
if let Some(path) = document.graphene_document.intersects_quad_root(quad, font_cache).last() {
|
||||
let color = match lmb_or_rmb {
|
||||
LeftMouseDown => global_tool_data.primary_color,
|
||||
RightMouseDown => global_tool_data.secondary_color,
|
||||
Abort => unreachable!(),
|
||||
};
|
||||
let fill = Fill::Solid(color);
|
||||
|
||||
responses.push_back(DocumentMessage::StartTransaction.into());
|
||||
responses.push_back(Operation::SetLayerFill { path: path.to_vec(), fill }.into());
|
||||
responses.push_back(DocumentMessage::CommitTransaction.into());
|
||||
}
|
||||
|
||||
Ready
|
||||
}
|
||||
_ => self,
|
||||
}
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
fn update_hints(&self, responses: &mut VecDeque<Message>) {
|
||||
let hint_data = match self {
|
||||
FillToolFsmState::Ready => HintData(vec![HintGroup(vec![
|
||||
HintInfo {
|
||||
key_groups: vec![],
|
||||
key_groups_mac: None,
|
||||
mouse: Some(MouseMotion::Lmb),
|
||||
label: String::from("Fill with Primary"),
|
||||
plus: false,
|
||||
},
|
||||
HintInfo {
|
||||
key_groups: vec![],
|
||||
key_groups_mac: None,
|
||||
mouse: Some(MouseMotion::Rmb),
|
||||
label: String::from("Fill with Secondary"),
|
||||
plus: false,
|
||||
},
|
||||
])]),
|
||||
};
|
||||
|
||||
responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into());
|
||||
}
|
||||
|
||||
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
|
||||
responses.push_back(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }.into());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
use crate::messages::frontend::utility_types::MouseCursorIcon;
|
||||
use crate::messages::input_mapper::utility_types::input_keyboard::MouseMotion;
|
||||
use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, PropertyHolder, Widget, WidgetCallback, WidgetHolder, WidgetLayout};
|
||||
use crate::messages::layout::utility_types::widgets::input_widgets::NumberInput;
|
||||
use crate::messages::prelude::*;
|
||||
use crate::messages::tool::utility_types::{DocumentToolData, EventToMessageMap, Fsm, ToolActionHandlerData, ToolMetadata, ToolTransition, ToolType};
|
||||
use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo};
|
||||
|
||||
use graphene::layers::style;
|
||||
use graphene::LayerId;
|
||||
use graphene::Operation;
|
||||
|
||||
use glam::{DAffine2, DVec2};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct FreehandTool {
|
||||
fsm_state: FreehandToolFsmState,
|
||||
data: FreehandToolData,
|
||||
options: FreehandOptions,
|
||||
}
|
||||
|
||||
pub struct FreehandOptions {
|
||||
line_weight: f64,
|
||||
}
|
||||
|
||||
impl Default for FreehandOptions {
|
||||
fn default() -> Self {
|
||||
Self { line_weight: 5. }
|
||||
}
|
||||
}
|
||||
|
||||
#[remain::sorted]
|
||||
#[impl_message(Message, ToolMessage, Freehand)]
|
||||
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum FreehandToolMessage {
|
||||
// Standard messages
|
||||
#[remain::unsorted]
|
||||
Abort,
|
||||
|
||||
// Tool-specific messages
|
||||
DragStart,
|
||||
DragStop,
|
||||
PointerMove,
|
||||
UpdateOptions(FreehandToolMessageOptionsUpdate),
|
||||
}
|
||||
|
||||
#[remain::sorted]
|
||||
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum FreehandToolMessageOptionsUpdate {
|
||||
LineWeight(f64),
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum FreehandToolFsmState {
|
||||
Ready,
|
||||
Drawing,
|
||||
}
|
||||
|
||||
impl ToolMetadata for FreehandTool {
|
||||
fn icon_name(&self) -> String {
|
||||
"VectorFreehandTool".into()
|
||||
}
|
||||
fn tooltip(&self) -> String {
|
||||
"Freehand Tool".into()
|
||||
}
|
||||
fn tool_type(&self) -> crate::messages::tool::utility_types::ToolType {
|
||||
ToolType::Freehand
|
||||
}
|
||||
}
|
||||
|
||||
impl PropertyHolder for FreehandTool {
|
||||
fn properties(&self) -> Layout {
|
||||
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row {
|
||||
widgets: vec![WidgetHolder::new(Widget::NumberInput(NumberInput {
|
||||
unit: " px".into(),
|
||||
label: "Weight".into(),
|
||||
value: Some(self.options.line_weight as f64),
|
||||
is_integer: false,
|
||||
min: Some(1.),
|
||||
on_update: WidgetCallback::new(|number_input: &NumberInput| FreehandToolMessage::UpdateOptions(FreehandToolMessageOptionsUpdate::LineWeight(number_input.value.unwrap())).into()),
|
||||
..NumberInput::default()
|
||||
}))],
|
||||
}]))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for FreehandTool {
|
||||
fn process_message(&mut self, message: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
|
||||
if message == ToolMessage::UpdateHints {
|
||||
self.fsm_state.update_hints(responses);
|
||||
return;
|
||||
}
|
||||
|
||||
if message == ToolMessage::UpdateCursor {
|
||||
self.fsm_state.update_cursor(responses);
|
||||
return;
|
||||
}
|
||||
|
||||
if let ToolMessage::Freehand(FreehandToolMessage::UpdateOptions(action)) = message {
|
||||
match action {
|
||||
FreehandToolMessageOptionsUpdate::LineWeight(line_weight) => self.options.line_weight = line_weight,
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let new_state = self.fsm_state.transition(message, &mut self.data, data, &self.options, responses);
|
||||
|
||||
if self.fsm_state != new_state {
|
||||
self.fsm_state = new_state;
|
||||
self.fsm_state.update_hints(responses);
|
||||
self.fsm_state.update_cursor(responses);
|
||||
}
|
||||
}
|
||||
|
||||
fn actions(&self) -> ActionList {
|
||||
use FreehandToolFsmState::*;
|
||||
|
||||
match self.fsm_state {
|
||||
Ready => actions!(FreehandToolMessageDiscriminant;
|
||||
DragStart,
|
||||
DragStop,
|
||||
Abort,
|
||||
),
|
||||
Drawing => actions!(FreehandToolMessageDiscriminant;
|
||||
DragStop,
|
||||
PointerMove,
|
||||
Abort,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolTransition for FreehandTool {
|
||||
fn event_to_message_map(&self) -> EventToMessageMap {
|
||||
EventToMessageMap {
|
||||
document_dirty: None,
|
||||
tool_abort: Some(FreehandToolMessage::Abort.into()),
|
||||
selection_changed: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for FreehandToolFsmState {
|
||||
fn default() -> Self {
|
||||
FreehandToolFsmState::Ready
|
||||
}
|
||||
}
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct FreehandToolData {
|
||||
points: Vec<DVec2>,
|
||||
weight: f64,
|
||||
path: Option<Vec<LayerId>>,
|
||||
}
|
||||
|
||||
impl Fsm for FreehandToolFsmState {
|
||||
type ToolData = FreehandToolData;
|
||||
type ToolOptions = FreehandOptions;
|
||||
|
||||
fn transition(
|
||||
self,
|
||||
event: ToolMessage,
|
||||
tool_data: &mut Self::ToolData,
|
||||
(document, global_tool_data, input, _font_cache): ToolActionHandlerData,
|
||||
tool_options: &Self::ToolOptions,
|
||||
responses: &mut VecDeque<Message>,
|
||||
) -> Self {
|
||||
use FreehandToolFsmState::*;
|
||||
use FreehandToolMessage::*;
|
||||
|
||||
let transform = document.graphene_document.root.transform;
|
||||
|
||||
if let ToolMessage::Freehand(event) = event {
|
||||
match (self, event) {
|
||||
(Ready, DragStart) => {
|
||||
responses.push_back(DocumentMessage::StartTransaction.into());
|
||||
responses.push_back(DocumentMessage::DeselectAllLayers.into());
|
||||
tool_data.path = Some(document.get_path_for_new_layer());
|
||||
|
||||
let pos = transform.inverse().transform_point2(input.mouse.position);
|
||||
|
||||
tool_data.points.push(pos);
|
||||
|
||||
tool_data.weight = tool_options.line_weight;
|
||||
|
||||
responses.push_back(add_polyline(tool_data, global_tool_data));
|
||||
|
||||
Drawing
|
||||
}
|
||||
(Drawing, PointerMove) => {
|
||||
let pos = transform.inverse().transform_point2(input.mouse.position);
|
||||
|
||||
if tool_data.points.last() != Some(&pos) {
|
||||
tool_data.points.push(pos);
|
||||
}
|
||||
|
||||
responses.push_back(remove_preview(tool_data));
|
||||
responses.push_back(add_polyline(tool_data, global_tool_data));
|
||||
|
||||
Drawing
|
||||
}
|
||||
(Drawing, DragStop) | (Drawing, Abort) => {
|
||||
if tool_data.points.len() >= 2 {
|
||||
responses.push_back(remove_preview(tool_data));
|
||||
responses.push_back(add_polyline(tool_data, global_tool_data));
|
||||
responses.push_back(DocumentMessage::CommitTransaction.into());
|
||||
} else {
|
||||
responses.push_back(DocumentMessage::AbortTransaction.into());
|
||||
}
|
||||
|
||||
tool_data.path = None;
|
||||
tool_data.points.clear();
|
||||
|
||||
Ready
|
||||
}
|
||||
_ => self,
|
||||
}
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
fn update_hints(&self, responses: &mut VecDeque<Message>) {
|
||||
let hint_data = match self {
|
||||
FreehandToolFsmState::Ready => HintData(vec![HintGroup(vec![HintInfo {
|
||||
key_groups: vec![],
|
||||
key_groups_mac: None,
|
||||
mouse: Some(MouseMotion::LmbDrag),
|
||||
label: String::from("Draw Polyline"),
|
||||
plus: false,
|
||||
}])]),
|
||||
FreehandToolFsmState::Drawing => HintData(vec![]),
|
||||
};
|
||||
|
||||
responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into());
|
||||
}
|
||||
|
||||
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
|
||||
responses.push_back(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }.into());
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_preview(data: &FreehandToolData) -> Message {
|
||||
Operation::DeleteLayer { path: data.path.clone().unwrap() }.into()
|
||||
}
|
||||
|
||||
fn add_polyline(data: &FreehandToolData, tool_data: &DocumentToolData) -> Message {
|
||||
let points: Vec<(f64, f64)> = data.points.iter().map(|p| (p.x, p.y)).collect();
|
||||
|
||||
Operation::AddPolyline {
|
||||
path: data.path.clone().unwrap(),
|
||||
insert_index: -1,
|
||||
transform: DAffine2::IDENTITY.to_cols_array(),
|
||||
points,
|
||||
style: style::PathStyle::new(Some(style::Stroke::new(tool_data.primary_color, data.weight)), style::Fill::None),
|
||||
}
|
||||
.into()
|
||||
}
|
||||
@@ -0,0 +1,494 @@
|
||||
use crate::application::generate_uuid;
|
||||
use crate::consts::{COLOR_ACCENT, LINE_ROTATE_SNAP_ANGLE, MANIPULATOR_GROUP_MARKER_SIZE, SELECTION_TOLERANCE};
|
||||
use crate::messages::frontend::utility_types::MouseCursorIcon;
|
||||
use crate::messages::input_mapper::utility_types::input_keyboard::{Key, KeysGroup, MouseMotion};
|
||||
use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, PropertyHolder, Widget, WidgetCallback, WidgetHolder, WidgetLayout};
|
||||
use crate::messages::layout::utility_types::widgets::input_widgets::{RadioEntryData, RadioInput};
|
||||
use crate::messages::prelude::*;
|
||||
use crate::messages::tool::common_functionality::snapping::SnapManager;
|
||||
use crate::messages::tool::utility_types::{EventToMessageMap, Fsm, ToolActionHandlerData, ToolMetadata, ToolTransition, ToolType};
|
||||
use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo};
|
||||
|
||||
use graphene::color::Color;
|
||||
use graphene::intersection::Quad;
|
||||
use graphene::layers::layer_info::Layer;
|
||||
use graphene::layers::style::{Fill, Gradient, GradientType, PathStyle, Stroke};
|
||||
use graphene::LayerId;
|
||||
use graphene::Operation;
|
||||
|
||||
use glam::{DAffine2, DVec2};
|
||||
use graphene::layers::text_layer::FontCache;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct GradientTool {
|
||||
fsm_state: GradientToolFsmState,
|
||||
data: GradientToolData,
|
||||
options: GradientOptions,
|
||||
}
|
||||
|
||||
pub struct GradientOptions {
|
||||
gradient_type: GradientType,
|
||||
}
|
||||
|
||||
impl Default for GradientOptions {
|
||||
fn default() -> Self {
|
||||
Self { gradient_type: GradientType::Linear }
|
||||
}
|
||||
}
|
||||
|
||||
#[remain::sorted]
|
||||
#[impl_message(Message, ToolMessage, Gradient)]
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Hash, Serialize, Deserialize)]
|
||||
pub enum GradientToolMessage {
|
||||
// Standard messages
|
||||
#[remain::unsorted]
|
||||
Abort,
|
||||
#[remain::unsorted]
|
||||
DocumentIsDirty,
|
||||
|
||||
// Tool-specific messages
|
||||
PointerDown,
|
||||
PointerMove {
|
||||
constrain_axis: Key,
|
||||
},
|
||||
PointerUp,
|
||||
UpdateOptions(GradientOptionsUpdate),
|
||||
}
|
||||
|
||||
#[remain::sorted]
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Hash, Serialize, Deserialize)]
|
||||
pub enum GradientOptionsUpdate {
|
||||
Type(GradientType),
|
||||
}
|
||||
|
||||
impl ToolMetadata for GradientTool {
|
||||
fn icon_name(&self) -> String {
|
||||
"GeneralGradientTool".into()
|
||||
}
|
||||
fn tooltip(&self) -> String {
|
||||
"Gradient Tool".into()
|
||||
}
|
||||
fn tool_type(&self) -> crate::messages::tool::utility_types::ToolType {
|
||||
ToolType::Gradient
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for GradientTool {
|
||||
fn process_message(&mut self, message: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
|
||||
if message == ToolMessage::UpdateHints {
|
||||
self.fsm_state.update_hints(responses);
|
||||
return;
|
||||
}
|
||||
|
||||
if message == ToolMessage::UpdateCursor {
|
||||
self.fsm_state.update_cursor(responses);
|
||||
return;
|
||||
}
|
||||
if let ToolMessage::Gradient(GradientToolMessage::UpdateOptions(action)) = message {
|
||||
match action {
|
||||
GradientOptionsUpdate::Type(gradient_type) => self.options.gradient_type = gradient_type,
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let new_state = self.fsm_state.transition(message, &mut self.data, data, &self.options, responses);
|
||||
|
||||
if self.fsm_state != new_state {
|
||||
self.fsm_state = new_state;
|
||||
self.fsm_state.update_hints(responses);
|
||||
}
|
||||
}
|
||||
|
||||
advertise_actions!(GradientToolMessageDiscriminant;
|
||||
PointerDown,
|
||||
PointerUp,
|
||||
PointerMove,
|
||||
Abort,
|
||||
);
|
||||
}
|
||||
|
||||
impl PropertyHolder for GradientTool {
|
||||
fn properties(&self) -> Layout {
|
||||
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row {
|
||||
widgets: vec![WidgetHolder::new(Widget::RadioInput(RadioInput {
|
||||
selected_index: if self.options.gradient_type == GradientType::Radial { 1 } else { 0 },
|
||||
entries: vec![
|
||||
RadioEntryData {
|
||||
value: "linear".into(),
|
||||
label: "Linear".into(),
|
||||
tooltip: "Linear Gradient".into(),
|
||||
on_update: WidgetCallback::new(move |_| GradientToolMessage::UpdateOptions(GradientOptionsUpdate::Type(GradientType::Linear)).into()),
|
||||
..RadioEntryData::default()
|
||||
},
|
||||
RadioEntryData {
|
||||
value: "radial".into(),
|
||||
label: "Radial".into(),
|
||||
tooltip: "Radial Gradient".into(),
|
||||
on_update: WidgetCallback::new(move |_| GradientToolMessage::UpdateOptions(GradientOptionsUpdate::Type(GradientType::Radial)).into()),
|
||||
..RadioEntryData::default()
|
||||
},
|
||||
],
|
||||
}))],
|
||||
}]))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum GradientToolFsmState {
|
||||
Ready,
|
||||
Drawing,
|
||||
}
|
||||
|
||||
impl Default for GradientToolFsmState {
|
||||
fn default() -> Self {
|
||||
GradientToolFsmState::Ready
|
||||
}
|
||||
}
|
||||
|
||||
/// Computes the transform from gradient space to layer space (where gradient space is 0..1 in layer space)
|
||||
fn gradient_space_transform(path: &[LayerId], layer: &Layer, document: &DocumentMessageHandler, font_cache: &FontCache) -> DAffine2 {
|
||||
let bounds = layer.aabb_for_transform(DAffine2::IDENTITY, font_cache).unwrap();
|
||||
let bound_transform = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]);
|
||||
|
||||
let multiplied = document.graphene_document.multiply_transforms(path).unwrap();
|
||||
|
||||
multiplied * bound_transform
|
||||
}
|
||||
|
||||
/// Contains info on the overlays for a single gradient
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct GradientOverlay {
|
||||
pub handles: [Vec<LayerId>; 2],
|
||||
pub line: Vec<LayerId>,
|
||||
path: Vec<LayerId>,
|
||||
transform: DAffine2,
|
||||
gradient: Gradient,
|
||||
}
|
||||
|
||||
impl GradientOverlay {
|
||||
fn generate_overlay_handle(translation: DVec2, responses: &mut VecDeque<Message>, selected: bool) -> Vec<LayerId> {
|
||||
let path = vec![generate_uuid()];
|
||||
|
||||
let size = DVec2::splat(MANIPULATOR_GROUP_MARKER_SIZE);
|
||||
|
||||
let fill = if selected { Fill::solid(COLOR_ACCENT) } else { Fill::solid(Color::WHITE) };
|
||||
|
||||
let operation = Operation::AddEllipse {
|
||||
path: path.clone(),
|
||||
transform: DAffine2::from_scale_angle_translation(size, 0., translation - size / 2.).to_cols_array(),
|
||||
style: PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 1.0)), fill),
|
||||
insert_index: -1,
|
||||
};
|
||||
responses.push_back(DocumentMessage::Overlays(operation.into()).into());
|
||||
|
||||
path
|
||||
}
|
||||
fn generate_overlay_line(start: DVec2, end: DVec2, responses: &mut VecDeque<Message>) -> Vec<LayerId> {
|
||||
let path = vec![generate_uuid()];
|
||||
|
||||
let line_vector = end - start;
|
||||
let scale = DVec2::splat(line_vector.length());
|
||||
let angle = -line_vector.angle_between(DVec2::X);
|
||||
let translation = start;
|
||||
let transform = DAffine2::from_scale_angle_translation(scale, angle, translation).to_cols_array();
|
||||
|
||||
let operation = Operation::AddLine {
|
||||
path: path.clone(),
|
||||
transform,
|
||||
style: PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 1.0)), Fill::None),
|
||||
insert_index: -1,
|
||||
};
|
||||
responses.push_back(DocumentMessage::Overlays(operation.into()).into());
|
||||
|
||||
path
|
||||
}
|
||||
|
||||
pub fn new(fill: &Gradient, dragging_start: Option<bool>, path: &[LayerId], layer: &Layer, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>, font_cache: &FontCache) -> Self {
|
||||
let transform = gradient_space_transform(path, layer, document, font_cache);
|
||||
let Gradient { start, end, .. } = fill;
|
||||
let [start, end] = [transform.transform_point2(*start), transform.transform_point2(*end)];
|
||||
|
||||
let line = Self::generate_overlay_line(start, end, responses);
|
||||
let handles = [
|
||||
Self::generate_overlay_handle(start, responses, dragging_start == Some(true)),
|
||||
Self::generate_overlay_handle(end, responses, dragging_start == Some(false)),
|
||||
];
|
||||
|
||||
let path = path.to_vec();
|
||||
let gradient = fill.clone();
|
||||
|
||||
Self {
|
||||
handles,
|
||||
line,
|
||||
path,
|
||||
transform,
|
||||
gradient,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delete_overlays(self, responses: &mut VecDeque<Message>) {
|
||||
responses.push_back(DocumentMessage::Overlays(Operation::DeleteLayer { path: self.line }.into()).into());
|
||||
let [start, end] = self.handles;
|
||||
responses.push_back(DocumentMessage::Overlays(Operation::DeleteLayer { path: start }.into()).into());
|
||||
responses.push_back(DocumentMessage::Overlays(Operation::DeleteLayer { path: end }.into()).into());
|
||||
}
|
||||
|
||||
pub fn evaluate_gradient_start(&self) -> DVec2 {
|
||||
self.transform.transform_point2(self.gradient.start)
|
||||
}
|
||||
|
||||
pub fn evaluate_gradient_end(&self) -> DVec2 {
|
||||
self.transform.transform_point2(self.gradient.end)
|
||||
}
|
||||
}
|
||||
|
||||
/// Contains information about the selected gradient handle
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct SelectedGradient {
|
||||
path: Vec<LayerId>,
|
||||
transform: DAffine2,
|
||||
gradient: Gradient,
|
||||
dragging_start: bool,
|
||||
}
|
||||
|
||||
impl SelectedGradient {
|
||||
pub fn new(gradient: Gradient, path: &[LayerId], layer: &Layer, document: &DocumentMessageHandler, font_cache: &FontCache) -> Self {
|
||||
let transform = gradient_space_transform(path, layer, document, font_cache);
|
||||
Self {
|
||||
path: path.to_vec(),
|
||||
transform,
|
||||
gradient,
|
||||
dragging_start: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_gradient_start(mut self, start: DVec2) -> Self {
|
||||
self.gradient.start = self.transform.inverse().transform_point2(start);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn update_gradient(&mut self, mut mouse: DVec2, responses: &mut VecDeque<Message>, snap_rotate: bool, gradient_type: GradientType) {
|
||||
self.gradient.gradient_type = gradient_type;
|
||||
|
||||
if snap_rotate {
|
||||
let point = if self.dragging_start {
|
||||
self.transform.transform_point2(self.gradient.end)
|
||||
} else {
|
||||
self.transform.transform_point2(self.gradient.start)
|
||||
};
|
||||
|
||||
let delta = point - mouse;
|
||||
|
||||
let length = delta.length();
|
||||
let mut angle = -delta.angle_between(DVec2::X);
|
||||
|
||||
let snap_resolution = LINE_ROTATE_SNAP_ANGLE.to_radians();
|
||||
angle = (angle / snap_resolution).round() * snap_resolution;
|
||||
|
||||
let rotated = DVec2::new(length * angle.cos(), length * angle.sin());
|
||||
mouse = point - rotated;
|
||||
}
|
||||
|
||||
mouse = self.transform.inverse().transform_point2(mouse);
|
||||
|
||||
if self.dragging_start {
|
||||
self.gradient.start = mouse;
|
||||
} else {
|
||||
self.gradient.end = mouse;
|
||||
}
|
||||
|
||||
self.gradient.transform = self.transform;
|
||||
let fill = Fill::Gradient(self.gradient.clone());
|
||||
let path = self.path.clone();
|
||||
responses.push_back(Operation::SetLayerFill { path, fill }.into());
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolTransition for GradientTool {
|
||||
fn event_to_message_map(&self) -> EventToMessageMap {
|
||||
EventToMessageMap {
|
||||
document_dirty: Some(GradientToolMessage::DocumentIsDirty.into()),
|
||||
tool_abort: Some(GradientToolMessage::Abort.into()),
|
||||
selection_changed: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct GradientToolData {
|
||||
gradient_overlays: Vec<GradientOverlay>,
|
||||
selected_gradient: Option<SelectedGradient>,
|
||||
snap_manager: SnapManager,
|
||||
}
|
||||
|
||||
pub fn start_snap(snap_manager: &mut SnapManager, document: &DocumentMessageHandler, font_cache: &FontCache) {
|
||||
snap_manager.start_snap(document, document.bounding_boxes(None, None, font_cache), true, true);
|
||||
snap_manager.add_all_document_handles(document, &[], &[], &[]);
|
||||
}
|
||||
|
||||
impl Fsm for GradientToolFsmState {
|
||||
type ToolData = GradientToolData;
|
||||
type ToolOptions = GradientOptions;
|
||||
|
||||
fn transition(
|
||||
self,
|
||||
event: ToolMessage,
|
||||
tool_data: &mut Self::ToolData,
|
||||
(document, global_tool_data, input, font_cache): ToolActionHandlerData,
|
||||
tool_options: &Self::ToolOptions,
|
||||
responses: &mut VecDeque<Message>,
|
||||
) -> Self {
|
||||
if let ToolMessage::Gradient(event) = event {
|
||||
match (self, event) {
|
||||
(_, GradientToolMessage::DocumentIsDirty) => {
|
||||
while let Some(overlay) = tool_data.gradient_overlays.pop() {
|
||||
overlay.delete_overlays(responses);
|
||||
}
|
||||
|
||||
for path in document.selected_visible_layers() {
|
||||
let layer = document.graphene_document.layer(path).unwrap();
|
||||
|
||||
if let Ok(Fill::Gradient(gradient)) = layer.style().map(|style| style.fill()) {
|
||||
let dragging_start = tool_data
|
||||
.selected_gradient
|
||||
.as_ref()
|
||||
.and_then(|selected| if selected.path == path { Some(selected.dragging_start) } else { None });
|
||||
tool_data
|
||||
.gradient_overlays
|
||||
.push(GradientOverlay::new(gradient, dragging_start, path, layer, document, responses, font_cache))
|
||||
}
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
(GradientToolFsmState::Ready, GradientToolMessage::PointerDown) => {
|
||||
responses.push_back(BroadcastEvent::DocumentIsDirty.into());
|
||||
|
||||
let mouse = input.mouse.position;
|
||||
let tolerance = MANIPULATOR_GROUP_MARKER_SIZE.powi(2);
|
||||
|
||||
let mut dragging = false;
|
||||
for overlay in &tool_data.gradient_overlays {
|
||||
if overlay.evaluate_gradient_start().distance_squared(mouse) < tolerance {
|
||||
dragging = true;
|
||||
start_snap(&mut tool_data.snap_manager, document, font_cache);
|
||||
tool_data.selected_gradient = Some(SelectedGradient {
|
||||
path: overlay.path.clone(),
|
||||
transform: overlay.transform,
|
||||
gradient: overlay.gradient.clone(),
|
||||
dragging_start: true,
|
||||
})
|
||||
}
|
||||
if overlay.evaluate_gradient_end().distance_squared(mouse) < tolerance {
|
||||
dragging = true;
|
||||
start_snap(&mut tool_data.snap_manager, document, font_cache);
|
||||
tool_data.selected_gradient = Some(SelectedGradient {
|
||||
path: overlay.path.clone(),
|
||||
transform: overlay.transform,
|
||||
gradient: overlay.gradient.clone(),
|
||||
dragging_start: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
if dragging {
|
||||
GradientToolFsmState::Drawing
|
||||
} else {
|
||||
let tolerance = DVec2::splat(SELECTION_TOLERANCE);
|
||||
let quad = Quad::from_box([input.mouse.position - tolerance, input.mouse.position + tolerance]);
|
||||
let intersection = document.graphene_document.intersects_quad_root(quad, font_cache).pop();
|
||||
|
||||
if let Some(intersection) = intersection {
|
||||
if !document.selected_layers_contains(&intersection) {
|
||||
let replacement_selected_layers = vec![intersection.clone()];
|
||||
|
||||
responses.push_back(DocumentMessage::SetSelectedLayers { replacement_selected_layers }.into());
|
||||
}
|
||||
|
||||
let layer = document.graphene_document.layer(&intersection).unwrap();
|
||||
|
||||
let gradient = Gradient::new(
|
||||
DVec2::ZERO,
|
||||
global_tool_data.secondary_color,
|
||||
DVec2::ONE,
|
||||
global_tool_data.primary_color,
|
||||
DAffine2::IDENTITY,
|
||||
generate_uuid(),
|
||||
tool_options.gradient_type,
|
||||
);
|
||||
let mut selected_gradient = SelectedGradient::new(gradient, &intersection, layer, document, font_cache).with_gradient_start(input.mouse.position);
|
||||
selected_gradient.update_gradient(input.mouse.position, responses, false, tool_options.gradient_type);
|
||||
|
||||
tool_data.selected_gradient = Some(selected_gradient);
|
||||
|
||||
start_snap(&mut tool_data.snap_manager, document, font_cache);
|
||||
|
||||
GradientToolFsmState::Drawing
|
||||
} else {
|
||||
GradientToolFsmState::Ready
|
||||
}
|
||||
}
|
||||
}
|
||||
(GradientToolFsmState::Drawing, GradientToolMessage::PointerMove { constrain_axis }) => {
|
||||
if let Some(selected_gradient) = &mut tool_data.selected_gradient {
|
||||
let mouse = tool_data.snap_manager.snap_position(responses, document, input.mouse.position);
|
||||
selected_gradient.update_gradient(mouse, responses, input.keyboard.get(constrain_axis as usize), selected_gradient.gradient.gradient_type);
|
||||
}
|
||||
GradientToolFsmState::Drawing
|
||||
}
|
||||
|
||||
(GradientToolFsmState::Drawing, GradientToolMessage::PointerUp) => {
|
||||
tool_data.snap_manager.cleanup(responses);
|
||||
|
||||
GradientToolFsmState::Ready
|
||||
}
|
||||
|
||||
(_, GradientToolMessage::Abort) => {
|
||||
tool_data.snap_manager.cleanup(responses);
|
||||
|
||||
while let Some(overlay) = tool_data.gradient_overlays.pop() {
|
||||
overlay.delete_overlays(responses);
|
||||
}
|
||||
GradientToolFsmState::Ready
|
||||
}
|
||||
_ => self,
|
||||
}
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
fn update_hints(&self, responses: &mut VecDeque<Message>) {
|
||||
let hint_data = match self {
|
||||
GradientToolFsmState::Ready => HintData(vec![HintGroup(vec![
|
||||
HintInfo {
|
||||
key_groups: vec![],
|
||||
key_groups_mac: None,
|
||||
mouse: Some(MouseMotion::LmbDrag),
|
||||
label: String::from("Draw Gradient"),
|
||||
plus: false,
|
||||
},
|
||||
HintInfo {
|
||||
key_groups: vec![KeysGroup(vec![Key::KeyShift])],
|
||||
key_groups_mac: None,
|
||||
mouse: None,
|
||||
label: String::from("Snap 15°"),
|
||||
plus: true,
|
||||
},
|
||||
])]),
|
||||
GradientToolFsmState::Drawing => HintData(vec![HintGroup(vec![HintInfo {
|
||||
key_groups: vec![KeysGroup(vec![Key::KeyShift])],
|
||||
key_groups_mac: None,
|
||||
mouse: None,
|
||||
label: String::from("Snap 15°"),
|
||||
plus: false,
|
||||
}])]),
|
||||
};
|
||||
|
||||
responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into());
|
||||
}
|
||||
|
||||
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
|
||||
responses.push_back(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }.into());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,340 @@
|
||||
use crate::consts::{DRAG_THRESHOLD, LINE_ROTATE_SNAP_ANGLE};
|
||||
use crate::messages::frontend::utility_types::MouseCursorIcon;
|
||||
use crate::messages::input_mapper::utility_types::input_keyboard::{Key, KeysGroup, MouseMotion};
|
||||
use crate::messages::input_mapper::utility_types::input_mouse::ViewportPosition;
|
||||
use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, PropertyHolder, Widget, WidgetCallback, WidgetHolder, WidgetLayout};
|
||||
use crate::messages::layout::utility_types::widgets::input_widgets::NumberInput;
|
||||
use crate::messages::prelude::*;
|
||||
use crate::messages::tool::common_functionality::snapping::SnapManager;
|
||||
use crate::messages::tool::utility_types::{EventToMessageMap, Fsm, ToolActionHandlerData, ToolMetadata, ToolTransition, ToolType};
|
||||
use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo};
|
||||
|
||||
use graphene::layers::style;
|
||||
use graphene::LayerId;
|
||||
use graphene::Operation;
|
||||
|
||||
use glam::{DAffine2, DVec2};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct LineTool {
|
||||
fsm_state: LineToolFsmState,
|
||||
tool_data: LineToolData,
|
||||
options: LineOptions,
|
||||
}
|
||||
|
||||
pub struct LineOptions {
|
||||
line_weight: f64,
|
||||
}
|
||||
|
||||
impl Default for LineOptions {
|
||||
fn default() -> Self {
|
||||
Self { line_weight: 5. }
|
||||
}
|
||||
}
|
||||
|
||||
#[remain::sorted]
|
||||
#[impl_message(Message, ToolMessage, Line)]
|
||||
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum LineToolMessage {
|
||||
// Standard messages
|
||||
#[remain::unsorted]
|
||||
Abort,
|
||||
|
||||
// Tool-specific messages
|
||||
DragStart,
|
||||
DragStop,
|
||||
Redraw {
|
||||
center: Key,
|
||||
lock_angle: Key,
|
||||
snap_angle: Key,
|
||||
},
|
||||
UpdateOptions(LineOptionsUpdate),
|
||||
}
|
||||
|
||||
#[remain::sorted]
|
||||
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum LineOptionsUpdate {
|
||||
LineWeight(f64),
|
||||
}
|
||||
|
||||
impl ToolMetadata for LineTool {
|
||||
fn icon_name(&self) -> String {
|
||||
"VectorLineTool".into()
|
||||
}
|
||||
fn tooltip(&self) -> String {
|
||||
"Line Tool".into()
|
||||
}
|
||||
fn tool_type(&self) -> crate::messages::tool::utility_types::ToolType {
|
||||
ToolType::Line
|
||||
}
|
||||
}
|
||||
|
||||
impl PropertyHolder for LineTool {
|
||||
fn properties(&self) -> Layout {
|
||||
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row {
|
||||
widgets: vec![WidgetHolder::new(Widget::NumberInput(NumberInput {
|
||||
unit: " px".into(),
|
||||
label: "Weight".into(),
|
||||
value: Some(self.options.line_weight as f64),
|
||||
is_integer: false,
|
||||
min: Some(0.),
|
||||
on_update: WidgetCallback::new(|number_input: &NumberInput| LineToolMessage::UpdateOptions(LineOptionsUpdate::LineWeight(number_input.value.unwrap())).into()),
|
||||
..NumberInput::default()
|
||||
}))],
|
||||
}]))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for LineTool {
|
||||
fn process_message(&mut self, message: ToolMessage, tool_data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
|
||||
if message == ToolMessage::UpdateHints {
|
||||
self.fsm_state.update_hints(responses);
|
||||
return;
|
||||
}
|
||||
|
||||
if message == ToolMessage::UpdateCursor {
|
||||
self.fsm_state.update_cursor(responses);
|
||||
return;
|
||||
}
|
||||
|
||||
if let ToolMessage::Line(LineToolMessage::UpdateOptions(action)) = message {
|
||||
match action {
|
||||
LineOptionsUpdate::LineWeight(line_weight) => self.options.line_weight = line_weight,
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let new_state = self.fsm_state.transition(message, &mut self.tool_data, tool_data, &self.options, responses);
|
||||
|
||||
if self.fsm_state != new_state {
|
||||
self.fsm_state = new_state;
|
||||
self.fsm_state.update_hints(responses);
|
||||
self.fsm_state.update_cursor(responses);
|
||||
}
|
||||
}
|
||||
|
||||
fn actions(&self) -> ActionList {
|
||||
use LineToolFsmState::*;
|
||||
|
||||
match self.fsm_state {
|
||||
Ready => actions!(LineToolMessageDiscriminant;
|
||||
DragStart,
|
||||
),
|
||||
Drawing => actions!(LineToolMessageDiscriminant;
|
||||
DragStop,
|
||||
Redraw,
|
||||
Abort,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolTransition for LineTool {
|
||||
fn event_to_message_map(&self) -> EventToMessageMap {
|
||||
EventToMessageMap {
|
||||
document_dirty: None,
|
||||
tool_abort: Some(LineToolMessage::Abort.into()),
|
||||
selection_changed: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum LineToolFsmState {
|
||||
Ready,
|
||||
Drawing,
|
||||
}
|
||||
|
||||
impl Default for LineToolFsmState {
|
||||
fn default() -> Self {
|
||||
LineToolFsmState::Ready
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct LineToolData {
|
||||
drag_start: ViewportPosition,
|
||||
drag_current: ViewportPosition,
|
||||
angle: f64,
|
||||
weight: f64,
|
||||
path: Option<Vec<LayerId>>,
|
||||
snap_manager: SnapManager,
|
||||
}
|
||||
|
||||
impl Fsm for LineToolFsmState {
|
||||
type ToolData = LineToolData;
|
||||
type ToolOptions = LineOptions;
|
||||
|
||||
fn transition(
|
||||
self,
|
||||
event: ToolMessage,
|
||||
tool_data: &mut Self::ToolData,
|
||||
(document, global_tool_data, input, font_cache): ToolActionHandlerData,
|
||||
tool_options: &Self::ToolOptions,
|
||||
responses: &mut VecDeque<Message>,
|
||||
) -> Self {
|
||||
use LineToolFsmState::*;
|
||||
use LineToolMessage::*;
|
||||
|
||||
if let ToolMessage::Line(event) = event {
|
||||
match (self, event) {
|
||||
(Ready, DragStart) => {
|
||||
tool_data.snap_manager.start_snap(document, document.bounding_boxes(None, None, font_cache), true, true);
|
||||
tool_data.snap_manager.add_all_document_handles(document, &[], &[], &[]);
|
||||
tool_data.drag_start = tool_data.snap_manager.snap_position(responses, document, input.mouse.position);
|
||||
|
||||
responses.push_back(DocumentMessage::StartTransaction.into());
|
||||
tool_data.path = Some(document.get_path_for_new_layer());
|
||||
responses.push_back(DocumentMessage::DeselectAllLayers.into());
|
||||
|
||||
tool_data.weight = tool_options.line_weight;
|
||||
|
||||
responses.push_back(
|
||||
Operation::AddLine {
|
||||
path: tool_data.path.clone().unwrap(),
|
||||
insert_index: -1,
|
||||
transform: DAffine2::ZERO.to_cols_array(),
|
||||
style: style::PathStyle::new(Some(style::Stroke::new(global_tool_data.primary_color, tool_data.weight)), style::Fill::None),
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
|
||||
Drawing
|
||||
}
|
||||
(Drawing, Redraw { center, snap_angle, lock_angle }) => {
|
||||
tool_data.drag_current = tool_data.snap_manager.snap_position(responses, document, input.mouse.position);
|
||||
|
||||
let values: Vec<_> = [lock_angle, snap_angle, center].iter().map(|k| input.keyboard.get(*k as usize)).collect();
|
||||
responses.push_back(generate_transform(tool_data, values[0], values[1], values[2]));
|
||||
|
||||
Drawing
|
||||
}
|
||||
(Drawing, DragStop) => {
|
||||
tool_data.drag_current = tool_data.snap_manager.snap_position(responses, document, input.mouse.position);
|
||||
tool_data.snap_manager.cleanup(responses);
|
||||
|
||||
match tool_data.drag_start.distance(input.mouse.position) <= DRAG_THRESHOLD {
|
||||
true => responses.push_back(DocumentMessage::AbortTransaction.into()),
|
||||
false => responses.push_back(DocumentMessage::CommitTransaction.into()),
|
||||
}
|
||||
|
||||
tool_data.path = None;
|
||||
|
||||
Ready
|
||||
}
|
||||
(Drawing, Abort) => {
|
||||
tool_data.snap_manager.cleanup(responses);
|
||||
responses.push_back(DocumentMessage::AbortTransaction.into());
|
||||
tool_data.path = None;
|
||||
Ready
|
||||
}
|
||||
_ => self,
|
||||
}
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
fn update_hints(&self, responses: &mut VecDeque<Message>) {
|
||||
let hint_data = match self {
|
||||
LineToolFsmState::Ready => HintData(vec![HintGroup(vec![
|
||||
HintInfo {
|
||||
key_groups: vec![],
|
||||
key_groups_mac: None,
|
||||
mouse: Some(MouseMotion::LmbDrag),
|
||||
label: String::from("Draw Line"),
|
||||
plus: false,
|
||||
},
|
||||
HintInfo {
|
||||
key_groups: vec![KeysGroup(vec![Key::KeyShift])],
|
||||
key_groups_mac: None,
|
||||
mouse: None,
|
||||
label: String::from("Snap 15°"),
|
||||
plus: true,
|
||||
},
|
||||
HintInfo {
|
||||
key_groups: vec![KeysGroup(vec![Key::KeyAlt])],
|
||||
key_groups_mac: None,
|
||||
mouse: None,
|
||||
label: String::from("From Center"),
|
||||
plus: true,
|
||||
},
|
||||
HintInfo {
|
||||
key_groups: vec![KeysGroup(vec![Key::KeyControl])],
|
||||
key_groups_mac: None,
|
||||
mouse: None,
|
||||
label: String::from("Lock Angle"),
|
||||
plus: true,
|
||||
},
|
||||
])]),
|
||||
LineToolFsmState::Drawing => HintData(vec![HintGroup(vec![
|
||||
HintInfo {
|
||||
key_groups: vec![KeysGroup(vec![Key::KeyShift])],
|
||||
key_groups_mac: None,
|
||||
mouse: None,
|
||||
label: String::from("Snap 15°"),
|
||||
plus: false,
|
||||
},
|
||||
HintInfo {
|
||||
key_groups: vec![KeysGroup(vec![Key::KeyAlt])],
|
||||
key_groups_mac: None,
|
||||
mouse: None,
|
||||
label: String::from("From Center"),
|
||||
plus: false,
|
||||
},
|
||||
HintInfo {
|
||||
key_groups: vec![KeysGroup(vec![Key::KeyControl])],
|
||||
key_groups_mac: None,
|
||||
mouse: None,
|
||||
label: String::from("Lock Angle"),
|
||||
plus: false,
|
||||
},
|
||||
])]),
|
||||
};
|
||||
|
||||
responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into());
|
||||
}
|
||||
|
||||
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
|
||||
responses.push_back(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Crosshair }.into());
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_transform(tool_data: &mut LineToolData, lock: bool, snap: bool, center: bool) -> Message {
|
||||
let mut start = tool_data.drag_start;
|
||||
let stop = tool_data.drag_current;
|
||||
|
||||
let dir = stop - start;
|
||||
|
||||
let mut angle = -dir.angle_between(DVec2::X);
|
||||
|
||||
if lock {
|
||||
angle = tool_data.angle
|
||||
};
|
||||
|
||||
if snap {
|
||||
let snap_resolution = LINE_ROTATE_SNAP_ANGLE.to_radians();
|
||||
angle = (angle / snap_resolution).round() * snap_resolution;
|
||||
}
|
||||
|
||||
tool_data.angle = angle;
|
||||
|
||||
let mut scale = dir.length();
|
||||
|
||||
if lock {
|
||||
let angle_vec = DVec2::new(angle.cos(), angle.sin());
|
||||
scale = dir.dot(angle_vec);
|
||||
}
|
||||
|
||||
if center {
|
||||
start -= scale * DVec2::new(angle.cos(), angle.sin());
|
||||
scale *= 2.;
|
||||
}
|
||||
|
||||
Operation::SetLayerTransformInViewport {
|
||||
path: tool_data.path.clone().unwrap(),
|
||||
transform: glam::DAffine2::from_scale_angle_translation(DVec2::new(scale, 1.), angle, start).to_cols_array(),
|
||||
}
|
||||
.into()
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
pub mod artboard_tool;
|
||||
pub mod ellipse_tool;
|
||||
pub mod eyedropper_tool;
|
||||
pub mod fill_tool;
|
||||
pub mod freehand_tool;
|
||||
pub mod gradient_tool;
|
||||
pub mod line_tool;
|
||||
pub mod navigate_tool;
|
||||
pub mod path_tool;
|
||||
pub mod pen_tool;
|
||||
pub mod rectangle_tool;
|
||||
pub mod select_tool;
|
||||
pub mod shape_tool;
|
||||
pub mod spline_tool;
|
||||
pub mod text_tool;
|
||||
@@ -0,0 +1,281 @@
|
||||
use crate::messages::frontend::utility_types::MouseCursorIcon;
|
||||
use crate::messages::input_mapper::utility_types::input_keyboard::{Key, KeysGroup, MouseMotion};
|
||||
use crate::messages::layout::utility_types::layout_widget::PropertyHolder;
|
||||
use crate::messages::prelude::*;
|
||||
use crate::messages::tool::utility_types::{EventToMessageMap, Fsm, ToolActionHandlerData, ToolMetadata, ToolTransition, ToolType};
|
||||
use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo};
|
||||
|
||||
use glam::DVec2;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct NavigateTool {
|
||||
fsm_state: NavigateToolFsmState,
|
||||
tool_data: NavigateToolData,
|
||||
}
|
||||
|
||||
#[remain::sorted]
|
||||
#[impl_message(Message, ToolMessage, Navigate)]
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Hash, Serialize, Deserialize)]
|
||||
pub enum NavigateToolMessage {
|
||||
// Standard messages
|
||||
#[remain::unsorted]
|
||||
Abort,
|
||||
|
||||
// Tool-specific messages
|
||||
ClickZoom {
|
||||
zoom_in: bool,
|
||||
},
|
||||
PointerMove {
|
||||
snap_angle: Key,
|
||||
snap_zoom: Key,
|
||||
},
|
||||
RotateCanvasBegin,
|
||||
TransformCanvasEnd,
|
||||
TranslateCanvasBegin,
|
||||
ZoomCanvasBegin,
|
||||
}
|
||||
|
||||
impl ToolMetadata for NavigateTool {
|
||||
fn icon_name(&self) -> String {
|
||||
"GeneralNavigateTool".into()
|
||||
}
|
||||
fn tooltip(&self) -> String {
|
||||
"Navigate Tool".into()
|
||||
}
|
||||
fn tool_type(&self) -> crate::messages::tool::utility_types::ToolType {
|
||||
ToolType::Navigate
|
||||
}
|
||||
}
|
||||
|
||||
impl PropertyHolder for NavigateTool {}
|
||||
|
||||
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for NavigateTool {
|
||||
fn process_message(&mut self, message: ToolMessage, tool_data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
|
||||
if message == ToolMessage::UpdateHints {
|
||||
self.fsm_state.update_hints(responses);
|
||||
return;
|
||||
}
|
||||
|
||||
if message == ToolMessage::UpdateCursor {
|
||||
self.fsm_state.update_cursor(responses);
|
||||
return;
|
||||
}
|
||||
|
||||
let new_state = self.fsm_state.transition(message, &mut self.tool_data, tool_data, &(), responses);
|
||||
|
||||
if self.fsm_state != new_state {
|
||||
self.fsm_state = new_state;
|
||||
self.fsm_state.update_hints(responses);
|
||||
self.fsm_state.update_cursor(responses);
|
||||
}
|
||||
}
|
||||
|
||||
fn actions(&self) -> ActionList {
|
||||
use NavigateToolFsmState::*;
|
||||
|
||||
match self.fsm_state {
|
||||
Ready => actions!(NavigateToolMessageDiscriminant;
|
||||
TranslateCanvasBegin,
|
||||
RotateCanvasBegin,
|
||||
ZoomCanvasBegin,
|
||||
),
|
||||
_ => actions!(NavigateToolMessageDiscriminant;
|
||||
ClickZoom,
|
||||
PointerMove,
|
||||
TransformCanvasEnd,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolTransition for NavigateTool {
|
||||
fn event_to_message_map(&self) -> EventToMessageMap {
|
||||
EventToMessageMap {
|
||||
document_dirty: None,
|
||||
tool_abort: Some(NavigateToolMessage::Abort.into()),
|
||||
selection_changed: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum NavigateToolFsmState {
|
||||
Ready,
|
||||
Panning,
|
||||
Tilting,
|
||||
Zooming,
|
||||
}
|
||||
|
||||
impl Default for NavigateToolFsmState {
|
||||
fn default() -> Self {
|
||||
NavigateToolFsmState::Ready
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct NavigateToolData {
|
||||
drag_start: DVec2,
|
||||
}
|
||||
|
||||
impl Fsm for NavigateToolFsmState {
|
||||
type ToolData = NavigateToolData;
|
||||
type ToolOptions = ();
|
||||
|
||||
fn transition(
|
||||
self,
|
||||
message: ToolMessage,
|
||||
tool_data: &mut Self::ToolData,
|
||||
(_document, _global_tool_data, input, _font_cache): ToolActionHandlerData,
|
||||
_tool_options: &Self::ToolOptions,
|
||||
messages: &mut VecDeque<Message>,
|
||||
) -> Self {
|
||||
if let ToolMessage::Navigate(navigate) = message {
|
||||
use NavigateToolMessage::*;
|
||||
|
||||
match navigate {
|
||||
ClickZoom { zoom_in } => {
|
||||
messages.push_front(MovementMessage::TransformCanvasEnd.into());
|
||||
|
||||
// Mouse has not moved from pointerdown to pointerup
|
||||
if tool_data.drag_start == input.mouse.position {
|
||||
messages.push_front(if zoom_in {
|
||||
MovementMessage::IncreaseCanvasZoom { center_on_mouse: true }.into()
|
||||
} else {
|
||||
MovementMessage::DecreaseCanvasZoom { center_on_mouse: true }.into()
|
||||
});
|
||||
}
|
||||
|
||||
NavigateToolFsmState::Ready
|
||||
}
|
||||
PointerMove { snap_angle, snap_zoom } => {
|
||||
messages.push_front(
|
||||
MovementMessage::PointerMove {
|
||||
snap_angle,
|
||||
wait_for_snap_angle_release: false,
|
||||
snap_zoom,
|
||||
zoom_from_viewport: Some(tool_data.drag_start),
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
self
|
||||
}
|
||||
TranslateCanvasBegin => {
|
||||
tool_data.drag_start = input.mouse.position;
|
||||
messages.push_front(MovementMessage::TranslateCanvasBegin.into());
|
||||
NavigateToolFsmState::Panning
|
||||
}
|
||||
RotateCanvasBegin => {
|
||||
tool_data.drag_start = input.mouse.position;
|
||||
messages.push_front(MovementMessage::RotateCanvasBegin.into());
|
||||
NavigateToolFsmState::Tilting
|
||||
}
|
||||
ZoomCanvasBegin => {
|
||||
tool_data.drag_start = input.mouse.position;
|
||||
messages.push_front(MovementMessage::ZoomCanvasBegin.into());
|
||||
NavigateToolFsmState::Zooming
|
||||
}
|
||||
TransformCanvasEnd => {
|
||||
messages.push_front(MovementMessage::TransformCanvasEnd.into());
|
||||
NavigateToolFsmState::Ready
|
||||
}
|
||||
Abort => {
|
||||
messages.push_front(MovementMessage::TransformCanvasEnd.into());
|
||||
NavigateToolFsmState::Ready
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
fn update_hints(&self, responses: &mut VecDeque<Message>) {
|
||||
let hint_data = match self {
|
||||
NavigateToolFsmState::Ready => HintData(vec![
|
||||
HintGroup(vec![
|
||||
HintInfo {
|
||||
key_groups: vec![],
|
||||
key_groups_mac: None,
|
||||
mouse: Some(MouseMotion::Lmb),
|
||||
label: String::from("Zoom In"),
|
||||
plus: false,
|
||||
},
|
||||
HintInfo {
|
||||
key_groups: vec![KeysGroup(vec![Key::KeyShift])],
|
||||
key_groups_mac: None,
|
||||
mouse: None,
|
||||
label: String::from("Zoom Out"),
|
||||
plus: true,
|
||||
},
|
||||
]),
|
||||
HintGroup(vec![
|
||||
HintInfo {
|
||||
key_groups: vec![],
|
||||
key_groups_mac: None,
|
||||
mouse: Some(MouseMotion::LmbDrag),
|
||||
label: String::from("Zoom"),
|
||||
plus: false,
|
||||
},
|
||||
HintInfo {
|
||||
key_groups: vec![KeysGroup(vec![Key::KeyControl])],
|
||||
key_groups_mac: None,
|
||||
mouse: None,
|
||||
label: String::from("Snap Increments"),
|
||||
plus: true,
|
||||
},
|
||||
]),
|
||||
HintGroup(vec![HintInfo {
|
||||
key_groups: vec![],
|
||||
key_groups_mac: None,
|
||||
mouse: Some(MouseMotion::MmbDrag),
|
||||
label: String::from("Pan"),
|
||||
plus: false,
|
||||
}]),
|
||||
HintGroup(vec![
|
||||
HintInfo {
|
||||
key_groups: vec![],
|
||||
key_groups_mac: None,
|
||||
mouse: Some(MouseMotion::RmbDrag),
|
||||
label: String::from("Tilt"),
|
||||
plus: false,
|
||||
},
|
||||
HintInfo {
|
||||
key_groups: vec![KeysGroup(vec![Key::KeyControl])],
|
||||
key_groups_mac: None,
|
||||
mouse: None,
|
||||
label: String::from("Snap 15°"),
|
||||
plus: true,
|
||||
},
|
||||
]),
|
||||
]),
|
||||
NavigateToolFsmState::Tilting => HintData(vec![HintGroup(vec![HintInfo {
|
||||
key_groups: vec![KeysGroup(vec![Key::KeyControl])],
|
||||
key_groups_mac: None,
|
||||
mouse: None,
|
||||
label: String::from("Snap 15°"),
|
||||
plus: false,
|
||||
}])]),
|
||||
NavigateToolFsmState::Zooming => HintData(vec![HintGroup(vec![HintInfo {
|
||||
key_groups: vec![KeysGroup(vec![Key::KeyControl])],
|
||||
key_groups_mac: None,
|
||||
mouse: None,
|
||||
label: String::from("Snap Increments"),
|
||||
plus: false,
|
||||
}])]),
|
||||
_ => HintData(Vec::new()),
|
||||
};
|
||||
|
||||
responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into());
|
||||
}
|
||||
|
||||
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
|
||||
let cursor = match *self {
|
||||
NavigateToolFsmState::Ready => MouseCursorIcon::ZoomIn,
|
||||
NavigateToolFsmState::Panning => MouseCursorIcon::Grabbing,
|
||||
NavigateToolFsmState::Tilting => MouseCursorIcon::Default,
|
||||
NavigateToolFsmState::Zooming => MouseCursorIcon::ZoomIn,
|
||||
};
|
||||
|
||||
responses.push_back(FrontendMessage::UpdateMouseCursor { cursor }.into());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,398 @@
|
||||
use crate::consts::SELECTION_THRESHOLD;
|
||||
use crate::messages::frontend::utility_types::MouseCursorIcon;
|
||||
use crate::messages::input_mapper::utility_types::input_keyboard::{Key, KeysGroup, MouseMotion};
|
||||
use crate::messages::layout::utility_types::layout_widget::PropertyHolder;
|
||||
use crate::messages::prelude::*;
|
||||
use crate::messages::tool::common_functionality::overlay_renderer::OverlayRenderer;
|
||||
use crate::messages::tool::common_functionality::shape_editor::ShapeEditor;
|
||||
use crate::messages::tool::common_functionality::snapping::SnapManager;
|
||||
use crate::messages::tool::utility_types::{EventToMessageMap, Fsm, ToolActionHandlerData, ToolMetadata, ToolTransition, ToolType};
|
||||
use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo};
|
||||
|
||||
use graphene::intersection::Quad;
|
||||
use graphene::layers::vector::consts::ManipulatorType;
|
||||
|
||||
use glam::DVec2;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct PathTool {
|
||||
fsm_state: PathToolFsmState,
|
||||
tool_data: PathToolData,
|
||||
}
|
||||
|
||||
#[remain::sorted]
|
||||
#[impl_message(Message, ToolMessage, Path)]
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Hash, Serialize, Deserialize)]
|
||||
pub enum PathToolMessage {
|
||||
// Standard messages
|
||||
#[remain::unsorted]
|
||||
Abort,
|
||||
#[remain::unsorted]
|
||||
DocumentIsDirty,
|
||||
#[remain::unsorted]
|
||||
SelectionChanged,
|
||||
|
||||
// Tool-specific messages
|
||||
Delete,
|
||||
DragStart {
|
||||
add_to_selection: Key,
|
||||
},
|
||||
DragStop,
|
||||
PointerMove {
|
||||
alt_mirror_angle: Key,
|
||||
shift_mirror_distance: Key,
|
||||
},
|
||||
}
|
||||
|
||||
impl ToolMetadata for PathTool {
|
||||
fn icon_name(&self) -> String {
|
||||
"VectorPathTool".into()
|
||||
}
|
||||
fn tooltip(&self) -> String {
|
||||
"Path Tool".into()
|
||||
}
|
||||
fn tool_type(&self) -> crate::messages::tool::utility_types::ToolType {
|
||||
ToolType::Path
|
||||
}
|
||||
}
|
||||
|
||||
impl PropertyHolder for PathTool {}
|
||||
|
||||
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for PathTool {
|
||||
fn process_message(&mut self, message: ToolMessage, tool_data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
|
||||
if message == ToolMessage::UpdateHints {
|
||||
self.fsm_state.update_hints(responses);
|
||||
return;
|
||||
}
|
||||
|
||||
if message == ToolMessage::UpdateCursor {
|
||||
self.fsm_state.update_cursor(responses);
|
||||
return;
|
||||
}
|
||||
|
||||
let new_state = self.fsm_state.transition(message, &mut self.tool_data, tool_data, &(), responses);
|
||||
|
||||
if self.fsm_state != new_state {
|
||||
self.fsm_state = new_state;
|
||||
self.fsm_state.update_hints(responses);
|
||||
self.fsm_state.update_cursor(responses);
|
||||
}
|
||||
}
|
||||
|
||||
// Different actions depending on state may be wanted:
|
||||
fn actions(&self) -> ActionList {
|
||||
use PathToolFsmState::*;
|
||||
|
||||
match self.fsm_state {
|
||||
Ready => actions!(PathToolMessageDiscriminant;
|
||||
DragStart,
|
||||
Delete,
|
||||
),
|
||||
Dragging => actions!(PathToolMessageDiscriminant;
|
||||
DragStop,
|
||||
PointerMove,
|
||||
Delete,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolTransition for PathTool {
|
||||
fn event_to_message_map(&self) -> EventToMessageMap {
|
||||
EventToMessageMap {
|
||||
document_dirty: Some(PathToolMessage::DocumentIsDirty.into()),
|
||||
tool_abort: Some(PathToolMessage::Abort.into()),
|
||||
selection_changed: Some(PathToolMessage::SelectionChanged.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum PathToolFsmState {
|
||||
Ready,
|
||||
Dragging,
|
||||
}
|
||||
|
||||
impl Default for PathToolFsmState {
|
||||
fn default() -> Self {
|
||||
PathToolFsmState::Ready
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct PathToolData {
|
||||
shape_editor: ShapeEditor,
|
||||
overlay_renderer: OverlayRenderer,
|
||||
snap_manager: SnapManager,
|
||||
|
||||
drag_start_pos: DVec2,
|
||||
alt_debounce: bool,
|
||||
shift_debounce: bool,
|
||||
}
|
||||
|
||||
impl Fsm for PathToolFsmState {
|
||||
type ToolData = PathToolData;
|
||||
type ToolOptions = ();
|
||||
|
||||
fn transition(
|
||||
self,
|
||||
event: ToolMessage,
|
||||
tool_data: &mut Self::ToolData,
|
||||
(document, _global_tool_data, input, font_cache): ToolActionHandlerData,
|
||||
_tool_options: &Self::ToolOptions,
|
||||
responses: &mut VecDeque<Message>,
|
||||
) -> Self {
|
||||
if let ToolMessage::Path(event) = event {
|
||||
use PathToolFsmState::*;
|
||||
use PathToolMessage::*;
|
||||
|
||||
match (self, event) {
|
||||
(_, SelectionChanged) => {
|
||||
// Set the previously selected layers to invisible
|
||||
for layer_path in document.all_layers() {
|
||||
tool_data.overlay_renderer.layer_overlay_visibility(&document.graphene_document, layer_path.to_vec(), false, responses);
|
||||
}
|
||||
|
||||
// Set the newly targeted layers to visible
|
||||
let layer_paths = document.selected_visible_layers().map(|layer_path| layer_path.to_vec()).collect();
|
||||
tool_data.shape_editor.set_selected_layers(layer_paths);
|
||||
// Render the new overlays
|
||||
for layer_path in tool_data.shape_editor.selected_layers() {
|
||||
tool_data.overlay_renderer.render_subpath_overlays(&document.graphene_document, layer_path.to_vec(), responses);
|
||||
}
|
||||
|
||||
// This can happen in any state (which is why we return self)
|
||||
self
|
||||
}
|
||||
(_, DocumentIsDirty) => {
|
||||
// When the document has moved / needs to be redraw, re-render the overlays
|
||||
// TODO the overlay system should probably receive this message instead of the tool
|
||||
for layer_path in document.selected_visible_layers() {
|
||||
tool_data.overlay_renderer.render_subpath_overlays(&document.graphene_document, layer_path.to_vec(), responses);
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
// Mouse down
|
||||
(_, DragStart { add_to_selection }) => {
|
||||
let toggle_add_to_selection = input.keyboard.get(add_to_selection as usize);
|
||||
|
||||
// Select the first point within the threshold (in pixels)
|
||||
if let Some(mut new_selected) = tool_data
|
||||
.shape_editor
|
||||
.select_point(&document.graphene_document, input.mouse.position, SELECTION_THRESHOLD, toggle_add_to_selection, responses)
|
||||
{
|
||||
responses.push_back(DocumentMessage::StartTransaction.into());
|
||||
|
||||
let ignore_document = tool_data.shape_editor.selected_layers().clone();
|
||||
tool_data
|
||||
.snap_manager
|
||||
.start_snap(document, document.bounding_boxes(Some(&ignore_document), None, font_cache), true, true);
|
||||
|
||||
// Do not snap against handles when anchor is selected
|
||||
let mut extension = Vec::new();
|
||||
for &(path, id, point_type) in new_selected.iter() {
|
||||
if point_type == ManipulatorType::Anchor {
|
||||
extension.push((path, id, ManipulatorType::InHandle));
|
||||
extension.push((path, id, ManipulatorType::OutHandle));
|
||||
}
|
||||
}
|
||||
new_selected.extend(extension);
|
||||
|
||||
let include_handles = tool_data.shape_editor.selected_layers_ref();
|
||||
tool_data.snap_manager.add_all_document_handles(document, &include_handles, &[], &new_selected);
|
||||
|
||||
tool_data.drag_start_pos = input.mouse.position;
|
||||
Dragging
|
||||
}
|
||||
// We didn't find a point nearby, so consider selecting the nearest shape instead
|
||||
else {
|
||||
let selection_size = DVec2::new(2.0, 2.0);
|
||||
// Select shapes directly under our mouse
|
||||
let intersection = document
|
||||
.graphene_document
|
||||
.intersects_quad_root(Quad::from_box([input.mouse.position - selection_size, input.mouse.position + selection_size]), font_cache);
|
||||
if !intersection.is_empty() {
|
||||
if toggle_add_to_selection {
|
||||
responses.push_back(DocumentMessage::AddSelectedLayers { additional_layers: intersection }.into());
|
||||
} else {
|
||||
responses.push_back(
|
||||
DocumentMessage::SetSelectedLayers {
|
||||
replacement_selected_layers: intersection,
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Clear the previous selection if we didn't find anything
|
||||
if !input.keyboard.get(toggle_add_to_selection as usize) {
|
||||
responses.push_back(DocumentMessage::DeselectAllLayers.into());
|
||||
}
|
||||
}
|
||||
Ready
|
||||
}
|
||||
}
|
||||
// Dragging
|
||||
(
|
||||
Dragging,
|
||||
PointerMove {
|
||||
alt_mirror_angle,
|
||||
shift_mirror_distance,
|
||||
},
|
||||
) => {
|
||||
// Determine when alt state changes
|
||||
let alt_pressed = input.keyboard.get(alt_mirror_angle as usize);
|
||||
if alt_pressed != tool_data.alt_debounce {
|
||||
tool_data.alt_debounce = alt_pressed;
|
||||
// Only on alt down
|
||||
if alt_pressed {
|
||||
tool_data.shape_editor.toggle_handle_mirroring_on_selected(true, false, responses);
|
||||
}
|
||||
}
|
||||
|
||||
// Determine when shift state changes
|
||||
let shift_pressed = input.keyboard.get(shift_mirror_distance as usize);
|
||||
if shift_pressed != tool_data.shift_debounce {
|
||||
tool_data.shift_debounce = shift_pressed;
|
||||
tool_data.shape_editor.toggle_handle_mirroring_on_selected(false, true, responses);
|
||||
}
|
||||
|
||||
// Move the selected points by the mouse position
|
||||
let snapped_position = tool_data.snap_manager.snap_position(responses, document, input.mouse.position);
|
||||
tool_data.shape_editor.move_selected_points(snapped_position - tool_data.drag_start_pos, snapped_position, responses);
|
||||
tool_data.drag_start_pos = snapped_position;
|
||||
Dragging
|
||||
}
|
||||
// Mouse up
|
||||
(_, DragStop) => {
|
||||
tool_data.snap_manager.cleanup(responses);
|
||||
Ready
|
||||
}
|
||||
// Delete key
|
||||
(_, Delete) => {
|
||||
// Delete the selected points and clean up overlays
|
||||
responses.push_back(DocumentMessage::StartTransaction.into());
|
||||
tool_data.shape_editor.delete_selected_points(responses);
|
||||
responses.push_back(SelectionChanged.into());
|
||||
for layer_path in document.all_layers() {
|
||||
tool_data.overlay_renderer.clear_subpath_overlays(&document.graphene_document, layer_path.to_vec(), responses);
|
||||
}
|
||||
Ready
|
||||
}
|
||||
(_, Abort) => {
|
||||
// TODO Tell overlay manager to remove the overlays
|
||||
for layer_path in document.all_layers() {
|
||||
tool_data.overlay_renderer.clear_subpath_overlays(&document.graphene_document, layer_path.to_vec(), responses);
|
||||
}
|
||||
Ready
|
||||
}
|
||||
(
|
||||
_,
|
||||
PointerMove {
|
||||
alt_mirror_angle: _,
|
||||
shift_mirror_distance: _,
|
||||
},
|
||||
) => self,
|
||||
}
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
fn update_hints(&self, responses: &mut VecDeque<Message>) {
|
||||
let hint_data = match self {
|
||||
PathToolFsmState::Ready => HintData(vec![
|
||||
HintGroup(vec![
|
||||
HintInfo {
|
||||
key_groups: vec![],
|
||||
key_groups_mac: None,
|
||||
mouse: Some(MouseMotion::Lmb),
|
||||
label: String::from("Select Point"),
|
||||
plus: false,
|
||||
},
|
||||
HintInfo {
|
||||
key_groups: vec![KeysGroup(vec![Key::KeyShift])],
|
||||
key_groups_mac: None,
|
||||
mouse: None,
|
||||
label: String::from("Grow/Shrink Selection"),
|
||||
plus: true,
|
||||
},
|
||||
]),
|
||||
HintGroup(vec![HintInfo {
|
||||
key_groups: vec![],
|
||||
key_groups_mac: None,
|
||||
mouse: Some(MouseMotion::LmbDrag),
|
||||
label: String::from("Drag Selected"),
|
||||
plus: false,
|
||||
}]),
|
||||
HintGroup(vec![
|
||||
HintInfo {
|
||||
key_groups: vec![
|
||||
KeysGroup(vec![Key::KeyArrowUp]),
|
||||
KeysGroup(vec![Key::KeyArrowRight]),
|
||||
KeysGroup(vec![Key::KeyArrowDown]),
|
||||
KeysGroup(vec![Key::KeyArrowLeft]),
|
||||
],
|
||||
key_groups_mac: None,
|
||||
mouse: None,
|
||||
label: String::from("Nudge Selected (coming soon)"),
|
||||
plus: false,
|
||||
},
|
||||
HintInfo {
|
||||
key_groups: vec![KeysGroup(vec![Key::KeyShift])],
|
||||
key_groups_mac: None,
|
||||
mouse: None,
|
||||
label: String::from("Big Increment Nudge"),
|
||||
plus: true,
|
||||
},
|
||||
]),
|
||||
HintGroup(vec![
|
||||
HintInfo {
|
||||
key_groups: vec![KeysGroup(vec![Key::KeyG])],
|
||||
key_groups_mac: None,
|
||||
mouse: None,
|
||||
label: String::from("Grab Selected (coming soon)"),
|
||||
plus: false,
|
||||
},
|
||||
HintInfo {
|
||||
key_groups: vec![KeysGroup(vec![Key::KeyR])],
|
||||
key_groups_mac: None,
|
||||
mouse: None,
|
||||
label: String::from("Rotate Selected (coming soon)"),
|
||||
plus: false,
|
||||
},
|
||||
HintInfo {
|
||||
key_groups: vec![KeysGroup(vec![Key::KeyS])],
|
||||
key_groups_mac: None,
|
||||
mouse: None,
|
||||
label: String::from("Scale Selected (coming soon)"),
|
||||
plus: false,
|
||||
},
|
||||
]),
|
||||
]),
|
||||
PathToolFsmState::Dragging => HintData(vec![HintGroup(vec![
|
||||
HintInfo {
|
||||
key_groups: vec![KeysGroup(vec![Key::KeyAlt])],
|
||||
key_groups_mac: None,
|
||||
mouse: None,
|
||||
label: String::from("Split/Align Handles (Toggle)"),
|
||||
plus: false,
|
||||
},
|
||||
HintInfo {
|
||||
key_groups: vec![KeysGroup(vec![Key::KeyShift])],
|
||||
key_groups_mac: None,
|
||||
mouse: None,
|
||||
label: String::from("Share Lengths of Aligned Handles"),
|
||||
plus: false,
|
||||
},
|
||||
])]),
|
||||
};
|
||||
|
||||
responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into());
|
||||
}
|
||||
|
||||
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
|
||||
responses.push_back(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }.into());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,478 @@
|
||||
use crate::consts::LINE_ROTATE_SNAP_ANGLE;
|
||||
use crate::messages::frontend::utility_types::MouseCursorIcon;
|
||||
use crate::messages::input_mapper::utility_types::input_keyboard::{Key, KeysGroup, MouseMotion};
|
||||
use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, PropertyHolder, Widget, WidgetCallback, WidgetHolder, WidgetLayout};
|
||||
use crate::messages::layout::utility_types::widgets::input_widgets::NumberInput;
|
||||
use crate::messages::prelude::*;
|
||||
use crate::messages::tool::common_functionality::overlay_renderer::OverlayRenderer;
|
||||
use crate::messages::tool::common_functionality::snapping::SnapManager;
|
||||
use crate::messages::tool::utility_types::{EventToMessageMap, Fsm, ToolActionHandlerData, ToolMetadata, ToolTransition, ToolType};
|
||||
use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo};
|
||||
|
||||
use graphene::layers::style;
|
||||
use graphene::layers::vector::consts::ManipulatorType;
|
||||
use graphene::layers::vector::manipulator_group::ManipulatorGroup;
|
||||
use graphene::layers::vector::subpath::Subpath;
|
||||
use graphene::LayerId;
|
||||
use graphene::Operation;
|
||||
|
||||
use glam::{DAffine2, DVec2};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct PenTool {
|
||||
fsm_state: PenToolFsmState,
|
||||
tool_data: PenToolData,
|
||||
options: PenOptions,
|
||||
}
|
||||
|
||||
pub struct PenOptions {
|
||||
line_weight: f64,
|
||||
}
|
||||
|
||||
impl Default for PenOptions {
|
||||
fn default() -> Self {
|
||||
Self { line_weight: 5. }
|
||||
}
|
||||
}
|
||||
|
||||
#[remain::sorted]
|
||||
#[impl_message(Message, ToolMessage, Pen)]
|
||||
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum PenToolMessage {
|
||||
// Standard messages
|
||||
#[remain::unsorted]
|
||||
DocumentIsDirty,
|
||||
#[remain::unsorted]
|
||||
Abort,
|
||||
#[remain::unsorted]
|
||||
SelectionChanged,
|
||||
|
||||
// Tool-specific messages
|
||||
Confirm,
|
||||
DragStart,
|
||||
DragStop,
|
||||
PointerMove {
|
||||
snap_angle: Key,
|
||||
break_handle: Key,
|
||||
},
|
||||
Undo,
|
||||
UpdateOptions(PenOptionsUpdate),
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum PenToolFsmState {
|
||||
Ready,
|
||||
DraggingHandle,
|
||||
PlacingAnchor,
|
||||
}
|
||||
|
||||
#[remain::sorted]
|
||||
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum PenOptionsUpdate {
|
||||
LineWeight(f64),
|
||||
}
|
||||
|
||||
impl ToolMetadata for PenTool {
|
||||
fn icon_name(&self) -> String {
|
||||
"VectorPenTool".into()
|
||||
}
|
||||
fn tooltip(&self) -> String {
|
||||
"Pen Tool".into()
|
||||
}
|
||||
fn tool_type(&self) -> crate::messages::tool::utility_types::ToolType {
|
||||
ToolType::Pen
|
||||
}
|
||||
}
|
||||
|
||||
impl PropertyHolder for PenTool {
|
||||
fn properties(&self) -> Layout {
|
||||
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row {
|
||||
widgets: vec![WidgetHolder::new(Widget::NumberInput(NumberInput {
|
||||
unit: " px".into(),
|
||||
label: "Weight".into(),
|
||||
value: Some(self.options.line_weight),
|
||||
is_integer: false,
|
||||
min: Some(0.),
|
||||
on_update: WidgetCallback::new(|number_input: &NumberInput| PenToolMessage::UpdateOptions(PenOptionsUpdate::LineWeight(number_input.value.unwrap())).into()),
|
||||
..NumberInput::default()
|
||||
}))],
|
||||
}]))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for PenTool {
|
||||
fn process_message(&mut self, message: ToolMessage, tool_data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
|
||||
if message == ToolMessage::UpdateHints {
|
||||
self.fsm_state.update_hints(responses);
|
||||
return;
|
||||
}
|
||||
|
||||
if message == ToolMessage::UpdateCursor {
|
||||
self.fsm_state.update_cursor(responses);
|
||||
return;
|
||||
}
|
||||
|
||||
if let ToolMessage::Pen(PenToolMessage::UpdateOptions(action)) = message {
|
||||
match action {
|
||||
PenOptionsUpdate::LineWeight(line_weight) => self.options.line_weight = line_weight,
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let new_state = self.fsm_state.transition(message, &mut self.tool_data, tool_data, &self.options, responses);
|
||||
|
||||
if self.fsm_state != new_state {
|
||||
self.fsm_state = new_state;
|
||||
self.fsm_state.update_hints(responses);
|
||||
self.fsm_state.update_cursor(responses);
|
||||
}
|
||||
}
|
||||
|
||||
fn actions(&self) -> ActionList {
|
||||
match self.fsm_state {
|
||||
PenToolFsmState::Ready => actions!(PenToolMessageDiscriminant;
|
||||
Undo,
|
||||
DragStart,
|
||||
DragStop,
|
||||
Confirm,
|
||||
Abort,
|
||||
),
|
||||
PenToolFsmState::DraggingHandle | PenToolFsmState::PlacingAnchor => actions!(PenToolMessageDiscriminant;
|
||||
DragStart,
|
||||
DragStop,
|
||||
PointerMove,
|
||||
Confirm,
|
||||
Abort,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolTransition for PenTool {
|
||||
fn event_to_message_map(&self) -> EventToMessageMap {
|
||||
EventToMessageMap {
|
||||
document_dirty: Some(PenToolMessage::DocumentIsDirty.into()),
|
||||
tool_abort: Some(PenToolMessage::Abort.into()),
|
||||
selection_changed: Some(PenToolMessage::SelectionChanged.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PenToolFsmState {
|
||||
fn default() -> Self {
|
||||
PenToolFsmState::Ready
|
||||
}
|
||||
}
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct PenToolData {
|
||||
weight: f64,
|
||||
path: Option<Vec<LayerId>>,
|
||||
overlay_renderer: OverlayRenderer,
|
||||
snap_manager: SnapManager,
|
||||
}
|
||||
|
||||
impl Fsm for PenToolFsmState {
|
||||
type ToolData = PenToolData;
|
||||
type ToolOptions = PenOptions;
|
||||
|
||||
fn transition(
|
||||
self,
|
||||
event: ToolMessage,
|
||||
tool_data: &mut Self::ToolData,
|
||||
(document, global_tool_data, input, font_cache): ToolActionHandlerData,
|
||||
tool_options: &Self::ToolOptions,
|
||||
responses: &mut VecDeque<Message>,
|
||||
) -> Self {
|
||||
let transform = tool_data.path.as_ref().and_then(|path| document.graphene_document.multiply_transforms(path).ok()).unwrap_or_default();
|
||||
|
||||
if let ToolMessage::Pen(event) = event {
|
||||
match (self, event) {
|
||||
(_, PenToolMessage::DocumentIsDirty) => {
|
||||
// When the document has moved / needs to be redraw, re-render the overlays
|
||||
// TODO the overlay system should probably receive this message instead of the tool
|
||||
for layer_path in document.selected_visible_layers() {
|
||||
tool_data.overlay_renderer.render_subpath_overlays(&document.graphene_document, layer_path.to_vec(), responses);
|
||||
}
|
||||
self
|
||||
}
|
||||
(_, PenToolMessage::SelectionChanged) => {
|
||||
// Set the previously selected layers to invisible
|
||||
for layer_path in document.all_layers() {
|
||||
tool_data.overlay_renderer.layer_overlay_visibility(&document.graphene_document, layer_path.to_vec(), false, responses);
|
||||
}
|
||||
self
|
||||
}
|
||||
(PenToolFsmState::Ready, PenToolMessage::DragStart) => {
|
||||
responses.push_back(DocumentMessage::StartTransaction.into());
|
||||
responses.push_back(DocumentMessage::DeselectAllLayers.into());
|
||||
|
||||
// Create a new layer and prep snap system
|
||||
tool_data.path = Some(document.get_path_for_new_layer());
|
||||
tool_data.snap_manager.start_snap(document, document.bounding_boxes(None, None, font_cache), true, true);
|
||||
tool_data.snap_manager.add_all_document_handles(document, &[], &[], &[]);
|
||||
let snapped_position = tool_data.snap_manager.snap_position(responses, document, input.mouse.position);
|
||||
|
||||
// Get the position and set properties
|
||||
let transform = tool_data
|
||||
.path
|
||||
.as_ref()
|
||||
.and_then(|path| document.graphene_document.multiply_transforms(&path[..path.len() - 1]).ok())
|
||||
.unwrap_or_default();
|
||||
let start_position = transform.inverse().transform_point2(snapped_position);
|
||||
tool_data.weight = tool_options.line_weight;
|
||||
|
||||
// Create the initial shape with a `bez_path` (only contains a moveto initially)
|
||||
if let Some(layer_path) = &tool_data.path {
|
||||
responses.push_back(
|
||||
Operation::AddShape {
|
||||
path: layer_path.clone(),
|
||||
transform: DAffine2::IDENTITY.to_cols_array(),
|
||||
insert_index: -1,
|
||||
subpath: Default::default(),
|
||||
style: style::PathStyle::new(Some(style::Stroke::new(global_tool_data.primary_color, tool_data.weight)), style::Fill::None),
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
responses.push_back(add_manipulator_group(
|
||||
&tool_data.path,
|
||||
ManipulatorGroup::new_with_handles(start_position, Some(start_position), Some(start_position)),
|
||||
));
|
||||
}
|
||||
|
||||
PenToolFsmState::DraggingHandle
|
||||
}
|
||||
(PenToolFsmState::PlacingAnchor, PenToolMessage::DragStart) => PenToolFsmState::DraggingHandle,
|
||||
(PenToolFsmState::DraggingHandle, PenToolMessage::DragStop) => {
|
||||
// Add new point onto path
|
||||
if let Some(layer_path) = &tool_data.path {
|
||||
if let Some(manipulator_group) = get_subpath(layer_path, document).and_then(|subpath| subpath.manipulator_groups().last()) {
|
||||
if let Some(out_handle) = &manipulator_group.points[ManipulatorType::OutHandle] {
|
||||
responses.push_back(add_manipulator_group(&tool_data.path, ManipulatorGroup::new_with_anchor(out_handle.position)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PenToolFsmState::PlacingAnchor
|
||||
}
|
||||
(PenToolFsmState::DraggingHandle, PenToolMessage::PointerMove { snap_angle, break_handle }) => {
|
||||
if let Some(layer_path) = &tool_data.path {
|
||||
let mouse = tool_data.snap_manager.snap_position(responses, document, input.mouse.position);
|
||||
let mut pos = transform.inverse().transform_point2(mouse);
|
||||
if let Some(((&id, manipulator_group), _previous)) = get_subpath(layer_path, document).and_then(last_2_manipulator_groups) {
|
||||
if let Some(anchor) = manipulator_group.points[ManipulatorType::Anchor].as_ref() {
|
||||
pos = compute_snapped_angle(input, snap_angle, pos, anchor.position);
|
||||
}
|
||||
|
||||
// Update points on current segment (to show preview of new handle)
|
||||
let msg = Operation::MoveManipulatorPoint {
|
||||
layer_path: layer_path.clone(),
|
||||
id,
|
||||
manipulator_type: ManipulatorType::OutHandle,
|
||||
position: pos.into(),
|
||||
};
|
||||
responses.push_back(msg.into());
|
||||
|
||||
// Mirror handle of last segment
|
||||
if !input.keyboard.get(break_handle as usize) && get_subpath(layer_path, document).map(|shape| shape.manipulator_groups().len() > 1).unwrap_or_default() {
|
||||
if let Some(anchor) = manipulator_group.points[ManipulatorType::Anchor].as_ref() {
|
||||
pos = anchor.position - (pos - anchor.position);
|
||||
}
|
||||
let msg = Operation::MoveManipulatorPoint {
|
||||
layer_path: layer_path.clone(),
|
||||
id,
|
||||
manipulator_type: ManipulatorType::InHandle,
|
||||
position: pos.into(),
|
||||
};
|
||||
responses.push_back(msg.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
(PenToolFsmState::PlacingAnchor, PenToolMessage::PointerMove { snap_angle, .. }) => {
|
||||
if let Some(layer_path) = &tool_data.path {
|
||||
let mouse = tool_data.snap_manager.snap_position(responses, document, input.mouse.position);
|
||||
let mut pos = transform.inverse().transform_point2(mouse);
|
||||
|
||||
if let Some(((&id, _), previous)) = get_subpath(layer_path, document).and_then(last_2_manipulator_groups) {
|
||||
if let Some(relative) = previous.as_ref().and_then(|(_, manipulator_group)| manipulator_group.points[ManipulatorType::Anchor].as_ref()) {
|
||||
pos = compute_snapped_angle(input, snap_angle, pos, relative.position);
|
||||
}
|
||||
|
||||
for manipulator_type in [ManipulatorType::Anchor, ManipulatorType::InHandle, ManipulatorType::OutHandle] {
|
||||
let msg = Operation::MoveManipulatorPoint {
|
||||
layer_path: layer_path.clone(),
|
||||
id,
|
||||
manipulator_type,
|
||||
position: pos.into(),
|
||||
};
|
||||
responses.push_back(msg.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
(PenToolFsmState::DraggingHandle | PenToolFsmState::PlacingAnchor, PenToolMessage::Abort | PenToolMessage::Confirm) => {
|
||||
// Abort or commit the transaction to the undo history
|
||||
if let Some(layer_path) = tool_data.path.as_ref() {
|
||||
if let Some(subpath) = (get_subpath(layer_path, document)).filter(|subpath| subpath.manipulator_groups().len() > 1) {
|
||||
if let Some(((&(mut id), mut manipulator_group), previous)) = last_2_manipulator_groups(subpath) {
|
||||
// Remove the unplaced anchor if in anchor placing mode
|
||||
if self == PenToolFsmState::PlacingAnchor {
|
||||
let layer_path = layer_path.clone();
|
||||
let op = Operation::RemoveManipulatorGroup { layer_path, id };
|
||||
responses.push_back(op.into());
|
||||
if let Some((&new_id, new_manipulator_group)) = previous {
|
||||
id = new_id;
|
||||
manipulator_group = new_manipulator_group;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the out handle if in dragging handle mode
|
||||
let op = Operation::MoveManipulatorPoint {
|
||||
layer_path: layer_path.clone(),
|
||||
id,
|
||||
manipulator_type: ManipulatorType::OutHandle,
|
||||
position: manipulator_group.points[ManipulatorType::Anchor].as_ref().unwrap().position.into(),
|
||||
};
|
||||
responses.push_back(op.into());
|
||||
}
|
||||
}
|
||||
|
||||
responses.push_back(DocumentMessage::CommitTransaction.into());
|
||||
} else {
|
||||
responses.push_back(DocumentMessage::AbortTransaction.into());
|
||||
}
|
||||
|
||||
// Clean up overlays
|
||||
for layer_path in document.all_layers() {
|
||||
tool_data.overlay_renderer.clear_subpath_overlays(&document.graphene_document, layer_path.to_vec(), responses);
|
||||
}
|
||||
tool_data.path = None;
|
||||
tool_data.snap_manager.cleanup(responses);
|
||||
|
||||
PenToolFsmState::Ready
|
||||
}
|
||||
(_, PenToolMessage::Abort) => {
|
||||
// Clean up overlays
|
||||
for layer_path in document.all_layers() {
|
||||
tool_data.overlay_renderer.clear_subpath_overlays(&document.graphene_document, layer_path.to_vec(), responses);
|
||||
}
|
||||
self
|
||||
}
|
||||
_ => self,
|
||||
}
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
fn update_hints(&self, responses: &mut VecDeque<Message>) {
|
||||
let hint_data = match self {
|
||||
PenToolFsmState::Ready => HintData(vec![HintGroup(vec![HintInfo {
|
||||
key_groups: vec![],
|
||||
key_groups_mac: None,
|
||||
mouse: Some(MouseMotion::Lmb),
|
||||
label: String::from("Draw Path"),
|
||||
plus: false,
|
||||
}])]),
|
||||
PenToolFsmState::DraggingHandle | PenToolFsmState::PlacingAnchor => HintData(vec![
|
||||
HintGroup(vec![HintInfo {
|
||||
key_groups: vec![],
|
||||
key_groups_mac: None,
|
||||
mouse: Some(MouseMotion::LmbDrag),
|
||||
label: String::from("Add Handle"),
|
||||
plus: false,
|
||||
}]),
|
||||
HintGroup(vec![HintInfo {
|
||||
key_groups: vec![],
|
||||
key_groups_mac: None,
|
||||
mouse: Some(MouseMotion::Lmb),
|
||||
label: String::from("Add Anchor"),
|
||||
plus: false,
|
||||
}]),
|
||||
HintGroup(vec![HintInfo {
|
||||
key_groups: vec![KeysGroup(vec![Key::KeyControl])],
|
||||
key_groups_mac: None,
|
||||
mouse: None,
|
||||
label: String::from("Snap 15°"),
|
||||
plus: false,
|
||||
}]),
|
||||
HintGroup(vec![HintInfo {
|
||||
key_groups: vec![KeysGroup(vec![Key::KeyShift])],
|
||||
key_groups_mac: None,
|
||||
mouse: None,
|
||||
label: String::from("Break Handle"),
|
||||
plus: false,
|
||||
}]),
|
||||
HintGroup(vec![HintInfo {
|
||||
key_groups: vec![KeysGroup(vec![Key::KeyEnter])],
|
||||
key_groups_mac: None,
|
||||
mouse: None,
|
||||
label: String::from("End Path"),
|
||||
plus: false,
|
||||
}]),
|
||||
]),
|
||||
};
|
||||
|
||||
responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into());
|
||||
}
|
||||
|
||||
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
|
||||
responses.push_back(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }.into());
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Expand `pos` name below to the full word (position?)
|
||||
/// Snap the angle of the line from relative to pos if the key is pressed.
|
||||
fn compute_snapped_angle(input: &InputPreprocessorMessageHandler, key: Key, pos: DVec2, relative: DVec2) -> DVec2 {
|
||||
if input.keyboard.get(key as usize) {
|
||||
let delta = relative - pos;
|
||||
|
||||
let length = delta.length();
|
||||
let mut angle = -delta.angle_between(DVec2::X);
|
||||
|
||||
let snap_resolution = LINE_ROTATE_SNAP_ANGLE.to_radians();
|
||||
angle = (angle / snap_resolution).round() * snap_resolution;
|
||||
|
||||
let rotated = DVec2::new(length * angle.cos(), length * angle.sin());
|
||||
relative - rotated
|
||||
} else {
|
||||
pos
|
||||
}
|
||||
}
|
||||
|
||||
/// Pushes a [ManipulatorGroup] to the current layer via an [Operation].
|
||||
fn add_manipulator_group(layer_path: &Option<Vec<LayerId>>, manipulator_group: ManipulatorGroup) -> Message {
|
||||
if let Some(layer_path) = layer_path {
|
||||
Operation::PushManipulatorGroup {
|
||||
layer_path: layer_path.clone(),
|
||||
manipulator_group,
|
||||
}
|
||||
.into()
|
||||
} else {
|
||||
Message::NoOp
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the currently editing [Subpath].
|
||||
fn get_subpath<'a>(layer_path: &'a [LayerId], document: &'a DocumentMessageHandler) -> Option<&'a Subpath> {
|
||||
document.graphene_document.layer(layer_path).ok().and_then(|layer| layer.as_subpath())
|
||||
}
|
||||
|
||||
type ManipulatorGroupRef<'a> = (&'a u64, &'a ManipulatorGroup);
|
||||
|
||||
/// Gets the last 2 [ManipulatorGroup]s on the currently editing layer along with its ID.
|
||||
fn last_2_manipulator_groups(subpath: &Subpath) -> Option<(ManipulatorGroupRef, Option<ManipulatorGroupRef>)> {
|
||||
subpath.manipulator_groups().enumerate().last().map(|last| {
|
||||
(
|
||||
last,
|
||||
(subpath.manipulator_groups().len() > 1)
|
||||
.then(|| subpath.manipulator_groups().enumerate().nth(subpath.manipulator_groups().len() - 2))
|
||||
.flatten(),
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
use crate::consts::DRAG_THRESHOLD;
|
||||
use crate::messages::frontend::utility_types::MouseCursorIcon;
|
||||
use crate::messages::input_mapper::utility_types::input_keyboard::{Key, KeysGroup, MouseMotion};
|
||||
use crate::messages::layout::utility_types::layout_widget::PropertyHolder;
|
||||
use crate::messages::prelude::*;
|
||||
use crate::messages::tool::common_functionality::resize::Resize;
|
||||
use crate::messages::tool::utility_types::{EventToMessageMap, Fsm, ToolActionHandlerData, ToolMetadata, ToolTransition, ToolType};
|
||||
use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo};
|
||||
|
||||
use graphene::layers::style;
|
||||
use graphene::Operation;
|
||||
|
||||
use glam::DAffine2;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct RectangleTool {
|
||||
fsm_state: RectangleToolFsmState,
|
||||
tool_data: RectangleToolData,
|
||||
}
|
||||
|
||||
#[remain::sorted]
|
||||
#[impl_message(Message, ToolMessage, Rectangle)]
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Hash, Serialize, Deserialize)]
|
||||
pub enum RectangleToolMessage {
|
||||
// Standard messages
|
||||
#[remain::unsorted]
|
||||
Abort,
|
||||
|
||||
// Tool-specific messages
|
||||
DragStart,
|
||||
DragStop,
|
||||
Resize {
|
||||
center: Key,
|
||||
lock_ratio: Key,
|
||||
},
|
||||
}
|
||||
|
||||
impl PropertyHolder for RectangleTool {}
|
||||
|
||||
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for RectangleTool {
|
||||
fn process_message(&mut self, message: ToolMessage, tool_data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
|
||||
if message == ToolMessage::UpdateHints {
|
||||
self.fsm_state.update_hints(responses);
|
||||
return;
|
||||
}
|
||||
|
||||
if message == ToolMessage::UpdateCursor {
|
||||
self.fsm_state.update_cursor(responses);
|
||||
return;
|
||||
}
|
||||
|
||||
let new_state = self.fsm_state.transition(message, &mut self.tool_data, tool_data, &(), responses);
|
||||
|
||||
if self.fsm_state != new_state {
|
||||
self.fsm_state = new_state;
|
||||
self.fsm_state.update_hints(responses);
|
||||
self.fsm_state.update_cursor(responses);
|
||||
}
|
||||
}
|
||||
|
||||
fn actions(&self) -> ActionList {
|
||||
use RectangleToolFsmState::*;
|
||||
|
||||
match self.fsm_state {
|
||||
Ready => actions!(RectangleToolMessageDiscriminant;
|
||||
DragStart,
|
||||
),
|
||||
Drawing => actions!(RectangleToolMessageDiscriminant;
|
||||
DragStop,
|
||||
Abort,
|
||||
Resize,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolMetadata for RectangleTool {
|
||||
fn icon_name(&self) -> String {
|
||||
"VectorRectangleTool".into()
|
||||
}
|
||||
fn tooltip(&self) -> String {
|
||||
"Rectangle Tool".into()
|
||||
}
|
||||
fn tool_type(&self) -> crate::messages::tool::utility_types::ToolType {
|
||||
ToolType::Rectangle
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolTransition for RectangleTool {
|
||||
fn event_to_message_map(&self) -> EventToMessageMap {
|
||||
EventToMessageMap {
|
||||
document_dirty: None,
|
||||
tool_abort: Some(RectangleToolMessage::Abort.into()),
|
||||
selection_changed: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum RectangleToolFsmState {
|
||||
Ready,
|
||||
Drawing,
|
||||
}
|
||||
|
||||
impl Default for RectangleToolFsmState {
|
||||
fn default() -> Self {
|
||||
RectangleToolFsmState::Ready
|
||||
}
|
||||
}
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct RectangleToolData {
|
||||
data: Resize,
|
||||
}
|
||||
|
||||
impl Fsm for RectangleToolFsmState {
|
||||
type ToolData = RectangleToolData;
|
||||
type ToolOptions = ();
|
||||
|
||||
fn transition(
|
||||
self,
|
||||
event: ToolMessage,
|
||||
tool_data: &mut Self::ToolData,
|
||||
(document, global_tool_data, input, font_cache): ToolActionHandlerData,
|
||||
_tool_options: &Self::ToolOptions,
|
||||
responses: &mut VecDeque<Message>,
|
||||
) -> Self {
|
||||
use RectangleToolFsmState::*;
|
||||
use RectangleToolMessage::*;
|
||||
|
||||
let mut shape_data = &mut tool_data.data;
|
||||
|
||||
if let ToolMessage::Rectangle(event) = event {
|
||||
match (self, event) {
|
||||
(Ready, DragStart) => {
|
||||
shape_data.start(responses, document, input.mouse.position, font_cache);
|
||||
responses.push_back(DocumentMessage::StartTransaction.into());
|
||||
shape_data.path = Some(document.get_path_for_new_layer());
|
||||
responses.push_back(DocumentMessage::DeselectAllLayers.into());
|
||||
|
||||
responses.push_back(
|
||||
Operation::AddRect {
|
||||
path: shape_data.path.clone().unwrap(),
|
||||
insert_index: -1,
|
||||
transform: DAffine2::ZERO.to_cols_array(),
|
||||
style: style::PathStyle::new(None, style::Fill::solid(global_tool_data.primary_color)),
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
|
||||
Drawing
|
||||
}
|
||||
(state, Resize { center, lock_ratio }) => {
|
||||
if let Some(message) = shape_data.calculate_transform(responses, document, center, lock_ratio, input) {
|
||||
responses.push_back(message);
|
||||
}
|
||||
|
||||
state
|
||||
}
|
||||
(Drawing, DragStop) => {
|
||||
match shape_data.drag_start.distance(input.mouse.position) <= DRAG_THRESHOLD {
|
||||
true => responses.push_back(DocumentMessage::AbortTransaction.into()),
|
||||
false => responses.push_back(DocumentMessage::CommitTransaction.into()),
|
||||
}
|
||||
|
||||
shape_data.cleanup(responses);
|
||||
|
||||
Ready
|
||||
}
|
||||
(Drawing, Abort) => {
|
||||
responses.push_back(DocumentMessage::AbortTransaction.into());
|
||||
|
||||
shape_data.cleanup(responses);
|
||||
|
||||
Ready
|
||||
}
|
||||
_ => self,
|
||||
}
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
fn update_hints(&self, responses: &mut VecDeque<Message>) {
|
||||
let hint_data = match self {
|
||||
RectangleToolFsmState::Ready => HintData(vec![HintGroup(vec![
|
||||
HintInfo {
|
||||
key_groups: vec![],
|
||||
key_groups_mac: None,
|
||||
mouse: Some(MouseMotion::LmbDrag),
|
||||
label: String::from("Draw Rectangle"),
|
||||
plus: false,
|
||||
},
|
||||
HintInfo {
|
||||
key_groups: vec![KeysGroup(vec![Key::KeyShift])],
|
||||
key_groups_mac: None,
|
||||
mouse: None,
|
||||
label: String::from("Constrain Square"),
|
||||
plus: true,
|
||||
},
|
||||
HintInfo {
|
||||
key_groups: vec![KeysGroup(vec![Key::KeyAlt])],
|
||||
key_groups_mac: None,
|
||||
mouse: None,
|
||||
label: String::from("From Center"),
|
||||
plus: true,
|
||||
},
|
||||
])]),
|
||||
RectangleToolFsmState::Drawing => HintData(vec![HintGroup(vec![
|
||||
HintInfo {
|
||||
key_groups: vec![KeysGroup(vec![Key::KeyShift])],
|
||||
key_groups_mac: None,
|
||||
mouse: None,
|
||||
label: String::from("Constrain Square"),
|
||||
plus: false,
|
||||
},
|
||||
HintInfo {
|
||||
key_groups: vec![KeysGroup(vec![Key::KeyAlt])],
|
||||
key_groups_mac: None,
|
||||
mouse: None,
|
||||
label: String::from("From Center"),
|
||||
plus: false,
|
||||
},
|
||||
])]),
|
||||
};
|
||||
|
||||
responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into());
|
||||
}
|
||||
|
||||
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
|
||||
responses.push_back(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Crosshair }.into());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,846 @@
|
||||
use crate::consts::{ROTATE_SNAP_ANGLE, SELECTION_TOLERANCE};
|
||||
use crate::messages::frontend::utility_types::MouseCursorIcon;
|
||||
use crate::messages::input_mapper::utility_types::input_keyboard::{Key, KeysGroup, MouseMotion};
|
||||
use crate::messages::input_mapper::utility_types::input_mouse::ViewportPosition;
|
||||
use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, PropertyHolder, Widget, WidgetCallback, WidgetHolder, WidgetLayout};
|
||||
use crate::messages::layout::utility_types::widgets::button_widgets::{IconButton, PopoverButton};
|
||||
use crate::messages::layout::utility_types::widgets::label_widgets::{Separator, SeparatorDirection, SeparatorType};
|
||||
use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, FlipAxis};
|
||||
use crate::messages::portfolio::document::utility_types::transformation::Selected;
|
||||
use crate::messages::prelude::*;
|
||||
use crate::messages::tool::common_functionality::path_outline::*;
|
||||
use crate::messages::tool::common_functionality::snapping::{self, SnapManager};
|
||||
use crate::messages::tool::common_functionality::transformation_cage::*;
|
||||
use crate::messages::tool::utility_types::{EventToMessageMap, Fsm, ToolActionHandlerData, ToolMetadata, ToolTransition, ToolType};
|
||||
use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo};
|
||||
|
||||
use graphene::boolean_ops::BooleanOperation;
|
||||
use graphene::document::Document;
|
||||
use graphene::intersection::Quad;
|
||||
use graphene::layers::layer_info::LayerDataType;
|
||||
use graphene::LayerId;
|
||||
use graphene::Operation;
|
||||
|
||||
use glam::{DAffine2, DVec2};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct SelectTool {
|
||||
fsm_state: SelectToolFsmState,
|
||||
tool_data: SelectToolData,
|
||||
}
|
||||
|
||||
#[remain::sorted]
|
||||
#[impl_message(Message, ToolMessage, Select)]
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Hash, Serialize, Deserialize)]
|
||||
pub enum SelectToolMessage {
|
||||
// Standard messages
|
||||
#[remain::unsorted]
|
||||
Abort,
|
||||
#[remain::unsorted]
|
||||
DocumentIsDirty,
|
||||
#[remain::unsorted]
|
||||
SelectionChanged,
|
||||
|
||||
// Tool-specific messages
|
||||
Align {
|
||||
axis: AlignAxis,
|
||||
aggregate: AlignAggregate,
|
||||
},
|
||||
DragStart {
|
||||
add_to_selection: Key,
|
||||
},
|
||||
DragStop,
|
||||
EditLayer,
|
||||
FlipHorizontal,
|
||||
FlipVertical,
|
||||
PointerMove {
|
||||
axis_align: Key,
|
||||
snap_angle: Key,
|
||||
center: Key,
|
||||
},
|
||||
}
|
||||
|
||||
impl ToolMetadata for SelectTool {
|
||||
fn icon_name(&self) -> String {
|
||||
"GeneralSelectTool".into()
|
||||
}
|
||||
fn tooltip(&self) -> String {
|
||||
"Select Tool".into()
|
||||
}
|
||||
fn tool_type(&self) -> crate::messages::tool::utility_types::ToolType {
|
||||
ToolType::Select
|
||||
}
|
||||
}
|
||||
|
||||
impl PropertyHolder for SelectTool {
|
||||
fn properties(&self) -> Layout {
|
||||
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row {
|
||||
widgets: vec![
|
||||
WidgetHolder::new(Widget::IconButton(IconButton {
|
||||
icon: "AlignLeft".into(),
|
||||
tooltip: "Align Left".into(),
|
||||
size: 24,
|
||||
on_update: WidgetCallback::new(|_| {
|
||||
DocumentMessage::AlignSelectedLayers {
|
||||
axis: AlignAxis::X,
|
||||
aggregate: AlignAggregate::Min,
|
||||
}
|
||||
.into()
|
||||
}),
|
||||
..IconButton::default()
|
||||
})),
|
||||
WidgetHolder::new(Widget::IconButton(IconButton {
|
||||
icon: "AlignHorizontalCenter".into(),
|
||||
tooltip: "Align Horizontal Center".into(),
|
||||
size: 24,
|
||||
on_update: WidgetCallback::new(|_| {
|
||||
DocumentMessage::AlignSelectedLayers {
|
||||
axis: AlignAxis::X,
|
||||
aggregate: AlignAggregate::Center,
|
||||
}
|
||||
.into()
|
||||
}),
|
||||
..IconButton::default()
|
||||
})),
|
||||
WidgetHolder::new(Widget::IconButton(IconButton {
|
||||
icon: "AlignRight".into(),
|
||||
tooltip: "Align Right".into(),
|
||||
size: 24,
|
||||
on_update: WidgetCallback::new(|_| {
|
||||
DocumentMessage::AlignSelectedLayers {
|
||||
axis: AlignAxis::X,
|
||||
aggregate: AlignAggregate::Max,
|
||||
}
|
||||
.into()
|
||||
}),
|
||||
..IconButton::default()
|
||||
})),
|
||||
WidgetHolder::new(Widget::Separator(Separator {
|
||||
direction: SeparatorDirection::Horizontal,
|
||||
separator_type: SeparatorType::Unrelated,
|
||||
})),
|
||||
WidgetHolder::new(Widget::IconButton(IconButton {
|
||||
icon: "AlignTop".into(),
|
||||
tooltip: "Align Top".into(),
|
||||
size: 24,
|
||||
on_update: WidgetCallback::new(|_| {
|
||||
DocumentMessage::AlignSelectedLayers {
|
||||
axis: AlignAxis::Y,
|
||||
aggregate: AlignAggregate::Min,
|
||||
}
|
||||
.into()
|
||||
}),
|
||||
..IconButton::default()
|
||||
})),
|
||||
WidgetHolder::new(Widget::IconButton(IconButton {
|
||||
icon: "AlignVerticalCenter".into(),
|
||||
tooltip: "Align Vertical Center".into(),
|
||||
size: 24,
|
||||
on_update: WidgetCallback::new(|_| {
|
||||
DocumentMessage::AlignSelectedLayers {
|
||||
axis: AlignAxis::Y,
|
||||
aggregate: AlignAggregate::Center,
|
||||
}
|
||||
.into()
|
||||
}),
|
||||
..IconButton::default()
|
||||
})),
|
||||
WidgetHolder::new(Widget::IconButton(IconButton {
|
||||
icon: "AlignBottom".into(),
|
||||
tooltip: "Align Bottom".into(),
|
||||
size: 24,
|
||||
on_update: WidgetCallback::new(|_| {
|
||||
DocumentMessage::AlignSelectedLayers {
|
||||
axis: AlignAxis::Y,
|
||||
aggregate: AlignAggregate::Max,
|
||||
}
|
||||
.into()
|
||||
}),
|
||||
..IconButton::default()
|
||||
})),
|
||||
WidgetHolder::new(Widget::Separator(Separator {
|
||||
direction: SeparatorDirection::Horizontal,
|
||||
separator_type: SeparatorType::Related,
|
||||
})),
|
||||
WidgetHolder::new(Widget::PopoverButton(PopoverButton {
|
||||
header: "Align".into(),
|
||||
text: "The contents of this popover menu are coming soon".into(),
|
||||
..Default::default()
|
||||
})),
|
||||
WidgetHolder::new(Widget::Separator(Separator {
|
||||
direction: SeparatorDirection::Horizontal,
|
||||
separator_type: SeparatorType::Section,
|
||||
})),
|
||||
WidgetHolder::new(Widget::IconButton(IconButton {
|
||||
icon: "FlipHorizontal".into(),
|
||||
tooltip: "Flip Horizontal".into(),
|
||||
size: 24,
|
||||
on_update: WidgetCallback::new(|_| SelectToolMessage::FlipHorizontal.into()),
|
||||
..IconButton::default()
|
||||
})),
|
||||
WidgetHolder::new(Widget::IconButton(IconButton {
|
||||
icon: "FlipVertical".into(),
|
||||
tooltip: "Flip Vertical".into(),
|
||||
size: 24,
|
||||
on_update: WidgetCallback::new(|_| SelectToolMessage::FlipVertical.into()),
|
||||
..IconButton::default()
|
||||
})),
|
||||
WidgetHolder::new(Widget::Separator(Separator {
|
||||
direction: SeparatorDirection::Horizontal,
|
||||
separator_type: SeparatorType::Related,
|
||||
})),
|
||||
WidgetHolder::new(Widget::PopoverButton(PopoverButton {
|
||||
header: "Flip".into(),
|
||||
text: "The contents of this popover menu are coming soon".into(),
|
||||
..Default::default()
|
||||
})),
|
||||
WidgetHolder::new(Widget::Separator(Separator {
|
||||
direction: SeparatorDirection::Horizontal,
|
||||
separator_type: SeparatorType::Section,
|
||||
})),
|
||||
WidgetHolder::new(Widget::IconButton(IconButton {
|
||||
icon: "BooleanUnion".into(),
|
||||
tooltip: "Boolean Union".into(),
|
||||
size: 24,
|
||||
on_update: WidgetCallback::new(|_| DocumentMessage::BooleanOperation(BooleanOperation::Union).into()),
|
||||
..IconButton::default()
|
||||
})),
|
||||
WidgetHolder::new(Widget::IconButton(IconButton {
|
||||
icon: "BooleanSubtractFront".into(),
|
||||
tooltip: "Boolean Subtract Front".into(),
|
||||
size: 24,
|
||||
on_update: WidgetCallback::new(|_| DocumentMessage::BooleanOperation(BooleanOperation::SubtractFront).into()),
|
||||
..IconButton::default()
|
||||
})),
|
||||
WidgetHolder::new(Widget::IconButton(IconButton {
|
||||
icon: "BooleanSubtractBack".into(),
|
||||
tooltip: "Boolean Subtract Back".into(),
|
||||
size: 24,
|
||||
on_update: WidgetCallback::new(|_| DocumentMessage::BooleanOperation(BooleanOperation::SubtractBack).into()),
|
||||
..IconButton::default()
|
||||
})),
|
||||
WidgetHolder::new(Widget::IconButton(IconButton {
|
||||
icon: "BooleanIntersect".into(),
|
||||
tooltip: "Boolean Intersect".into(),
|
||||
size: 24,
|
||||
on_update: WidgetCallback::new(|_| DocumentMessage::BooleanOperation(BooleanOperation::Intersection).into()),
|
||||
..IconButton::default()
|
||||
})),
|
||||
WidgetHolder::new(Widget::IconButton(IconButton {
|
||||
icon: "BooleanDifference".into(),
|
||||
tooltip: "Boolean Difference".into(),
|
||||
size: 24,
|
||||
on_update: WidgetCallback::new(|_| DocumentMessage::BooleanOperation(BooleanOperation::Difference).into()),
|
||||
..IconButton::default()
|
||||
})),
|
||||
WidgetHolder::new(Widget::Separator(Separator {
|
||||
direction: SeparatorDirection::Horizontal,
|
||||
separator_type: SeparatorType::Related,
|
||||
})),
|
||||
WidgetHolder::new(Widget::PopoverButton(PopoverButton {
|
||||
header: "Boolean".into(),
|
||||
text: "The contents of this popover menu are coming soon".into(),
|
||||
..Default::default()
|
||||
})),
|
||||
],
|
||||
}]))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for SelectTool {
|
||||
fn process_message(&mut self, message: ToolMessage, tool_data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
|
||||
if message == ToolMessage::UpdateHints {
|
||||
self.fsm_state.update_hints(responses);
|
||||
return;
|
||||
}
|
||||
|
||||
if message == ToolMessage::UpdateCursor {
|
||||
responses.push_back(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }.into());
|
||||
return;
|
||||
}
|
||||
|
||||
let new_state = self.fsm_state.transition(message, &mut self.tool_data, tool_data, &(), responses);
|
||||
|
||||
if self.fsm_state != new_state {
|
||||
self.fsm_state = new_state;
|
||||
self.fsm_state.update_hints(responses);
|
||||
}
|
||||
}
|
||||
|
||||
fn actions(&self) -> ActionList {
|
||||
use SelectToolFsmState::*;
|
||||
|
||||
match self.fsm_state {
|
||||
Ready => actions!(SelectToolMessageDiscriminant;
|
||||
DragStart,
|
||||
PointerMove,
|
||||
Abort,
|
||||
EditLayer,
|
||||
),
|
||||
_ => actions!(SelectToolMessageDiscriminant;
|
||||
DragStop,
|
||||
PointerMove,
|
||||
Abort,
|
||||
EditLayer,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolTransition for SelectTool {
|
||||
fn event_to_message_map(&self) -> EventToMessageMap {
|
||||
EventToMessageMap {
|
||||
document_dirty: Some(SelectToolMessage::DocumentIsDirty.into()),
|
||||
tool_abort: Some(SelectToolMessage::Abort.into()),
|
||||
selection_changed: Some(SelectToolMessage::SelectionChanged.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
enum SelectToolFsmState {
|
||||
Ready,
|
||||
Dragging,
|
||||
DrawingBox,
|
||||
ResizingBounds,
|
||||
RotatingBounds,
|
||||
}
|
||||
|
||||
impl Default for SelectToolFsmState {
|
||||
fn default() -> Self {
|
||||
SelectToolFsmState::Ready
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct SelectToolData {
|
||||
drag_start: ViewportPosition,
|
||||
drag_current: ViewportPosition,
|
||||
layers_dragging: Vec<Vec<LayerId>>, // Paths and offsets
|
||||
drag_box_overlay_layer: Option<Vec<LayerId>>,
|
||||
path_outlines: PathOutline,
|
||||
bounding_box_overlays: Option<BoundingBoxOverlays>,
|
||||
snap_manager: SnapManager,
|
||||
cursor: MouseCursorIcon,
|
||||
}
|
||||
|
||||
impl SelectToolData {
|
||||
fn selection_quad(&self) -> Quad {
|
||||
let bbox = self.selection_box();
|
||||
Quad::from_box(bbox)
|
||||
}
|
||||
|
||||
fn selection_box(&self) -> [DVec2; 2] {
|
||||
if self.drag_current == self.drag_start {
|
||||
let tolerance = DVec2::splat(SELECTION_TOLERANCE);
|
||||
[self.drag_start - tolerance, self.drag_start + tolerance]
|
||||
} else {
|
||||
[self.drag_start, self.drag_current]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Fsm for SelectToolFsmState {
|
||||
type ToolData = SelectToolData;
|
||||
type ToolOptions = ();
|
||||
|
||||
fn transition(
|
||||
self,
|
||||
event: ToolMessage,
|
||||
tool_data: &mut Self::ToolData,
|
||||
(document, _global_tool_data, input, font_cache): ToolActionHandlerData,
|
||||
_tool_options: &Self::ToolOptions,
|
||||
responses: &mut VecDeque<Message>,
|
||||
) -> Self {
|
||||
use SelectToolFsmState::*;
|
||||
use SelectToolMessage::*;
|
||||
|
||||
if let ToolMessage::Select(event) = event {
|
||||
match (self, event) {
|
||||
(_, DocumentIsDirty | SelectionChanged) => {
|
||||
match (document.selected_visible_layers_bounding_box(font_cache), tool_data.bounding_box_overlays.take()) {
|
||||
(None, Some(bounding_box_overlays)) => bounding_box_overlays.delete(responses),
|
||||
(Some(bounds), paths) => {
|
||||
let mut bounding_box_overlays = paths.unwrap_or_else(|| BoundingBoxOverlays::new(responses));
|
||||
|
||||
bounding_box_overlays.bounds = bounds;
|
||||
bounding_box_overlays.transform = DAffine2::IDENTITY;
|
||||
|
||||
bounding_box_overlays.transform(responses);
|
||||
|
||||
tool_data.bounding_box_overlays = Some(bounding_box_overlays);
|
||||
}
|
||||
(_, _) => {}
|
||||
};
|
||||
|
||||
tool_data.path_outlines.update_selected(document.selected_visible_layers(), document, responses, font_cache);
|
||||
tool_data.path_outlines.intersect_test_hovered(input, document, responses, font_cache);
|
||||
|
||||
self
|
||||
}
|
||||
(_, EditLayer) => {
|
||||
let mouse_pos = input.mouse.position;
|
||||
let tolerance = DVec2::splat(SELECTION_TOLERANCE);
|
||||
let quad = Quad::from_box([mouse_pos - tolerance, mouse_pos + tolerance]);
|
||||
|
||||
if let Some(Ok(intersect)) = document
|
||||
.graphene_document
|
||||
.intersects_quad_root(quad, font_cache)
|
||||
.last()
|
||||
.map(|path| document.graphene_document.layer(path))
|
||||
{
|
||||
match intersect.data {
|
||||
LayerDataType::Text(_) => {
|
||||
responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Text }.into());
|
||||
responses.push_back(TextToolMessage::Interact.into());
|
||||
}
|
||||
LayerDataType::Shape(_) => {
|
||||
responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Path }.into());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
(Ready, DragStart { add_to_selection }) => {
|
||||
tool_data.path_outlines.clear_hovered(responses);
|
||||
|
||||
tool_data.drag_start = input.mouse.position;
|
||||
tool_data.drag_current = input.mouse.position;
|
||||
|
||||
let dragging_bounds = if let Some(bounding_box) = &mut tool_data.bounding_box_overlays {
|
||||
let edges = bounding_box.check_selected_edges(input.mouse.position);
|
||||
|
||||
bounding_box.selected_edges = edges.map(|(top, bottom, left, right)| {
|
||||
let edges = SelectedEdges::new(top, bottom, left, right, bounding_box.bounds);
|
||||
bounding_box.pivot = edges.calculate_pivot();
|
||||
edges
|
||||
});
|
||||
|
||||
edges
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let rotating_bounds = if let Some(bounding_box) = &mut tool_data.bounding_box_overlays {
|
||||
bounding_box.check_rotate(input.mouse.position)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
let mut selected: Vec<_> = document.selected_visible_layers().map(|path| path.to_vec()).collect();
|
||||
let quad = tool_data.selection_quad();
|
||||
let mut intersection = document.graphene_document.intersects_quad_root(quad, font_cache);
|
||||
// If the user is dragging the bounding box bounds, go into ResizingBounds mode.
|
||||
// If the user is dragging the rotate trigger, go into RotatingBounds mode.
|
||||
// If the user clicks on a layer that is in their current selection, go into the dragging mode.
|
||||
// If the user clicks on new shape, make that layer their new selection.
|
||||
// Otherwise enter the box select mode
|
||||
let state = if let Some(selected_edges) = dragging_bounds {
|
||||
let snap_x = selected_edges.2 || selected_edges.3;
|
||||
let snap_y = selected_edges.0 || selected_edges.1;
|
||||
|
||||
tool_data.snap_manager.start_snap(document, document.bounding_boxes(Some(&selected), None, font_cache), snap_x, snap_y);
|
||||
tool_data
|
||||
.snap_manager
|
||||
.add_all_document_handles(document, &[], &selected.iter().map(|x| x.as_slice()).collect::<Vec<_>>(), &[]);
|
||||
|
||||
tool_data.layers_dragging = selected;
|
||||
|
||||
ResizingBounds
|
||||
} else if rotating_bounds {
|
||||
if let Some(bounds) = &mut tool_data.bounding_box_overlays {
|
||||
let selected = selected.iter().collect::<Vec<_>>();
|
||||
let mut selected = Selected::new(&mut bounds.original_transforms, &mut bounds.pivot, &selected, responses, &document.graphene_document);
|
||||
|
||||
*selected.pivot = selected.calculate_pivot(font_cache);
|
||||
}
|
||||
|
||||
tool_data.layers_dragging = selected;
|
||||
|
||||
RotatingBounds
|
||||
} else if intersection.last().map(|last| selected.contains(last)).unwrap_or(false) {
|
||||
responses.push_back(DocumentMessage::StartTransaction.into());
|
||||
tool_data.layers_dragging = selected;
|
||||
|
||||
tool_data
|
||||
.snap_manager
|
||||
.start_snap(document, document.bounding_boxes(Some(&tool_data.layers_dragging), None, font_cache), true, true);
|
||||
|
||||
Dragging
|
||||
} else {
|
||||
if !input.keyboard.get(add_to_selection as usize) {
|
||||
responses.push_back(DocumentMessage::DeselectAllLayers.into());
|
||||
tool_data.layers_dragging.clear();
|
||||
}
|
||||
|
||||
if let Some(intersection) = intersection.pop() {
|
||||
selected = vec![intersection];
|
||||
responses.push_back(DocumentMessage::AddSelectedLayers { additional_layers: selected.clone() }.into());
|
||||
responses.push_back(DocumentMessage::StartTransaction.into());
|
||||
tool_data.layers_dragging.append(&mut selected);
|
||||
tool_data
|
||||
.snap_manager
|
||||
.start_snap(document, document.bounding_boxes(Some(&tool_data.layers_dragging), None, font_cache), true, true);
|
||||
|
||||
Dragging
|
||||
} else {
|
||||
tool_data.drag_box_overlay_layer = Some(add_bounding_box(responses));
|
||||
DrawingBox
|
||||
}
|
||||
};
|
||||
|
||||
state
|
||||
}
|
||||
(Dragging, PointerMove { axis_align, .. }) => {
|
||||
// TODO: This is a cheat. Break out the relevant functionality from the handler above and call it from there and here.
|
||||
responses.push_front(SelectToolMessage::DocumentIsDirty.into());
|
||||
|
||||
let mouse_position = axis_align_drag(input.keyboard.get(axis_align as usize), input.mouse.position, tool_data.drag_start);
|
||||
|
||||
let mouse_delta = mouse_position - tool_data.drag_current;
|
||||
|
||||
let snap = tool_data
|
||||
.layers_dragging
|
||||
.iter()
|
||||
.filter_map(|path| document.graphene_document.viewport_bounding_box(path, font_cache).ok()?)
|
||||
.flat_map(snapping::expand_bounds)
|
||||
.collect();
|
||||
|
||||
let closest_move = tool_data.snap_manager.snap_layers(responses, document, snap, mouse_delta);
|
||||
// TODO: Cache the result of `shallowest_unique_layers` to avoid this heavy computation every frame of movement, see https://github.com/GraphiteEditor/Graphite/pull/481
|
||||
for path in Document::shallowest_unique_layers(tool_data.layers_dragging.iter()) {
|
||||
responses.push_front(
|
||||
Operation::TransformLayerInViewport {
|
||||
path: path.clone(),
|
||||
transform: DAffine2::from_translation(mouse_delta + closest_move).to_cols_array(),
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
tool_data.drag_current = mouse_position + closest_move;
|
||||
Dragging
|
||||
}
|
||||
(ResizingBounds, PointerMove { axis_align, center, .. }) => {
|
||||
if let Some(bounds) = &mut tool_data.bounding_box_overlays {
|
||||
if let Some(movement) = &mut bounds.selected_edges {
|
||||
let (center, axis_align) = (input.keyboard.get(center as usize), input.keyboard.get(axis_align as usize));
|
||||
|
||||
let mouse_position = input.mouse.position;
|
||||
|
||||
let snapped_mouse_position = tool_data.snap_manager.snap_position(responses, document, mouse_position);
|
||||
|
||||
let (_, size) = movement.new_size(snapped_mouse_position, bounds.transform, center, axis_align);
|
||||
let delta = movement.bounds_to_scale_transform(center, size);
|
||||
|
||||
let selected = tool_data.layers_dragging.iter().collect::<Vec<_>>();
|
||||
let mut selected = Selected::new(&mut bounds.original_transforms, &mut bounds.pivot, &selected, responses, &document.graphene_document);
|
||||
|
||||
selected.update_transforms(delta);
|
||||
}
|
||||
}
|
||||
ResizingBounds
|
||||
}
|
||||
(RotatingBounds, PointerMove { snap_angle, .. }) => {
|
||||
if let Some(bounds) = &mut tool_data.bounding_box_overlays {
|
||||
let angle = {
|
||||
let start_offset = tool_data.drag_start - bounds.pivot;
|
||||
let end_offset = input.mouse.position - bounds.pivot;
|
||||
|
||||
start_offset.angle_between(end_offset)
|
||||
};
|
||||
|
||||
let snapped_angle = if input.keyboard.get(snap_angle as usize) {
|
||||
let snap_resolution = ROTATE_SNAP_ANGLE.to_radians();
|
||||
(angle / snap_resolution).round() * snap_resolution
|
||||
} else {
|
||||
angle
|
||||
};
|
||||
|
||||
let delta = DAffine2::from_angle(snapped_angle);
|
||||
|
||||
let selected = tool_data.layers_dragging.iter().collect::<Vec<_>>();
|
||||
let mut selected = Selected::new(&mut bounds.original_transforms, &mut bounds.pivot, &selected, responses, &document.graphene_document);
|
||||
|
||||
selected.update_transforms(delta);
|
||||
}
|
||||
|
||||
RotatingBounds
|
||||
}
|
||||
(DrawingBox, PointerMove { .. }) => {
|
||||
tool_data.drag_current = input.mouse.position;
|
||||
|
||||
responses.push_front(
|
||||
DocumentMessage::Overlays(
|
||||
Operation::SetLayerTransformInViewport {
|
||||
path: tool_data.drag_box_overlay_layer.clone().unwrap(),
|
||||
transform: transform_from_box(tool_data.drag_start, tool_data.drag_current, DAffine2::IDENTITY).to_cols_array(),
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
DrawingBox
|
||||
}
|
||||
(Ready, PointerMove { .. }) => {
|
||||
let cursor = tool_data.bounding_box_overlays.as_ref().map_or(MouseCursorIcon::Default, |bounds| bounds.get_cursor(input, true));
|
||||
|
||||
// Generate the select outline (but not if the user is going to use the bound overlays)
|
||||
if cursor == MouseCursorIcon::Default {
|
||||
tool_data.path_outlines.intersect_test_hovered(input, document, responses, font_cache);
|
||||
} else {
|
||||
tool_data.path_outlines.clear_hovered(responses);
|
||||
}
|
||||
|
||||
if tool_data.cursor != cursor {
|
||||
tool_data.cursor = cursor;
|
||||
responses.push_back(FrontendMessage::UpdateMouseCursor { cursor }.into());
|
||||
}
|
||||
|
||||
Ready
|
||||
}
|
||||
(Dragging, DragStop) => {
|
||||
let response = match input.mouse.position.distance(tool_data.drag_start) < 10. * f64::EPSILON {
|
||||
true => DocumentMessage::Undo,
|
||||
false => DocumentMessage::CommitTransaction,
|
||||
};
|
||||
tool_data.snap_manager.cleanup(responses);
|
||||
responses.push_front(response.into());
|
||||
Ready
|
||||
}
|
||||
(ResizingBounds, DragStop) => {
|
||||
tool_data.snap_manager.cleanup(responses);
|
||||
|
||||
if let Some(bounds) = &mut tool_data.bounding_box_overlays {
|
||||
bounds.original_transforms.clear();
|
||||
}
|
||||
|
||||
Ready
|
||||
}
|
||||
(RotatingBounds, DragStop) => {
|
||||
if let Some(bounds) = &mut tool_data.bounding_box_overlays {
|
||||
bounds.original_transforms.clear();
|
||||
}
|
||||
|
||||
Ready
|
||||
}
|
||||
(DrawingBox, DragStop) => {
|
||||
let quad = tool_data.selection_quad();
|
||||
responses.push_front(
|
||||
DocumentMessage::AddSelectedLayers {
|
||||
additional_layers: document.graphene_document.intersects_quad_root(quad, font_cache),
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
responses.push_front(
|
||||
DocumentMessage::Overlays(
|
||||
Operation::DeleteLayer {
|
||||
path: tool_data.drag_box_overlay_layer.take().unwrap(),
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
Ready
|
||||
}
|
||||
(Dragging, Abort) => {
|
||||
tool_data.snap_manager.cleanup(responses);
|
||||
responses.push_back(DocumentMessage::Undo.into());
|
||||
|
||||
tool_data.path_outlines.clear_selected(responses);
|
||||
|
||||
Ready
|
||||
}
|
||||
(_, Abort) => {
|
||||
if let Some(path) = tool_data.drag_box_overlay_layer.take() {
|
||||
responses.push_front(DocumentMessage::Overlays(Operation::DeleteLayer { path }.into()).into())
|
||||
};
|
||||
if let Some(mut bounding_box_overlays) = tool_data.bounding_box_overlays.take() {
|
||||
let selected = tool_data.layers_dragging.iter().collect::<Vec<_>>();
|
||||
let mut selected = Selected::new(
|
||||
&mut bounding_box_overlays.original_transforms,
|
||||
&mut bounding_box_overlays.pivot,
|
||||
&selected,
|
||||
responses,
|
||||
&document.graphene_document,
|
||||
);
|
||||
|
||||
selected.revert_operation();
|
||||
|
||||
bounding_box_overlays.delete(responses);
|
||||
}
|
||||
|
||||
tool_data.path_outlines.clear_hovered(responses);
|
||||
tool_data.path_outlines.clear_selected(responses);
|
||||
|
||||
tool_data.snap_manager.cleanup(responses);
|
||||
Ready
|
||||
}
|
||||
(_, Align { axis, aggregate }) => {
|
||||
responses.push_back(DocumentMessage::AlignSelectedLayers { axis, aggregate }.into());
|
||||
|
||||
self
|
||||
}
|
||||
(_, FlipHorizontal) => {
|
||||
responses.push_back(DocumentMessage::FlipSelectedLayers { flip_axis: FlipAxis::X }.into());
|
||||
|
||||
self
|
||||
}
|
||||
(_, FlipVertical) => {
|
||||
responses.push_back(DocumentMessage::FlipSelectedLayers { flip_axis: FlipAxis::Y }.into());
|
||||
|
||||
self
|
||||
}
|
||||
_ => self,
|
||||
}
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
fn update_hints(&self, responses: &mut VecDeque<Message>) {
|
||||
let hint_data = match self {
|
||||
SelectToolFsmState::Ready => HintData(vec![
|
||||
HintGroup(vec![HintInfo {
|
||||
key_groups: vec![],
|
||||
key_groups_mac: None,
|
||||
mouse: Some(MouseMotion::LmbDrag),
|
||||
label: String::from("Drag Selected"),
|
||||
plus: false,
|
||||
}]),
|
||||
HintGroup(vec![
|
||||
HintInfo {
|
||||
key_groups: vec![KeysGroup(vec![Key::KeyG])],
|
||||
key_groups_mac: None,
|
||||
mouse: None,
|
||||
label: String::from("Grab Selected"),
|
||||
plus: false,
|
||||
},
|
||||
HintInfo {
|
||||
key_groups: vec![KeysGroup(vec![Key::KeyR])],
|
||||
key_groups_mac: None,
|
||||
mouse: None,
|
||||
label: String::from("Rotate Selected"),
|
||||
plus: false,
|
||||
},
|
||||
HintInfo {
|
||||
key_groups: vec![KeysGroup(vec![Key::KeyS])],
|
||||
key_groups_mac: None,
|
||||
mouse: None,
|
||||
label: String::from("Scale Selected"),
|
||||
plus: false,
|
||||
},
|
||||
]),
|
||||
HintGroup(vec![
|
||||
HintInfo {
|
||||
key_groups: vec![],
|
||||
key_groups_mac: None,
|
||||
mouse: Some(MouseMotion::Lmb),
|
||||
label: String::from("Select Object"),
|
||||
plus: false,
|
||||
},
|
||||
HintInfo {
|
||||
key_groups: vec![KeysGroup(vec![Key::KeyControl])],
|
||||
key_groups_mac: Some(vec![KeysGroup(vec![Key::KeyCommand])]),
|
||||
mouse: None,
|
||||
label: String::from("Innermost"),
|
||||
plus: true,
|
||||
},
|
||||
HintInfo {
|
||||
key_groups: vec![KeysGroup(vec![Key::KeyShift])],
|
||||
key_groups_mac: None,
|
||||
mouse: None,
|
||||
label: String::from("Grow/Shrink Selection"),
|
||||
plus: true,
|
||||
},
|
||||
]),
|
||||
HintGroup(vec![
|
||||
HintInfo {
|
||||
key_groups: vec![],
|
||||
key_groups_mac: None,
|
||||
mouse: Some(MouseMotion::LmbDrag),
|
||||
label: String::from("Select Area"),
|
||||
plus: false,
|
||||
},
|
||||
HintInfo {
|
||||
key_groups: vec![KeysGroup(vec![Key::KeyShift])],
|
||||
key_groups_mac: None,
|
||||
mouse: None,
|
||||
label: String::from("Grow/Shrink Selection"),
|
||||
plus: true,
|
||||
},
|
||||
]),
|
||||
HintGroup(vec![
|
||||
HintInfo {
|
||||
key_groups: vec![
|
||||
KeysGroup(vec![Key::KeyArrowUp]),
|
||||
KeysGroup(vec![Key::KeyArrowRight]),
|
||||
KeysGroup(vec![Key::KeyArrowDown]),
|
||||
KeysGroup(vec![Key::KeyArrowLeft]),
|
||||
],
|
||||
key_groups_mac: None,
|
||||
mouse: None,
|
||||
label: String::from("Nudge Selected"),
|
||||
plus: false,
|
||||
},
|
||||
HintInfo {
|
||||
key_groups: vec![KeysGroup(vec![Key::KeyShift])],
|
||||
key_groups_mac: None,
|
||||
mouse: None,
|
||||
label: String::from("Big Increment Nudge"),
|
||||
plus: true,
|
||||
},
|
||||
]),
|
||||
HintGroup(vec![
|
||||
HintInfo {
|
||||
key_groups: vec![KeysGroup(vec![Key::KeyAlt])],
|
||||
key_groups_mac: None,
|
||||
mouse: Some(MouseMotion::LmbDrag),
|
||||
label: String::from("Move Duplicate"),
|
||||
plus: false,
|
||||
},
|
||||
HintInfo {
|
||||
key_groups: vec![KeysGroup(vec![Key::KeyControl, Key::KeyD])],
|
||||
key_groups_mac: Some(vec![KeysGroup(vec![Key::KeyCommand, Key::KeyD])]),
|
||||
mouse: None,
|
||||
label: String::from("Duplicate"),
|
||||
plus: false,
|
||||
},
|
||||
]),
|
||||
]),
|
||||
SelectToolFsmState::Dragging => HintData(vec![HintGroup(vec![
|
||||
HintInfo {
|
||||
key_groups: vec![KeysGroup(vec![Key::KeyShift])],
|
||||
key_groups_mac: None,
|
||||
mouse: None,
|
||||
label: String::from("Constrain to Axis"),
|
||||
plus: false,
|
||||
},
|
||||
HintInfo {
|
||||
key_groups: vec![KeysGroup(vec![Key::KeyControl])],
|
||||
key_groups_mac: None,
|
||||
mouse: None,
|
||||
label: String::from("Snap to Points (coming soon)"),
|
||||
plus: false,
|
||||
},
|
||||
])]),
|
||||
SelectToolFsmState::DrawingBox => HintData(vec![]),
|
||||
SelectToolFsmState::ResizingBounds => HintData(vec![]),
|
||||
SelectToolFsmState::RotatingBounds => HintData(vec![HintGroup(vec![HintInfo {
|
||||
key_groups: vec![KeysGroup(vec![Key::KeyControl])],
|
||||
key_groups_mac: None,
|
||||
mouse: None,
|
||||
label: String::from("Snap 15°"),
|
||||
plus: false,
|
||||
}])]),
|
||||
};
|
||||
|
||||
responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into());
|
||||
}
|
||||
|
||||
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
|
||||
responses.push_back(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }.into());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
use crate::consts::DRAG_THRESHOLD;
|
||||
use crate::messages::frontend::utility_types::MouseCursorIcon;
|
||||
use crate::messages::input_mapper::utility_types::input_keyboard::{Key, KeysGroup, MouseMotion};
|
||||
use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, PropertyHolder, Widget, WidgetCallback, WidgetHolder, WidgetLayout};
|
||||
use crate::messages::layout::utility_types::widgets::input_widgets::NumberInput;
|
||||
use crate::messages::prelude::*;
|
||||
use crate::messages::tool::common_functionality::resize::Resize;
|
||||
use crate::messages::tool::utility_types::{EventToMessageMap, Fsm, ToolActionHandlerData, ToolMetadata, ToolTransition, ToolType};
|
||||
use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo};
|
||||
|
||||
use graphene::layers::style;
|
||||
use graphene::Operation;
|
||||
|
||||
use glam::DAffine2;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ShapeTool {
|
||||
fsm_state: ShapeToolFsmState,
|
||||
tool_data: ShapeToolData,
|
||||
options: ShapeOptions,
|
||||
}
|
||||
|
||||
pub struct ShapeOptions {
|
||||
vertices: u32,
|
||||
}
|
||||
|
||||
impl Default for ShapeOptions {
|
||||
fn default() -> Self {
|
||||
Self { vertices: 6 }
|
||||
}
|
||||
}
|
||||
|
||||
#[remain::sorted]
|
||||
#[impl_message(Message, ToolMessage, Shape)]
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Hash, Serialize, Deserialize)]
|
||||
pub enum ShapeToolMessage {
|
||||
// Standard messages
|
||||
#[remain::unsorted]
|
||||
Abort,
|
||||
|
||||
// Tool-specific messages
|
||||
DragStart,
|
||||
DragStop,
|
||||
Resize {
|
||||
center: Key,
|
||||
lock_ratio: Key,
|
||||
},
|
||||
UpdateOptions(ShapeOptionsUpdate),
|
||||
}
|
||||
|
||||
#[remain::sorted]
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Hash, Serialize, Deserialize)]
|
||||
pub enum ShapeOptionsUpdate {
|
||||
Vertices(u32),
|
||||
}
|
||||
|
||||
impl ToolMetadata for ShapeTool {
|
||||
fn icon_name(&self) -> String {
|
||||
"VectorShapeTool".into()
|
||||
}
|
||||
fn tooltip(&self) -> String {
|
||||
"Shape Tool".into()
|
||||
}
|
||||
fn tool_type(&self) -> crate::messages::tool::utility_types::ToolType {
|
||||
ToolType::Shape
|
||||
}
|
||||
}
|
||||
|
||||
impl PropertyHolder for ShapeTool {
|
||||
fn properties(&self) -> Layout {
|
||||
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row {
|
||||
widgets: vec![WidgetHolder::new(Widget::NumberInput(NumberInput {
|
||||
label: "Sides".into(),
|
||||
value: Some(self.options.vertices as f64),
|
||||
is_integer: true,
|
||||
min: Some(3.),
|
||||
max: Some(1000.),
|
||||
on_update: WidgetCallback::new(|number_input: &NumberInput| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::Vertices(number_input.value.unwrap() as u32)).into()),
|
||||
..NumberInput::default()
|
||||
}))],
|
||||
}]))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for ShapeTool {
|
||||
fn process_message(&mut self, message: ToolMessage, tool_data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
|
||||
if message == ToolMessage::UpdateHints {
|
||||
self.fsm_state.update_hints(responses);
|
||||
return;
|
||||
}
|
||||
|
||||
if message == ToolMessage::UpdateCursor {
|
||||
self.fsm_state.update_cursor(responses);
|
||||
return;
|
||||
}
|
||||
|
||||
if let ToolMessage::Shape(ShapeToolMessage::UpdateOptions(action)) = message {
|
||||
match action {
|
||||
ShapeOptionsUpdate::Vertices(vertices) => self.options.vertices = vertices,
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let new_state = self.fsm_state.transition(message, &mut self.tool_data, tool_data, &self.options, responses);
|
||||
|
||||
if self.fsm_state != new_state {
|
||||
self.fsm_state = new_state;
|
||||
self.fsm_state.update_hints(responses);
|
||||
self.fsm_state.update_cursor(responses);
|
||||
}
|
||||
}
|
||||
|
||||
fn actions(&self) -> ActionList {
|
||||
use ShapeToolFsmState::*;
|
||||
|
||||
match self.fsm_state {
|
||||
Ready => actions!(ShapeToolMessageDiscriminant;
|
||||
DragStart,
|
||||
),
|
||||
Drawing => actions!(ShapeToolMessageDiscriminant;
|
||||
DragStop,
|
||||
Abort,
|
||||
Resize,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolTransition for ShapeTool {
|
||||
fn event_to_message_map(&self) -> EventToMessageMap {
|
||||
EventToMessageMap {
|
||||
document_dirty: None,
|
||||
tool_abort: Some(ShapeToolMessage::Abort.into()),
|
||||
selection_changed: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum ShapeToolFsmState {
|
||||
Ready,
|
||||
Drawing,
|
||||
}
|
||||
|
||||
impl Default for ShapeToolFsmState {
|
||||
fn default() -> Self {
|
||||
ShapeToolFsmState::Ready
|
||||
}
|
||||
}
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct ShapeToolData {
|
||||
sides: u32,
|
||||
data: Resize,
|
||||
}
|
||||
|
||||
impl Fsm for ShapeToolFsmState {
|
||||
type ToolData = ShapeToolData;
|
||||
type ToolOptions = ShapeOptions;
|
||||
|
||||
fn transition(
|
||||
self,
|
||||
event: ToolMessage,
|
||||
tool_data: &mut Self::ToolData,
|
||||
(document, global_tool_data, input, font_cache): ToolActionHandlerData,
|
||||
tool_options: &Self::ToolOptions,
|
||||
responses: &mut VecDeque<Message>,
|
||||
) -> Self {
|
||||
use ShapeToolFsmState::*;
|
||||
use ShapeToolMessage::*;
|
||||
|
||||
let mut shape_data = &mut tool_data.data;
|
||||
|
||||
if let ToolMessage::Shape(event) = event {
|
||||
match (self, event) {
|
||||
(Ready, DragStart) => {
|
||||
shape_data.start(responses, document, input.mouse.position, font_cache);
|
||||
responses.push_back(DocumentMessage::StartTransaction.into());
|
||||
shape_data.path = Some(document.get_path_for_new_layer());
|
||||
responses.push_back(DocumentMessage::DeselectAllLayers.into());
|
||||
tool_data.sides = tool_options.vertices;
|
||||
|
||||
responses.push_back(
|
||||
Operation::AddNgon {
|
||||
path: shape_data.path.clone().unwrap(),
|
||||
insert_index: -1,
|
||||
transform: DAffine2::ZERO.to_cols_array(),
|
||||
sides: tool_data.sides,
|
||||
style: style::PathStyle::new(None, style::Fill::solid(global_tool_data.primary_color)),
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
|
||||
Drawing
|
||||
}
|
||||
(state, Resize { center, lock_ratio }) => {
|
||||
if let Some(message) = shape_data.calculate_transform(responses, document, center, lock_ratio, input) {
|
||||
responses.push_back(message);
|
||||
}
|
||||
|
||||
state
|
||||
}
|
||||
(Drawing, DragStop) => {
|
||||
match shape_data.drag_start.distance(input.mouse.position) <= DRAG_THRESHOLD {
|
||||
true => responses.push_back(DocumentMessage::AbortTransaction.into()),
|
||||
false => responses.push_back(DocumentMessage::CommitTransaction.into()),
|
||||
}
|
||||
|
||||
shape_data.cleanup(responses);
|
||||
|
||||
Ready
|
||||
}
|
||||
(Drawing, Abort) => {
|
||||
responses.push_back(DocumentMessage::AbortTransaction.into());
|
||||
|
||||
shape_data.cleanup(responses);
|
||||
|
||||
Ready
|
||||
}
|
||||
_ => self,
|
||||
}
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
fn update_hints(&self, responses: &mut VecDeque<Message>) {
|
||||
let hint_data = match self {
|
||||
ShapeToolFsmState::Ready => HintData(vec![HintGroup(vec![
|
||||
HintInfo {
|
||||
key_groups: vec![],
|
||||
key_groups_mac: None,
|
||||
mouse: Some(MouseMotion::LmbDrag),
|
||||
label: String::from("Draw Shape"),
|
||||
plus: false,
|
||||
},
|
||||
HintInfo {
|
||||
key_groups: vec![KeysGroup(vec![Key::KeyShift])],
|
||||
key_groups_mac: None,
|
||||
mouse: None,
|
||||
label: String::from("Constrain 1:1 Aspect"),
|
||||
plus: true,
|
||||
},
|
||||
HintInfo {
|
||||
key_groups: vec![KeysGroup(vec![Key::KeyAlt])],
|
||||
key_groups_mac: None,
|
||||
mouse: None,
|
||||
label: String::from("From Center"),
|
||||
plus: true,
|
||||
},
|
||||
])]),
|
||||
ShapeToolFsmState::Drawing => HintData(vec![HintGroup(vec![
|
||||
HintInfo {
|
||||
key_groups: vec![KeysGroup(vec![Key::KeyShift])],
|
||||
key_groups_mac: None,
|
||||
mouse: None,
|
||||
label: String::from("Constrain 1:1 Aspect"),
|
||||
plus: false,
|
||||
},
|
||||
HintInfo {
|
||||
key_groups: vec![KeysGroup(vec![Key::KeyAlt])],
|
||||
key_groups_mac: None,
|
||||
mouse: None,
|
||||
label: String::from("From Center"),
|
||||
plus: false,
|
||||
},
|
||||
])]),
|
||||
};
|
||||
|
||||
responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into());
|
||||
}
|
||||
|
||||
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
|
||||
responses.push_back(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Crosshair }.into());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,308 @@
|
||||
use crate::consts::DRAG_THRESHOLD;
|
||||
use crate::messages::frontend::utility_types::MouseCursorIcon;
|
||||
use crate::messages::input_mapper::utility_types::input_keyboard::{Key, KeysGroup, MouseMotion};
|
||||
use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, PropertyHolder, Widget, WidgetCallback, WidgetHolder, WidgetLayout};
|
||||
use crate::messages::layout::utility_types::widgets::input_widgets::NumberInput;
|
||||
use crate::messages::prelude::*;
|
||||
use crate::messages::tool::common_functionality::snapping::SnapManager;
|
||||
use crate::messages::tool::utility_types::{DocumentToolData, EventToMessageMap, Fsm, ToolActionHandlerData, ToolMetadata, ToolTransition, ToolType};
|
||||
use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo};
|
||||
|
||||
use graphene::layers::style;
|
||||
use graphene::LayerId;
|
||||
use graphene::Operation;
|
||||
|
||||
use glam::{DAffine2, DVec2};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct SplineTool {
|
||||
fsm_state: SplineToolFsmState,
|
||||
tool_data: SplineToolData,
|
||||
options: SplineOptions,
|
||||
}
|
||||
|
||||
pub struct SplineOptions {
|
||||
line_weight: f64,
|
||||
}
|
||||
|
||||
impl Default for SplineOptions {
|
||||
fn default() -> Self {
|
||||
Self { line_weight: 5. }
|
||||
}
|
||||
}
|
||||
|
||||
#[remain::sorted]
|
||||
#[impl_message(Message, ToolMessage, Spline)]
|
||||
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum SplineToolMessage {
|
||||
// Standard messages
|
||||
#[remain::unsorted]
|
||||
Abort,
|
||||
|
||||
// Tool-specific messages
|
||||
Confirm,
|
||||
DragStart,
|
||||
DragStop,
|
||||
PointerMove,
|
||||
Undo,
|
||||
UpdateOptions(SplineOptionsUpdate),
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum SplineToolFsmState {
|
||||
Ready,
|
||||
Drawing,
|
||||
}
|
||||
|
||||
#[remain::sorted]
|
||||
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum SplineOptionsUpdate {
|
||||
LineWeight(f64),
|
||||
}
|
||||
|
||||
impl ToolMetadata for SplineTool {
|
||||
fn icon_name(&self) -> String {
|
||||
"VectorSplineTool".into()
|
||||
}
|
||||
fn tooltip(&self) -> String {
|
||||
"Spline Tool".into()
|
||||
}
|
||||
fn tool_type(&self) -> crate::messages::tool::utility_types::ToolType {
|
||||
ToolType::Spline
|
||||
}
|
||||
}
|
||||
|
||||
impl PropertyHolder for SplineTool {
|
||||
fn properties(&self) -> Layout {
|
||||
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row {
|
||||
widgets: vec![WidgetHolder::new(Widget::NumberInput(NumberInput {
|
||||
unit: " px".into(),
|
||||
label: "Weight".into(),
|
||||
value: Some(self.options.line_weight),
|
||||
is_integer: false,
|
||||
min: Some(0.),
|
||||
on_update: WidgetCallback::new(|number_input: &NumberInput| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::LineWeight(number_input.value.unwrap())).into()),
|
||||
..NumberInput::default()
|
||||
}))],
|
||||
}]))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for SplineTool {
|
||||
fn process_message(&mut self, message: ToolMessage, tool_data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
|
||||
if message == ToolMessage::UpdateHints {
|
||||
self.fsm_state.update_hints(responses);
|
||||
return;
|
||||
}
|
||||
|
||||
if message == ToolMessage::UpdateCursor {
|
||||
self.fsm_state.update_cursor(responses);
|
||||
return;
|
||||
}
|
||||
|
||||
if let ToolMessage::Spline(SplineToolMessage::UpdateOptions(action)) = message {
|
||||
match action {
|
||||
SplineOptionsUpdate::LineWeight(line_weight) => self.options.line_weight = line_weight,
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let new_state = self.fsm_state.transition(message, &mut self.tool_data, tool_data, &self.options, responses);
|
||||
|
||||
if self.fsm_state != new_state {
|
||||
self.fsm_state = new_state;
|
||||
self.fsm_state.update_hints(responses);
|
||||
self.fsm_state.update_cursor(responses);
|
||||
}
|
||||
}
|
||||
|
||||
fn actions(&self) -> ActionList {
|
||||
use SplineToolFsmState::*;
|
||||
|
||||
match self.fsm_state {
|
||||
Ready => actions!(SplineToolMessageDiscriminant;
|
||||
Undo,
|
||||
DragStart,
|
||||
DragStop,
|
||||
Confirm,
|
||||
Abort,
|
||||
),
|
||||
Drawing => actions!(SplineToolMessageDiscriminant;
|
||||
DragStop,
|
||||
PointerMove,
|
||||
Confirm,
|
||||
Abort,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolTransition for SplineTool {
|
||||
fn event_to_message_map(&self) -> EventToMessageMap {
|
||||
EventToMessageMap {
|
||||
document_dirty: None,
|
||||
tool_abort: Some(SplineToolMessage::Abort.into()),
|
||||
selection_changed: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SplineToolFsmState {
|
||||
fn default() -> Self {
|
||||
SplineToolFsmState::Ready
|
||||
}
|
||||
}
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct SplineToolData {
|
||||
points: Vec<DVec2>,
|
||||
next_point: DVec2,
|
||||
weight: f64,
|
||||
path: Option<Vec<LayerId>>,
|
||||
snap_manager: SnapManager,
|
||||
}
|
||||
|
||||
impl Fsm for SplineToolFsmState {
|
||||
type ToolData = SplineToolData;
|
||||
type ToolOptions = SplineOptions;
|
||||
|
||||
fn transition(
|
||||
self,
|
||||
event: ToolMessage,
|
||||
tool_data: &mut Self::ToolData,
|
||||
(document, global_tool_data, input, font_cache): ToolActionHandlerData,
|
||||
tool_options: &Self::ToolOptions,
|
||||
responses: &mut VecDeque<Message>,
|
||||
) -> Self {
|
||||
use SplineToolFsmState::*;
|
||||
use SplineToolMessage::*;
|
||||
|
||||
let transform = document.graphene_document.root.transform;
|
||||
|
||||
if let ToolMessage::Spline(event) = event {
|
||||
match (self, event) {
|
||||
(Ready, DragStart) => {
|
||||
responses.push_back(DocumentMessage::StartTransaction.into());
|
||||
responses.push_back(DocumentMessage::DeselectAllLayers.into());
|
||||
tool_data.path = Some(document.get_path_for_new_layer());
|
||||
|
||||
tool_data.snap_manager.start_snap(document, document.bounding_boxes(None, None, font_cache), true, true);
|
||||
tool_data.snap_manager.add_all_document_handles(document, &[], &[], &[]);
|
||||
let snapped_position = tool_data.snap_manager.snap_position(responses, document, input.mouse.position);
|
||||
|
||||
let pos = transform.inverse().transform_point2(snapped_position);
|
||||
|
||||
tool_data.points.push(pos);
|
||||
tool_data.next_point = pos;
|
||||
|
||||
tool_data.weight = tool_options.line_weight;
|
||||
|
||||
responses.push_back(add_spline(tool_data, global_tool_data, true));
|
||||
|
||||
Drawing
|
||||
}
|
||||
(Drawing, DragStop) => {
|
||||
let snapped_position = tool_data.snap_manager.snap_position(responses, document, input.mouse.position);
|
||||
let pos = transform.inverse().transform_point2(snapped_position);
|
||||
|
||||
if let Some(last_pos) = tool_data.points.last() {
|
||||
if last_pos.distance(pos) > DRAG_THRESHOLD {
|
||||
tool_data.points.push(pos);
|
||||
tool_data.next_point = pos;
|
||||
}
|
||||
}
|
||||
|
||||
responses.push_back(remove_preview(tool_data));
|
||||
responses.push_back(add_spline(tool_data, global_tool_data, true));
|
||||
|
||||
Drawing
|
||||
}
|
||||
(Drawing, PointerMove) => {
|
||||
let snapped_position = tool_data.snap_manager.snap_position(responses, document, input.mouse.position);
|
||||
let pos = transform.inverse().transform_point2(snapped_position);
|
||||
tool_data.next_point = pos;
|
||||
|
||||
responses.push_back(remove_preview(tool_data));
|
||||
responses.push_back(add_spline(tool_data, global_tool_data, true));
|
||||
|
||||
Drawing
|
||||
}
|
||||
(Drawing, Confirm) | (Drawing, Abort) => {
|
||||
if tool_data.points.len() >= 2 {
|
||||
responses.push_back(remove_preview(tool_data));
|
||||
responses.push_back(add_spline(tool_data, global_tool_data, false));
|
||||
responses.push_back(DocumentMessage::CommitTransaction.into());
|
||||
} else {
|
||||
responses.push_back(DocumentMessage::AbortTransaction.into());
|
||||
}
|
||||
|
||||
tool_data.path = None;
|
||||
tool_data.points.clear();
|
||||
tool_data.snap_manager.cleanup(responses);
|
||||
|
||||
Ready
|
||||
}
|
||||
_ => self,
|
||||
}
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
fn update_hints(&self, responses: &mut VecDeque<Message>) {
|
||||
let hint_data = match self {
|
||||
SplineToolFsmState::Ready => HintData(vec![HintGroup(vec![HintInfo {
|
||||
key_groups: vec![],
|
||||
key_groups_mac: None,
|
||||
mouse: Some(MouseMotion::Lmb),
|
||||
label: String::from("Draw Spline"),
|
||||
plus: false,
|
||||
}])]),
|
||||
SplineToolFsmState::Drawing => HintData(vec![
|
||||
HintGroup(vec![HintInfo {
|
||||
key_groups: vec![],
|
||||
key_groups_mac: None,
|
||||
mouse: Some(MouseMotion::Lmb),
|
||||
label: String::from("Extend Spline"),
|
||||
plus: false,
|
||||
}]),
|
||||
HintGroup(vec![HintInfo {
|
||||
key_groups: vec![KeysGroup(vec![Key::KeyEnter])],
|
||||
key_groups_mac: None,
|
||||
mouse: None,
|
||||
label: String::from("End Spline"),
|
||||
plus: false,
|
||||
}]),
|
||||
]),
|
||||
};
|
||||
|
||||
responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into());
|
||||
}
|
||||
|
||||
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
|
||||
responses.push_back(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }.into());
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_preview(tool_data: &SplineToolData) -> Message {
|
||||
Operation::DeleteLayer {
|
||||
path: tool_data.path.clone().unwrap(),
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
fn add_spline(tool_data: &SplineToolData, global_tool_data: &DocumentToolData, show_preview: bool) -> Message {
|
||||
let mut points: Vec<(f64, f64)> = tool_data.points.iter().map(|p| (p.x, p.y)).collect();
|
||||
if show_preview {
|
||||
points.push((tool_data.next_point.x, tool_data.next_point.y))
|
||||
}
|
||||
|
||||
Operation::AddSpline {
|
||||
path: tool_data.path.clone().unwrap(),
|
||||
insert_index: -1,
|
||||
transform: DAffine2::IDENTITY.to_cols_array(),
|
||||
points,
|
||||
style: style::PathStyle::new(Some(style::Stroke::new(global_tool_data.primary_color, tool_data.weight)), style::Fill::None),
|
||||
}
|
||||
.into()
|
||||
}
|
||||
@@ -0,0 +1,499 @@
|
||||
use crate::application::generate_uuid;
|
||||
use crate::consts::{COLOR_ACCENT, SELECTION_TOLERANCE};
|
||||
use crate::messages::frontend::utility_types::MouseCursorIcon;
|
||||
use crate::messages::input_mapper::utility_types::input_keyboard::{Key, KeysGroup, MouseMotion};
|
||||
use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, PropertyHolder, Widget, WidgetCallback, WidgetHolder, WidgetLayout};
|
||||
use crate::messages::layout::utility_types::misc::LayoutTarget;
|
||||
use crate::messages::layout::utility_types::widgets::input_widgets::{FontInput, NumberInput};
|
||||
use crate::messages::layout::utility_types::widgets::label_widgets::{Separator, SeparatorDirection, SeparatorType};
|
||||
use crate::messages::prelude::*;
|
||||
use crate::messages::tool::utility_types::{EventToMessageMap, Fsm, ToolActionHandlerData, ToolMetadata, ToolTransition, ToolType};
|
||||
use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo};
|
||||
|
||||
use graphene::intersection::Quad;
|
||||
use graphene::layers::style::{self, Fill, Stroke};
|
||||
use graphene::layers::text_layer::FontCache;
|
||||
use graphene::LayerId;
|
||||
use graphene::Operation;
|
||||
|
||||
use glam::{DAffine2, DVec2};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct TextTool {
|
||||
fsm_state: TextToolFsmState,
|
||||
tool_data: TextToolData,
|
||||
options: TextOptions,
|
||||
}
|
||||
|
||||
pub struct TextOptions {
|
||||
font_size: u32,
|
||||
font_name: String,
|
||||
font_style: String,
|
||||
}
|
||||
|
||||
impl Default for TextOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
font_size: 24,
|
||||
font_name: "Merriweather".into(),
|
||||
font_style: "Normal (400)".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[remain::sorted]
|
||||
#[impl_message(Message, ToolMessage, Text)]
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Hash, Serialize, Deserialize)]
|
||||
pub enum TextToolMessage {
|
||||
// Standard messages
|
||||
#[remain::unsorted]
|
||||
Abort,
|
||||
|
||||
#[remain::unsorted]
|
||||
DocumentIsDirty,
|
||||
|
||||
// Tool-specific messages
|
||||
CommitText,
|
||||
Interact,
|
||||
TextChange {
|
||||
new_text: String,
|
||||
},
|
||||
UpdateBounds {
|
||||
new_text: String,
|
||||
},
|
||||
UpdateOptions(TextOptionsUpdate),
|
||||
}
|
||||
|
||||
#[remain::sorted]
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Hash, Serialize, Deserialize)]
|
||||
pub enum TextOptionsUpdate {
|
||||
Font { family: String, style: String },
|
||||
FontSize(u32),
|
||||
}
|
||||
|
||||
impl ToolMetadata for TextTool {
|
||||
fn icon_name(&self) -> String {
|
||||
"VectorTextTool".into()
|
||||
}
|
||||
fn tooltip(&self) -> String {
|
||||
"Text Tool".into()
|
||||
}
|
||||
fn tool_type(&self) -> crate::messages::tool::utility_types::ToolType {
|
||||
ToolType::Text
|
||||
}
|
||||
}
|
||||
|
||||
impl PropertyHolder for TextTool {
|
||||
fn properties(&self) -> Layout {
|
||||
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row {
|
||||
widgets: vec![
|
||||
WidgetHolder::new(Widget::FontInput(FontInput {
|
||||
is_style_picker: false,
|
||||
font_family: self.options.font_name.clone(),
|
||||
font_style: self.options.font_style.clone(),
|
||||
on_update: WidgetCallback::new(|font_input: &FontInput| {
|
||||
TextToolMessage::UpdateOptions(TextOptionsUpdate::Font {
|
||||
family: font_input.font_family.clone(),
|
||||
style: font_input.font_style.clone(),
|
||||
})
|
||||
.into()
|
||||
}),
|
||||
..Default::default()
|
||||
})),
|
||||
WidgetHolder::new(Widget::Separator(Separator {
|
||||
direction: SeparatorDirection::Horizontal,
|
||||
separator_type: SeparatorType::Related,
|
||||
})),
|
||||
WidgetHolder::new(Widget::FontInput(FontInput {
|
||||
is_style_picker: true,
|
||||
font_family: self.options.font_name.clone(),
|
||||
font_style: self.options.font_style.clone(),
|
||||
on_update: WidgetCallback::new(|font_input: &FontInput| {
|
||||
TextToolMessage::UpdateOptions(TextOptionsUpdate::Font {
|
||||
family: font_input.font_family.clone(),
|
||||
style: font_input.font_style.clone(),
|
||||
})
|
||||
.into()
|
||||
}),
|
||||
..Default::default()
|
||||
})),
|
||||
WidgetHolder::new(Widget::Separator(Separator {
|
||||
direction: SeparatorDirection::Horizontal,
|
||||
separator_type: SeparatorType::Related,
|
||||
})),
|
||||
WidgetHolder::new(Widget::NumberInput(NumberInput {
|
||||
unit: " px".into(),
|
||||
label: "Size".into(),
|
||||
value: Some(self.options.font_size as f64),
|
||||
is_integer: true,
|
||||
min: Some(1.),
|
||||
on_update: WidgetCallback::new(|number_input: &NumberInput| TextToolMessage::UpdateOptions(TextOptionsUpdate::FontSize(number_input.value.unwrap() as u32)).into()),
|
||||
..NumberInput::default()
|
||||
})),
|
||||
],
|
||||
}]))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for TextTool {
|
||||
fn process_message(&mut self, message: ToolMessage, tool_data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
|
||||
if message == ToolMessage::UpdateHints {
|
||||
self.fsm_state.update_hints(responses);
|
||||
return;
|
||||
}
|
||||
|
||||
if message == ToolMessage::UpdateCursor {
|
||||
self.fsm_state.update_cursor(responses);
|
||||
return;
|
||||
}
|
||||
|
||||
if let ToolMessage::Text(TextToolMessage::UpdateOptions(action)) = message {
|
||||
match action {
|
||||
TextOptionsUpdate::Font { family, style } => {
|
||||
self.options.font_name = family;
|
||||
self.options.font_style = style;
|
||||
|
||||
self.register_properties(responses, LayoutTarget::ToolOptions);
|
||||
}
|
||||
TextOptionsUpdate::FontSize(font_size) => self.options.font_size = font_size,
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let new_state = self.fsm_state.transition(message, &mut self.tool_data, tool_data, &self.options, responses);
|
||||
|
||||
if self.fsm_state != new_state {
|
||||
self.fsm_state = new_state;
|
||||
self.fsm_state.update_hints(responses);
|
||||
self.fsm_state.update_cursor(responses);
|
||||
}
|
||||
}
|
||||
|
||||
fn actions(&self) -> ActionList {
|
||||
use TextToolFsmState::*;
|
||||
|
||||
match self.fsm_state {
|
||||
Ready => actions!(TextToolMessageDiscriminant;
|
||||
Interact,
|
||||
),
|
||||
Editing => actions!(TextToolMessageDiscriminant;
|
||||
Interact,
|
||||
Abort,
|
||||
CommitText,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolTransition for TextTool {
|
||||
fn event_to_message_map(&self) -> EventToMessageMap {
|
||||
EventToMessageMap {
|
||||
document_dirty: Some(TextToolMessage::DocumentIsDirty.into()),
|
||||
tool_abort: Some(TextToolMessage::Abort.into()),
|
||||
selection_changed: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum TextToolFsmState {
|
||||
Ready,
|
||||
Editing,
|
||||
}
|
||||
|
||||
impl Default for TextToolFsmState {
|
||||
fn default() -> Self {
|
||||
TextToolFsmState::Ready
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct TextToolData {
|
||||
path: Vec<LayerId>,
|
||||
overlays: Vec<Vec<LayerId>>,
|
||||
}
|
||||
|
||||
fn transform_from_box(pos1: DVec2, pos2: DVec2) -> [f64; 6] {
|
||||
DAffine2::from_scale_angle_translation((pos2 - pos1).round(), 0., pos1.round() - DVec2::splat(0.5)).to_cols_array()
|
||||
}
|
||||
|
||||
fn resize_overlays(overlays: &mut Vec<Vec<LayerId>>, responses: &mut VecDeque<Message>, newlen: usize) {
|
||||
while overlays.len() > newlen {
|
||||
let operation = Operation::DeleteLayer { path: overlays.pop().unwrap() };
|
||||
responses.push_back(DocumentMessage::Overlays(operation.into()).into());
|
||||
}
|
||||
while overlays.len() < newlen {
|
||||
let path = vec![generate_uuid()];
|
||||
overlays.push(path.clone());
|
||||
|
||||
let operation = Operation::AddRect {
|
||||
path,
|
||||
transform: DAffine2::ZERO.to_cols_array(),
|
||||
style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 1.0)), Fill::None),
|
||||
insert_index: -1,
|
||||
};
|
||||
responses.push_back(DocumentMessage::Overlays(operation.into()).into());
|
||||
}
|
||||
}
|
||||
|
||||
fn update_overlays(document: &DocumentMessageHandler, tool_data: &mut TextToolData, responses: &mut VecDeque<Message>, font_cache: &FontCache) {
|
||||
let visible_text_layers = document.selected_visible_text_layers().collect::<Vec<_>>();
|
||||
resize_overlays(&mut tool_data.overlays, responses, visible_text_layers.len());
|
||||
|
||||
let bounds = visible_text_layers
|
||||
.into_iter()
|
||||
.zip(&tool_data.overlays)
|
||||
.filter_map(|(layer_path, overlay_path)| {
|
||||
document
|
||||
.graphene_document
|
||||
.layer(layer_path)
|
||||
.unwrap()
|
||||
.aabb_for_transform(document.graphene_document.multiply_transforms(layer_path).unwrap(), font_cache)
|
||||
.map(|bounds| (bounds, overlay_path))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let new_len = bounds.len();
|
||||
|
||||
for (bounds, overlay_path) in bounds {
|
||||
let operation = Operation::SetLayerTransformInViewport {
|
||||
path: overlay_path.to_vec(),
|
||||
transform: transform_from_box(bounds[0], bounds[1]),
|
||||
};
|
||||
responses.push_back(DocumentMessage::Overlays(operation.into()).into());
|
||||
}
|
||||
resize_overlays(&mut tool_data.overlays, responses, new_len);
|
||||
}
|
||||
|
||||
impl Fsm for TextToolFsmState {
|
||||
type ToolData = TextToolData;
|
||||
type ToolOptions = TextOptions;
|
||||
|
||||
fn transition(
|
||||
self,
|
||||
event: ToolMessage,
|
||||
tool_data: &mut Self::ToolData,
|
||||
(document, global_tool_data, input, font_cache): ToolActionHandlerData,
|
||||
tool_options: &Self::ToolOptions,
|
||||
responses: &mut VecDeque<Message>,
|
||||
) -> Self {
|
||||
use TextToolFsmState::*;
|
||||
use TextToolMessage::*;
|
||||
|
||||
if let ToolMessage::Text(event) = event {
|
||||
match (self, event) {
|
||||
(state, DocumentIsDirty) => {
|
||||
update_overlays(document, tool_data, responses, font_cache);
|
||||
|
||||
state
|
||||
}
|
||||
(state, Interact) => {
|
||||
let mouse_pos = input.mouse.position;
|
||||
let tolerance = DVec2::splat(SELECTION_TOLERANCE);
|
||||
let quad = Quad::from_box([mouse_pos - tolerance, mouse_pos + tolerance]);
|
||||
|
||||
let new_state = if let Some(l) = document
|
||||
.graphene_document
|
||||
.intersects_quad_root(quad, font_cache)
|
||||
.last()
|
||||
.filter(|l| document.graphene_document.layer(l).map(|l| l.as_text().is_ok()).unwrap_or(false))
|
||||
// Editing existing text
|
||||
{
|
||||
if state == TextToolFsmState::Editing {
|
||||
responses.push_back(
|
||||
DocumentMessage::SetTexboxEditability {
|
||||
path: tool_data.path.clone(),
|
||||
editable: false,
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
tool_data.path = l.clone();
|
||||
|
||||
responses.push_back(
|
||||
DocumentMessage::SetTexboxEditability {
|
||||
path: tool_data.path.clone(),
|
||||
editable: true,
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
responses.push_back(
|
||||
DocumentMessage::SetSelectedLayers {
|
||||
replacement_selected_layers: vec![tool_data.path.clone()],
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
|
||||
Editing
|
||||
}
|
||||
// Creating new text
|
||||
else if state == TextToolFsmState::Ready {
|
||||
let transform = DAffine2::from_translation(input.mouse.position).to_cols_array();
|
||||
let font_size = tool_options.font_size;
|
||||
let font_name = tool_options.font_name.clone();
|
||||
let font_style = tool_options.font_style.clone();
|
||||
tool_data.path = document.get_path_for_new_layer();
|
||||
|
||||
responses.push_back(
|
||||
Operation::AddText {
|
||||
path: tool_data.path.clone(),
|
||||
transform: DAffine2::ZERO.to_cols_array(),
|
||||
insert_index: -1,
|
||||
text: r#""#.to_string(),
|
||||
style: style::PathStyle::new(None, Fill::solid(global_tool_data.primary_color)),
|
||||
size: font_size as f64,
|
||||
font_name,
|
||||
font_style,
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
responses.push_back(
|
||||
Operation::SetLayerTransformInViewport {
|
||||
path: tool_data.path.clone(),
|
||||
transform,
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
|
||||
responses.push_back(
|
||||
DocumentMessage::SetTexboxEditability {
|
||||
path: tool_data.path.clone(),
|
||||
editable: true,
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
|
||||
responses.push_back(
|
||||
DocumentMessage::SetSelectedLayers {
|
||||
replacement_selected_layers: vec![tool_data.path.clone()],
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
|
||||
Editing
|
||||
} else {
|
||||
// Removing old text as editable
|
||||
responses.push_back(
|
||||
DocumentMessage::SetTexboxEditability {
|
||||
path: tool_data.path.clone(),
|
||||
editable: false,
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
|
||||
resize_overlays(&mut tool_data.overlays, responses, 0);
|
||||
|
||||
Ready
|
||||
};
|
||||
|
||||
new_state
|
||||
}
|
||||
(state, Abort) => {
|
||||
if state == TextToolFsmState::Editing {
|
||||
responses.push_back(
|
||||
DocumentMessage::SetTexboxEditability {
|
||||
path: tool_data.path.clone(),
|
||||
editable: false,
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
resize_overlays(&mut tool_data.overlays, responses, 0);
|
||||
|
||||
Ready
|
||||
}
|
||||
(Editing, CommitText) => {
|
||||
responses.push_back(FrontendMessage::TriggerTextCommit.into());
|
||||
|
||||
Editing
|
||||
}
|
||||
(Editing, TextChange { new_text }) => {
|
||||
responses.push_back(
|
||||
Operation::SetTextContent {
|
||||
path: tool_data.path.clone(),
|
||||
new_text,
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
|
||||
responses.push_back(
|
||||
DocumentMessage::SetTexboxEditability {
|
||||
path: tool_data.path.clone(),
|
||||
editable: false,
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
|
||||
resize_overlays(&mut tool_data.overlays, responses, 0);
|
||||
|
||||
Ready
|
||||
}
|
||||
(Editing, UpdateBounds { new_text }) => {
|
||||
resize_overlays(&mut tool_data.overlays, responses, 1);
|
||||
let text = document.graphene_document.layer(&tool_data.path).unwrap().as_text().unwrap();
|
||||
let quad = text.bounding_box(&new_text, text.load_face(font_cache));
|
||||
|
||||
let transformed_quad = document.graphene_document.multiply_transforms(&tool_data.path).unwrap() * quad;
|
||||
let bounds = transformed_quad.bounding_box();
|
||||
|
||||
let operation = Operation::SetLayerTransformInViewport {
|
||||
path: tool_data.overlays[0].clone(),
|
||||
transform: transform_from_box(bounds[0], bounds[1]),
|
||||
};
|
||||
responses.push_back(DocumentMessage::Overlays(operation.into()).into());
|
||||
|
||||
Editing
|
||||
}
|
||||
_ => self,
|
||||
}
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
fn update_hints(&self, responses: &mut VecDeque<Message>) {
|
||||
let hint_data = match self {
|
||||
TextToolFsmState::Ready => HintData(vec![HintGroup(vec![
|
||||
HintInfo {
|
||||
key_groups: vec![],
|
||||
key_groups_mac: None,
|
||||
mouse: Some(MouseMotion::Lmb),
|
||||
label: String::from("Add Text"),
|
||||
plus: false,
|
||||
},
|
||||
HintInfo {
|
||||
key_groups: vec![],
|
||||
key_groups_mac: None,
|
||||
mouse: Some(MouseMotion::Lmb),
|
||||
label: String::from("Edit Text"),
|
||||
plus: false,
|
||||
},
|
||||
])]),
|
||||
TextToolFsmState::Editing => HintData(vec![HintGroup(vec![
|
||||
HintInfo {
|
||||
key_groups: vec![KeysGroup(vec![Key::KeyControl, Key::KeyEnter])],
|
||||
key_groups_mac: Some(vec![KeysGroup(vec![Key::KeyCommand, Key::KeyEnter])]),
|
||||
mouse: None,
|
||||
label: String::from("Commit Edit"),
|
||||
plus: false,
|
||||
},
|
||||
HintInfo {
|
||||
key_groups: vec![KeysGroup(vec![Key::KeyEscape])],
|
||||
key_groups_mac: None,
|
||||
mouse: None,
|
||||
label: String::from("Discard Edit"),
|
||||
plus: false,
|
||||
},
|
||||
])]),
|
||||
};
|
||||
|
||||
responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into());
|
||||
}
|
||||
|
||||
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
|
||||
responses.push_back(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Text }.into());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,460 @@
|
||||
use super::tool_messages::*;
|
||||
use crate::messages::input_mapper::utility_types::input_keyboard::KeysGroup;
|
||||
use crate::messages::input_mapper::utility_types::input_keyboard::MouseMotion;
|
||||
use crate::messages::input_mapper::utility_types::macros::action_keys;
|
||||
use crate::messages::input_mapper::utility_types::misc::ActionKeys;
|
||||
use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, PropertyHolder, Widget, WidgetCallback, WidgetHolder, WidgetLayout};
|
||||
use crate::messages::layout::utility_types::misc::LayoutTarget;
|
||||
use crate::messages::layout::utility_types::widgets::button_widgets::IconButton;
|
||||
use crate::messages::layout::utility_types::widgets::input_widgets::SwatchPairInput;
|
||||
use crate::messages::layout::utility_types::widgets::label_widgets::{Separator, SeparatorDirection, SeparatorType};
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
use graphene::color::Color;
|
||||
use graphene::layers::text_layer::FontCache;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use std::fmt::{self, Debug};
|
||||
|
||||
pub type ToolActionHandlerData<'a> = (&'a DocumentMessageHandler, &'a DocumentToolData, &'a InputPreprocessorMessageHandler, &'a FontCache);
|
||||
|
||||
pub trait ToolCommon: for<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> + PropertyHolder + ToolTransition + ToolMetadata {}
|
||||
impl<T> ToolCommon for T where T: for<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> + PropertyHolder + ToolTransition + ToolMetadata {}
|
||||
|
||||
type Tool = dyn ToolCommon;
|
||||
|
||||
pub trait Fsm {
|
||||
type ToolData;
|
||||
type ToolOptions;
|
||||
|
||||
#[must_use]
|
||||
fn transition(self, message: ToolMessage, tool_data: &mut Self::ToolData, transition_data: ToolActionHandlerData, options: &Self::ToolOptions, messages: &mut VecDeque<Message>) -> Self;
|
||||
|
||||
fn update_hints(&self, responses: &mut VecDeque<Message>);
|
||||
fn update_cursor(&self, responses: &mut VecDeque<Message>);
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DocumentToolData {
|
||||
pub primary_color: Color,
|
||||
pub secondary_color: Color,
|
||||
}
|
||||
|
||||
impl DocumentToolData {
|
||||
pub fn update_working_colors(&self, responses: &mut VecDeque<Message>) {
|
||||
let layout = WidgetLayout::new(vec![
|
||||
LayoutGroup::Row {
|
||||
widgets: vec![WidgetHolder::new(Widget::SwatchPairInput(SwatchPairInput {
|
||||
primary: self.primary_color,
|
||||
secondary: self.secondary_color,
|
||||
}))],
|
||||
},
|
||||
LayoutGroup::Row {
|
||||
widgets: vec![
|
||||
WidgetHolder::new(Widget::IconButton(IconButton {
|
||||
size: 16,
|
||||
icon: "Swap".into(),
|
||||
tooltip: "Swap".into(),
|
||||
tooltip_shortcut: action_keys!(ToolMessageDiscriminant::SwapColors),
|
||||
on_update: WidgetCallback::new(|_| ToolMessage::SwapColors.into()),
|
||||
..Default::default()
|
||||
})),
|
||||
WidgetHolder::new(Widget::IconButton(IconButton {
|
||||
size: 16,
|
||||
icon: "ResetColors".into(),
|
||||
tooltip: "Reset".into(),
|
||||
tooltip_shortcut: action_keys!(ToolMessageDiscriminant::ResetColors),
|
||||
on_update: WidgetCallback::new(|_| ToolMessage::ResetColors.into()),
|
||||
..Default::default()
|
||||
})),
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
responses.push_back(
|
||||
LayoutMessage::SendLayout {
|
||||
layout: Layout::WidgetLayout(layout),
|
||||
layout_target: LayoutTarget::WorkingColors,
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct EventToMessageMap {
|
||||
pub document_dirty: Option<ToolMessage>,
|
||||
pub selection_changed: Option<ToolMessage>,
|
||||
pub tool_abort: Option<ToolMessage>,
|
||||
}
|
||||
|
||||
pub trait ToolTransition {
|
||||
fn event_to_message_map(&self) -> EventToMessageMap;
|
||||
|
||||
fn activate(&self, responses: &mut VecDeque<Message>) {
|
||||
let mut subscribe_message = |broadcast_to_tool_mapping: Option<ToolMessage>, event: BroadcastEvent| {
|
||||
if let Some(mapping) = broadcast_to_tool_mapping {
|
||||
responses.push_back(
|
||||
BroadcastMessage::SubscribeEvent {
|
||||
on: event,
|
||||
send: Box::new(mapping.into()),
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
let event_to_tool_map = self.event_to_message_map();
|
||||
subscribe_message(event_to_tool_map.document_dirty, BroadcastEvent::DocumentIsDirty);
|
||||
subscribe_message(event_to_tool_map.tool_abort, BroadcastEvent::ToolAbort);
|
||||
subscribe_message(event_to_tool_map.selection_changed, BroadcastEvent::SelectionChanged);
|
||||
}
|
||||
|
||||
fn deactivate(&self, responses: &mut VecDeque<Message>) {
|
||||
let mut unsubscribe_message = |broadcast_to_tool_mapping: Option<ToolMessage>, event: BroadcastEvent| {
|
||||
if let Some(mapping) = broadcast_to_tool_mapping {
|
||||
responses.push_back(
|
||||
BroadcastMessage::UnsubscribeEvent {
|
||||
on: event,
|
||||
message: Box::new(mapping.into()),
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
let event_to_tool_map = self.event_to_message_map();
|
||||
unsubscribe_message(event_to_tool_map.document_dirty, BroadcastEvent::DocumentIsDirty);
|
||||
unsubscribe_message(event_to_tool_map.tool_abort, BroadcastEvent::ToolAbort);
|
||||
unsubscribe_message(event_to_tool_map.selection_changed, BroadcastEvent::SelectionChanged);
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ToolMetadata {
|
||||
fn icon_name(&self) -> String;
|
||||
fn tooltip(&self) -> String;
|
||||
fn tool_type(&self) -> ToolType;
|
||||
}
|
||||
|
||||
pub struct ToolData {
|
||||
pub active_tool_type: ToolType,
|
||||
pub tools: HashMap<ToolType, Box<Tool>>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for ToolData {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("ToolData").field("active_tool_type", &self.active_tool_type).field("tool_options", &"[…]").finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolData {
|
||||
pub fn active_tool_mut(&mut self) -> &mut Box<Tool> {
|
||||
self.tools.get_mut(&self.active_tool_type).expect("The active tool is not initialized")
|
||||
}
|
||||
|
||||
pub fn active_tool(&self) -> &Tool {
|
||||
self.tools.get(&self.active_tool_type).map(|x| x.as_ref()).expect("The active tool is not initialized")
|
||||
}
|
||||
}
|
||||
|
||||
impl PropertyHolder for ToolData {
|
||||
fn properties(&self) -> Layout {
|
||||
let tool_groups_layout = list_tools_in_groups()
|
||||
.iter()
|
||||
.map(|tool_group| tool_group.iter().map(|tool| ToolEntry {
|
||||
tooltip: tool.tooltip(),
|
||||
tooltip_shortcut: action_keys!(tool_type_to_activate_tool_message(tool.tool_type())),
|
||||
icon_name: tool.icon_name(),
|
||||
tool_type: tool.tool_type(),
|
||||
}).collect::<Vec<_>>())
|
||||
.chain(coming_soon_tools())
|
||||
.flat_map(|group| {
|
||||
let separator = std::iter::once(WidgetHolder::new(Widget::Separator(Separator {
|
||||
direction: SeparatorDirection::Vertical,
|
||||
separator_type: SeparatorType::Section,
|
||||
})));
|
||||
let buttons = group.into_iter().map(|ToolEntry { tooltip, tooltip_shortcut, tool_type, icon_name }| {
|
||||
WidgetHolder::new(Widget::IconButton(IconButton {
|
||||
icon: icon_name,
|
||||
size: 32,
|
||||
tooltip: tooltip.clone(),
|
||||
tooltip_shortcut,
|
||||
active: self.active_tool_type == tool_type,
|
||||
on_update: WidgetCallback::new(move |_| {
|
||||
if !tooltip.contains("Coming Soon") {
|
||||
ToolMessage::ActivateTool { tool_type }.into()
|
||||
} else {
|
||||
DialogMessage::RequestComingSoonDialog { issue: None }.into()
|
||||
}
|
||||
}),
|
||||
}))
|
||||
});
|
||||
separator.chain(buttons)
|
||||
})
|
||||
// Skip the initial separator
|
||||
.skip(1)
|
||||
.collect();
|
||||
|
||||
Layout::WidgetLayout(WidgetLayout {
|
||||
layout: vec![LayoutGroup::Column { widgets: tool_groups_layout }],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ToolEntry {
|
||||
pub tooltip: String,
|
||||
pub tooltip_shortcut: Option<ActionKeys>,
|
||||
pub icon_name: String,
|
||||
pub tool_type: ToolType,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ToolFsmState {
|
||||
pub document_tool_data: DocumentToolData,
|
||||
pub tool_data: ToolData,
|
||||
}
|
||||
|
||||
impl Default for ToolFsmState {
|
||||
fn default() -> Self {
|
||||
ToolFsmState {
|
||||
tool_data: ToolData {
|
||||
active_tool_type: ToolType::Select,
|
||||
tools: list_tools_in_groups().into_iter().flatten().map(|tool| (tool.tool_type(), tool)).collect(),
|
||||
},
|
||||
document_tool_data: DocumentToolData {
|
||||
primary_color: Color::BLACK,
|
||||
secondary_color: Color::WHITE,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolFsmState {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(usize)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum ToolType {
|
||||
// General tool group
|
||||
Select,
|
||||
Artboard,
|
||||
Navigate,
|
||||
Eyedropper,
|
||||
Fill,
|
||||
Gradient,
|
||||
|
||||
// Vector tool group
|
||||
Path,
|
||||
Pen,
|
||||
Freehand,
|
||||
Spline,
|
||||
Line,
|
||||
Rectangle,
|
||||
Ellipse,
|
||||
Shape,
|
||||
Text,
|
||||
|
||||
// Raster tool group
|
||||
Brush,
|
||||
Heal,
|
||||
Clone,
|
||||
Patch,
|
||||
Detail,
|
||||
Relight,
|
||||
}
|
||||
|
||||
/// List of all the tools in their conventional ordering and grouping.
|
||||
pub fn list_tools_in_groups() -> Vec<Vec<Box<Tool>>> {
|
||||
vec![
|
||||
vec![
|
||||
// General tool group
|
||||
Box::new(select_tool::SelectTool::default()),
|
||||
Box::new(artboard_tool::ArtboardTool::default()),
|
||||
Box::new(navigate_tool::NavigateTool::default()),
|
||||
Box::new(eyedropper_tool::EyedropperTool::default()),
|
||||
Box::new(fill_tool::FillTool::default()),
|
||||
Box::new(gradient_tool::GradientTool::default()),
|
||||
],
|
||||
vec![
|
||||
// Vector tool group
|
||||
Box::new(path_tool::PathTool::default()),
|
||||
Box::new(pen_tool::PenTool::default()),
|
||||
Box::new(freehand_tool::FreehandTool::default()),
|
||||
Box::new(spline_tool::SplineTool::default()),
|
||||
Box::new(line_tool::LineTool::default()),
|
||||
Box::new(rectangle_tool::RectangleTool::default()),
|
||||
Box::new(ellipse_tool::EllipseTool::default()),
|
||||
Box::new(shape_tool::ShapeTool::default()),
|
||||
Box::new(text_tool::TextTool::default()),
|
||||
],
|
||||
]
|
||||
}
|
||||
|
||||
pub fn coming_soon_tools() -> Vec<Vec<ToolEntry>> {
|
||||
vec![vec![
|
||||
ToolEntry {
|
||||
tool_type: ToolType::Brush,
|
||||
icon_name: "RasterBrushTool".into(),
|
||||
tooltip: "Coming Soon: Brush Tool (B)".into(),
|
||||
tooltip_shortcut: None,
|
||||
},
|
||||
ToolEntry {
|
||||
tool_type: ToolType::Heal,
|
||||
icon_name: "RasterHealTool".into(),
|
||||
tooltip: "Coming Soon: Heal Tool (J)".into(),
|
||||
tooltip_shortcut: None,
|
||||
},
|
||||
ToolEntry {
|
||||
tool_type: ToolType::Clone,
|
||||
icon_name: "RasterCloneTool".into(),
|
||||
tooltip: "Coming Soon: Clone Tool (C)".into(),
|
||||
tooltip_shortcut: None,
|
||||
},
|
||||
ToolEntry {
|
||||
tool_type: ToolType::Patch,
|
||||
icon_name: "RasterPatchTool".into(),
|
||||
tooltip: "Coming Soon: Patch Tool".into(),
|
||||
tooltip_shortcut: None,
|
||||
},
|
||||
ToolEntry {
|
||||
tool_type: ToolType::Detail,
|
||||
icon_name: "RasterDetailTool".into(),
|
||||
tooltip: "Coming Soon: Detail Tool (D)".into(),
|
||||
tooltip_shortcut: None,
|
||||
},
|
||||
ToolEntry {
|
||||
tool_type: ToolType::Relight,
|
||||
icon_name: "RasterRelightTool".into(),
|
||||
tooltip: "Coming Soon: Relight Tool (O)".into(),
|
||||
tooltip_shortcut: None,
|
||||
},
|
||||
]]
|
||||
}
|
||||
|
||||
pub fn tool_message_to_tool_type(tool_message: &ToolMessage) -> ToolType {
|
||||
match tool_message {
|
||||
// General tool group
|
||||
ToolMessage::Select(_) => ToolType::Select,
|
||||
ToolMessage::Artboard(_) => ToolType::Artboard,
|
||||
ToolMessage::Navigate(_) => ToolType::Navigate,
|
||||
ToolMessage::Eyedropper(_) => ToolType::Eyedropper,
|
||||
ToolMessage::Fill(_) => ToolType::Fill,
|
||||
ToolMessage::Gradient(_) => ToolType::Gradient,
|
||||
|
||||
// Vector tool group
|
||||
ToolMessage::Path(_) => ToolType::Path,
|
||||
ToolMessage::Pen(_) => ToolType::Pen,
|
||||
ToolMessage::Freehand(_) => ToolType::Freehand,
|
||||
ToolMessage::Spline(_) => ToolType::Spline,
|
||||
ToolMessage::Line(_) => ToolType::Line,
|
||||
ToolMessage::Rectangle(_) => ToolType::Rectangle,
|
||||
ToolMessage::Ellipse(_) => ToolType::Ellipse,
|
||||
ToolMessage::Shape(_) => ToolType::Shape,
|
||||
ToolMessage::Text(_) => ToolType::Text,
|
||||
|
||||
// Raster tool group
|
||||
// ToolMessage::Brush(_) => ToolType::Brush,
|
||||
// ToolMessage::Heal(_) => ToolType::Heal,
|
||||
// ToolMessage::Clone(_) => ToolType::Clone,
|
||||
// ToolMessage::Patch(_) => ToolType::Patch,
|
||||
// ToolMessage::Detail(_) => ToolType::Detail,
|
||||
// ToolMessage::Relight(_) => ToolType::Relight,
|
||||
_ => panic!(
|
||||
"Conversion from ToolMessage to ToolType impossible because the given ToolMessage does not have a matching ToolType. Got: {:?}",
|
||||
tool_message
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tool_type_to_activate_tool_message(tool_type: ToolType) -> ToolMessageDiscriminant {
|
||||
match tool_type {
|
||||
// General tool group
|
||||
ToolType::Select => ToolMessageDiscriminant::ActivateToolSelect,
|
||||
ToolType::Artboard => ToolMessageDiscriminant::ActivateToolArtboard,
|
||||
ToolType::Navigate => ToolMessageDiscriminant::ActivateToolNavigate,
|
||||
ToolType::Eyedropper => ToolMessageDiscriminant::ActivateToolEyedropper,
|
||||
ToolType::Fill => ToolMessageDiscriminant::ActivateToolFill,
|
||||
ToolType::Gradient => ToolMessageDiscriminant::ActivateToolGradient,
|
||||
|
||||
// Vector tool group
|
||||
ToolType::Path => ToolMessageDiscriminant::ActivateToolPath,
|
||||
ToolType::Pen => ToolMessageDiscriminant::ActivateToolPen,
|
||||
ToolType::Freehand => ToolMessageDiscriminant::ActivateToolFreehand,
|
||||
ToolType::Spline => ToolMessageDiscriminant::ActivateToolSpline,
|
||||
ToolType::Line => ToolMessageDiscriminant::ActivateToolLine,
|
||||
ToolType::Rectangle => ToolMessageDiscriminant::ActivateToolRectangle,
|
||||
ToolType::Ellipse => ToolMessageDiscriminant::ActivateToolEllipse,
|
||||
ToolType::Shape => ToolMessageDiscriminant::ActivateToolShape,
|
||||
ToolType::Text => ToolMessageDiscriminant::ActivateToolText,
|
||||
|
||||
// Raster tool group
|
||||
// ToolType::Brush => ToolMessageDiscriminant::ActivateToolBrush,
|
||||
// ToolType::Heal => ToolMessageDiscriminant::ActivateToolHeal,
|
||||
// ToolType::Clone => ToolMessageDiscriminant::ActivateToolClone,
|
||||
// ToolType::Patch => ToolMessageDiscriminant::ActivateToolPatch,
|
||||
// ToolType::Detail => ToolMessageDiscriminant::ActivateToolDetail,
|
||||
// ToolType::Relight => ToolMessageDiscriminant::ActivateToolRelight,
|
||||
_ => panic!(
|
||||
"Conversion from ToolType to ToolMessage impossible because the given ToolType does not have a matching ToolMessage. Got: {:?}",
|
||||
tool_type
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct HintData(pub Vec<HintGroup>);
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct HintGroup(pub Vec<HintInfo>);
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct HintInfo {
|
||||
/// A `KeysGroup` specifies all the keys pressed simultaneously to perform an action (like "Ctrl C" to copy).
|
||||
/// Usually at most one is given, but less commonly, multiple can be used to describe additional hotkeys not used simultaneously (like the four different arrow keys to nudge a layer).
|
||||
#[serde(rename = "keyGroups")]
|
||||
pub key_groups: Vec<KeysGroup>,
|
||||
/// `None` means that the regular `key_groups` should be used for all platforms, `Some` is an override for a Mac-only input hint.
|
||||
#[serde(rename = "keyGroupsMac")]
|
||||
pub key_groups_mac: Option<Vec<KeysGroup>>,
|
||||
/// An optional `MouseMotion` that can indicate the mouse action, like which mouse button is used and whether a drag occurs.
|
||||
/// No such icon is shown if `None` is given, and it can be combined with `key_groups` if desired.
|
||||
pub mouse: Option<MouseMotion>,
|
||||
/// The text describing what occurs with this input combination.
|
||||
pub label: String,
|
||||
/// Draws a prepended "+" symbol which indicates that this is a refinement upon a previous hint in the group.
|
||||
pub plus: bool,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tool_crash_on_layer_delete_tests {
|
||||
use crate::application::{set_uuid_seed, Editor};
|
||||
use crate::messages::portfolio::document::DocumentMessage;
|
||||
use crate::messages::tool::utility_types::ToolType;
|
||||
use crate::test_utils::EditorTestUtils;
|
||||
|
||||
use test_case::test_case;
|
||||
|
||||
#[test_case(ToolType::Pen; "while using pen tool")]
|
||||
#[test_case(ToolType::Freehand; "while using freehand tool")]
|
||||
#[test_case(ToolType::Spline; "while using spline tool")]
|
||||
#[test_case(ToolType::Line; "while using line tool")]
|
||||
#[test_case(ToolType::Rectangle; "while using rectangle tool")]
|
||||
#[test_case(ToolType::Ellipse; "while using ellipse tool")]
|
||||
#[test_case(ToolType::Shape; "while using shape tool")]
|
||||
#[test_case(ToolType::Path; "while using path tool")]
|
||||
fn should_not_crash_when_layer_is_deleted(tool: ToolType) {
|
||||
set_uuid_seed(0);
|
||||
let mut test_editor = Editor::new();
|
||||
|
||||
test_editor.select_tool(tool);
|
||||
test_editor.lmb_mousedown(0.0, 0.0);
|
||||
test_editor.move_mouse(100.0, 100.0);
|
||||
|
||||
test_editor.handle_message(DocumentMessage::DeleteSelectedLayers);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user