mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-22 11:18:12 +08:00
New overlay system (#1516)
* Port gradient tool overlays * Fix tests * Text tool * Artboard tool and some of select tool * Port select tool drawing box * Pen and path tool * Remove overlays document * Show the overlay refactor as done on the website roadmap * Select tool bounds in layer space (first layer) * Code review and fixes --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
committed by
Keavon Chambers
co-authored by
Keavon Chambers
parent
9e06e70aa2
commit
c42d030f18
@@ -1,7 +1,5 @@
|
||||
pub mod color_selector;
|
||||
pub mod graph_modification_utils;
|
||||
pub mod overlay_renderer;
|
||||
pub mod path_outline;
|
||||
pub mod pivot;
|
||||
pub mod resize;
|
||||
pub mod shape_editor;
|
||||
|
||||
@@ -1,359 +0,0 @@
|
||||
use super::shape_editor::SelectedShapeState;
|
||||
use crate::application::generate_uuid;
|
||||
use crate::consts::VIEWPORT_GRID_ROUNDING_BIAS;
|
||||
use crate::consts::{COLOR_ACCENT, HIDE_HANDLE_DISTANCE, MANIPULATOR_GROUP_MARKER_SIZE, PATH_OUTLINE_WEIGHT};
|
||||
use crate::messages::prelude::*;
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils::{get_manipulator_groups, get_subpaths};
|
||||
|
||||
use bezier_rs::ManipulatorGroup;
|
||||
use document_legacy::document::Document;
|
||||
use document_legacy::document_metadata::LayerNodeIdentifier;
|
||||
use document_legacy::layers::style::{self, Fill, Stroke};
|
||||
use document_legacy::{LayerId, Operation};
|
||||
use graphene_core::raster::color::Color;
|
||||
use graphene_core::uuid::ManipulatorGroupId;
|
||||
use graphene_core::vector::{ManipulatorPointId, SelectedType};
|
||||
|
||||
use glam::{DAffine2, DVec2};
|
||||
|
||||
/// [ManipulatorGroupOverlay]s is the collection of overlays that make up an [ManipulatorGroup] visible in the editor.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct ManipulatorGroupOverlays {
|
||||
pub anchor: Option<Vec<LayerId>>,
|
||||
pub in_handle: Option<Vec<LayerId>>,
|
||||
pub in_line: Option<Vec<LayerId>>,
|
||||
pub out_handle: Option<Vec<LayerId>>,
|
||||
pub out_line: Option<Vec<LayerId>>,
|
||||
}
|
||||
impl ManipulatorGroupOverlays {
|
||||
pub fn iter(&self) -> impl Iterator<Item = &'_ Option<Vec<LayerId>>> {
|
||||
[&self.anchor, &self.in_handle, &self.in_line, &self.out_handle, &self.out_line].into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
type GraphiteManipulatorGroup = ManipulatorGroup<ManipulatorGroupId>;
|
||||
|
||||
const POINT_STROKE_WEIGHT: f64 = 2.;
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct OverlayRenderer {
|
||||
shape_overlay_cache: HashMap<LayerNodeIdentifier, Vec<LayerId>>,
|
||||
manipulator_group_overlay_cache: HashMap<LayerNodeIdentifier, HashMap<ManipulatorGroupId, ManipulatorGroupOverlays>>,
|
||||
}
|
||||
|
||||
impl OverlayRenderer {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn query_cache(&self, layer: &LayerNodeIdentifier) -> Option<&Vec<LayerId>> {
|
||||
self.shape_overlay_cache.get(layer)
|
||||
}
|
||||
|
||||
pub fn render_subpath_overlays(&mut self, selected_shape_state: &SelectedShapeState, document: &Document, layer: LayerNodeIdentifier, responses: &mut VecDeque<Message>) {
|
||||
let transform = document.metadata.transform_to_viewport(layer);
|
||||
|
||||
let Some(subpaths) = get_subpaths(layer, document) else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.layer_overlay_visibility(document, layer, true, responses);
|
||||
|
||||
let outline_cache = self.shape_overlay_cache.get(&layer);
|
||||
trace!("Overlay: Outline cache {outline_cache:?}");
|
||||
|
||||
// Create an outline if we do not have a cached one
|
||||
if outline_cache.is_none() {
|
||||
let outline_path = self.create_shape_outline_overlay(graphene_core::vector::Subpath::from_bezier_rs(subpaths), responses);
|
||||
self.shape_overlay_cache.insert(layer, outline_path.clone());
|
||||
Self::place_outline_overlays(outline_path.clone(), &transform, responses);
|
||||
trace!("Overlay: Creating new outline {outline_path:?}");
|
||||
} else if let Some(outline_path) = outline_cache {
|
||||
trace!("Overlay: Updating overlays for {outline_path:?} owning layer: {layer:?}");
|
||||
Self::modify_outline_overlays(outline_path.clone(), graphene_core::vector::Subpath::from_bezier_rs(subpaths), responses);
|
||||
Self::place_outline_overlays(outline_path.clone(), &transform, responses);
|
||||
}
|
||||
|
||||
// Create, place, and style the manipulator overlays
|
||||
for manipulator_group in get_manipulator_groups(subpaths) {
|
||||
let manipulator_group_cache = self.manipulator_group_overlay_cache.entry(layer).or_default().entry(manipulator_group.id).or_default();
|
||||
|
||||
// Only view in and out handles if they are not on top of the anchor
|
||||
let [in_handle, out_handle] = {
|
||||
let anchor = manipulator_group.anchor;
|
||||
|
||||
let anchor_position = transform.transform_point2(anchor);
|
||||
let not_under_anchor = |&position: &DVec2| transform.transform_point2(position).distance_squared(anchor_position) >= HIDE_HANDLE_DISTANCE * HIDE_HANDLE_DISTANCE;
|
||||
let filter_handle = |manipulator: Option<DVec2>| manipulator.filter(not_under_anchor);
|
||||
[filter_handle(manipulator_group.in_handle), filter_handle(manipulator_group.out_handle)]
|
||||
};
|
||||
|
||||
// Create anchor
|
||||
manipulator_group_cache.anchor = manipulator_group_cache.anchor.take().or_else(|| Some(Self::create_anchor_overlay(responses)));
|
||||
// Create or delete in handle
|
||||
if in_handle.is_none() {
|
||||
Self::remove_overlay(manipulator_group_cache.in_handle.take(), responses);
|
||||
Self::remove_overlay(manipulator_group_cache.in_line.take(), responses);
|
||||
} else {
|
||||
manipulator_group_cache.in_handle = manipulator_group_cache.in_handle.take().or_else(|| Self::create_handle_overlay_if_exists(in_handle, responses));
|
||||
manipulator_group_cache.in_line = manipulator_group_cache.in_line.take().or_else(|| Self::create_handle_line_overlay_if_exists(in_handle, responses));
|
||||
}
|
||||
// Create or delete out handle
|
||||
if out_handle.is_none() {
|
||||
Self::remove_overlay(manipulator_group_cache.out_handle.take(), responses);
|
||||
Self::remove_overlay(manipulator_group_cache.out_line.take(), responses);
|
||||
} else {
|
||||
manipulator_group_cache.out_handle = manipulator_group_cache.out_handle.take().or_else(|| Self::create_handle_overlay_if_exists(out_handle, responses));
|
||||
manipulator_group_cache.out_line = manipulator_group_cache.out_line.take().or_else(|| Self::create_handle_line_overlay_if_exists(out_handle, responses));
|
||||
}
|
||||
|
||||
// Update placement and style
|
||||
Self::place_manipulator_group_overlays(manipulator_group, manipulator_group_cache, &transform, responses);
|
||||
Self::style_overlays(selected_shape_state, layer, manipulator_group, manipulator_group_cache, responses);
|
||||
}
|
||||
|
||||
if let Some(layer_overlays) = self.manipulator_group_overlay_cache.get_mut(&layer) {
|
||||
if layer_overlays.len() > subpaths.iter().map(|subpath| subpath.len()).sum() {
|
||||
layer_overlays.retain(|manipulator, manipulator_group_overlays| {
|
||||
if get_manipulator_groups(subpaths).any(|current_manipulator| current_manipulator.id == *manipulator) {
|
||||
true
|
||||
} else {
|
||||
Self::remove_manipulator_group_overlays(manipulator_group_overlays, responses);
|
||||
false
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
// TODO Handle removing shapes from cache so we don't memory leak
|
||||
// Eventually will get replaced with am immediate mode renderer for overlays
|
||||
|
||||
responses.add(OverlaysMessage::Rerender);
|
||||
}
|
||||
|
||||
/// Delete all cached overlays
|
||||
pub fn clear_all_overlays(&mut self, responses: &mut VecDeque<Message>) {
|
||||
for (_, overlay_path) in self.shape_overlay_cache.drain() {
|
||||
Self::remove_outline_overlays(overlay_path, responses)
|
||||
}
|
||||
for (_, layer_cache) in self.manipulator_group_overlay_cache.drain() {
|
||||
for manipulator_group_overlays in layer_cache.values() {
|
||||
Self::remove_manipulator_group_overlays(manipulator_group_overlays, responses);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn layer_overlay_visibility(&mut self, document: &Document, layer: LayerNodeIdentifier, visibility: bool, responses: &mut VecDeque<Message>) {
|
||||
// Hide the shape outline overlays
|
||||
if let Some(overlay_path) = self.shape_overlay_cache.get(&layer) {
|
||||
Self::set_outline_overlay_visibility(overlay_path.clone(), visibility, responses);
|
||||
}
|
||||
|
||||
// Hide the manipulator group overlays
|
||||
let Some(manipulator_groups) = self.manipulator_group_overlay_cache.get(&layer) else { return };
|
||||
if visibility {
|
||||
let Some(subpaths) = get_subpaths(layer, document) else { return };
|
||||
for manipulator_group in get_manipulator_groups(subpaths) {
|
||||
let id = manipulator_group.id;
|
||||
if let Some(manipulator_group_overlays) = manipulator_groups.get(&id) {
|
||||
Self::set_manipulator_group_overlay_visibility(manipulator_group_overlays, visibility, responses);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for manipulator_group_overlays in manipulator_groups.values() {
|
||||
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: graphene_core::vector::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(Some(COLOR_ACCENT), PATH_OUTLINE_WEIGHT)), Fill::None),
|
||||
insert_index: -1,
|
||||
transform: DAffine2::IDENTITY.to_cols_array(),
|
||||
};
|
||||
responses.add(DocumentMessage::Overlays(operation.into()));
|
||||
|
||||
layer_path
|
||||
}
|
||||
|
||||
/// Create a single anchor overlay and return its layer ID.
|
||||
fn create_anchor_overlay(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(Some(COLOR_ACCENT), 2.0)), Fill::solid(Color::WHITE)),
|
||||
insert_index: -1,
|
||||
};
|
||||
responses.add(DocumentMessage::Overlays(operation.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(Some(COLOR_ACCENT), 2.0)), Fill::solid(Color::WHITE)),
|
||||
insert_index: -1,
|
||||
};
|
||||
responses.add(DocumentMessage::Overlays(operation.into()));
|
||||
layer_path
|
||||
}
|
||||
|
||||
/// Create a single handle overlay and return its layer id if it exists.
|
||||
fn create_handle_overlay_if_exists(handle: Option<DVec2>, responses: &mut VecDeque<Message>) -> Option<Vec<LayerId>> {
|
||||
handle.map(|_| Self::create_handle_overlay(responses))
|
||||
}
|
||||
|
||||
/// Remove an overlay at the specified path
|
||||
fn remove_overlay(path: Option<Vec<LayerId>>, responses: &mut VecDeque<Message>) {
|
||||
if let Some(path) = path {
|
||||
responses.add(DocumentMessage::Overlays(Operation::DeleteLayer { path }.into()));
|
||||
}
|
||||
}
|
||||
|
||||
/// 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(Some(COLOR_ACCENT), 1.0)), Fill::None),
|
||||
insert_index: -1,
|
||||
};
|
||||
responses.add_front(DocumentMessage::Overlays(operation.into()));
|
||||
layer_path
|
||||
}
|
||||
|
||||
/// Create the shape outline overlay and return its layer ID.
|
||||
fn create_handle_line_overlay_if_exists(handle: Option<DVec2>, 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.add(transform_message);
|
||||
}
|
||||
|
||||
fn modify_outline_overlays(outline_path: Vec<LayerId>, subpath: graphene_core::vector::Subpath, responses: &mut VecDeque<Message>) {
|
||||
let outline_modify_message = Self::overlay_modify_message(outline_path, subpath);
|
||||
responses.add(outline_modify_message);
|
||||
}
|
||||
|
||||
/// Updates the position of the overlays based on the [Subpath] points.
|
||||
fn place_manipulator_group_overlays(manipulator_group: &GraphiteManipulatorGroup, overlays: &mut ManipulatorGroupOverlays, parent_transform: &DAffine2, responses: &mut VecDeque<Message>) {
|
||||
let anchor = manipulator_group.anchor;
|
||||
|
||||
let mut place_handle_and_line = |handle_position: DVec2, line_overlay: &[LayerId], marker_source: &mut Option<Vec<LayerId>>| {
|
||||
let line_vector = parent_transform.transform_point2(anchor) - 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.add(Self::overlay_transform_message(line_overlay.to_vec(), transform));
|
||||
|
||||
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.add(Self::overlay_transform_message(marker_overlay.clone(), transform));
|
||||
|
||||
*marker_source = Some(marker_overlay);
|
||||
};
|
||||
|
||||
// Place the handle overlays
|
||||
if let (Some(handle_position), Some(line_overlay)) = (manipulator_group.in_handle, overlays.in_line.as_mut()) {
|
||||
place_handle_and_line(handle_position, line_overlay, &mut overlays.in_handle);
|
||||
}
|
||||
if let (Some(handle_position), Some(line_overlay)) = (manipulator_group.out_handle, overlays.out_line.as_ref()) {
|
||||
place_handle_and_line(handle_position, line_overlay, &mut overlays.out_handle);
|
||||
}
|
||||
|
||||
// Place the anchor point overlay
|
||||
if let Some(anchor_overlay) = &overlays.anchor {
|
||||
let scale = DVec2::splat(MANIPULATOR_GROUP_MARKER_SIZE);
|
||||
let angle = 0.;
|
||||
let translation = (parent_transform.transform_point2(anchor) - (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.add(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| {
|
||||
trace!("Overlay: Sending delete message for: {layer_id:?}");
|
||||
responses.add(DocumentMessage::Overlays(Operation::DeleteLayer { path: layer_id.clone() }.into()));
|
||||
});
|
||||
}
|
||||
|
||||
fn remove_outline_overlays(overlay_path: Vec<LayerId>, responses: &mut VecDeque<Message>) {
|
||||
responses.add(DocumentMessage::Overlays(Operation::DeleteLayer { path: overlay_path }.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.add(Self::overlay_visibility_message(layer_id.clone(), visibility));
|
||||
});
|
||||
}
|
||||
|
||||
fn set_outline_overlay_visibility(overlay_path: Vec<LayerId>, visibility: bool, responses: &mut VecDeque<Message>) {
|
||||
responses.add(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: graphene_core::vector::Subpath) -> Message {
|
||||
DocumentMessage::Overlays(Operation::SetShapePath { path: layer_path, subpath }.into()).into()
|
||||
}
|
||||
|
||||
/// Sets the overlay style for this point.
|
||||
fn style_overlays(state: &SelectedShapeState, layer: LayerNodeIdentifier, manipulator_group: &GraphiteManipulatorGroup, 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(Some(COLOR_ACCENT), POINT_STROKE_WEIGHT + 1.0)), Fill::solid(COLOR_ACCENT));
|
||||
let deselected_style = style::PathStyle::new(Some(Stroke::new(Some(COLOR_ACCENT), POINT_STROKE_WEIGHT)), Fill::solid(Color::WHITE));
|
||||
let selected_shape_state = state.get(&layer);
|
||||
// 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, overlay) in [&overlays.in_handle, &overlays.out_handle, &overlays.anchor].into_iter().enumerate() {
|
||||
let selected_type = [SelectedType::InHandle, SelectedType::OutHandle, SelectedType::Anchor][index];
|
||||
if let Some(overlay_path) = overlay {
|
||||
let selected = selected_shape_state
|
||||
.filter(|state| state.is_selected(ManipulatorPointId::new(manipulator_group.id, selected_type)))
|
||||
.is_some();
|
||||
|
||||
let style = if selected { selected_style.clone() } else { deselected_style.clone() };
|
||||
responses.add(DocumentMessage::Overlays(Operation::SetLayerStyle { path: overlay_path.clone(), style }.into()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
use crate::application::generate_uuid;
|
||||
use crate::consts::{COLOR_ACCENT, PATH_OUTLINE_WEIGHT};
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
use document_legacy::document_metadata::LayerNodeIdentifier;
|
||||
use document_legacy::layers::style::{self, Fill, Stroke};
|
||||
use document_legacy::{LayerId, Operation};
|
||||
|
||||
use glam::DAffine2;
|
||||
|
||||
/// 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<LayerNodeIdentifier>,
|
||||
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 try_create_outline(layer: LayerNodeIdentifier, overlay_path: Option<Vec<LayerId>>, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) -> Option<Vec<LayerId>> {
|
||||
let subpath = document.metadata().layer_outline(layer);
|
||||
let transform = document.metadata().transform_to_viewport(layer);
|
||||
|
||||
// Generate a new overlay layer if necessary
|
||||
let overlay = overlay_path.unwrap_or_else(|| {
|
||||
let overlay_path = vec![generate_uuid()];
|
||||
|
||||
responses.add(DocumentMessage::Overlays(
|
||||
(Operation::AddShape {
|
||||
path: overlay_path.clone(),
|
||||
subpath: Default::default(),
|
||||
style: style::PathStyle::new(Some(Stroke::new(Some(COLOR_ACCENT), PATH_OUTLINE_WEIGHT)), Fill::None),
|
||||
insert_index: -1,
|
||||
transform: DAffine2::IDENTITY.to_cols_array(),
|
||||
})
|
||||
.into(),
|
||||
));
|
||||
|
||||
overlay_path
|
||||
});
|
||||
|
||||
// Update the shape subpath
|
||||
responses.add(DocumentMessage::Overlays((Operation::SetShapePath { path: overlay.clone(), subpath }).into()));
|
||||
|
||||
// Update the transform to match the document
|
||||
responses.add(DocumentMessage::Overlays(
|
||||
(Operation::SetLayerTransform {
|
||||
path: overlay.clone(),
|
||||
transform: transform.to_cols_array(),
|
||||
})
|
||||
.into(),
|
||||
));
|
||||
|
||||
Some(overlay)
|
||||
}
|
||||
|
||||
/// Creates an outline of a layer either with a pre-existing overlay or by generating a new one.
|
||||
///
|
||||
/// Creates an outline, discarding the overlay on failure.
|
||||
fn create_outline(layer: LayerNodeIdentifier, overlay_path: Option<Vec<LayerId>>, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) -> Option<Vec<LayerId>> {
|
||||
let copied_overlay_path = overlay_path.clone();
|
||||
let result = Self::try_create_outline(layer, overlay_path, document, responses);
|
||||
if result.is_none() {
|
||||
// Discard the overlay layer if it exists
|
||||
if let Some(overlay_path) = copied_overlay_path {
|
||||
let operation = Operation::DeleteLayer { path: overlay_path };
|
||||
responses.add(DocumentMessage::Overlays(operation.into()));
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// 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.add(DocumentMessage::Overlays(operation.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>) {
|
||||
// Get the layer the user is hovering over
|
||||
let intersection = document.document_legacy.click(input.mouse.position, &document.document_legacy.document_network);
|
||||
|
||||
let Some(hovered_layer) = intersection else {
|
||||
self.clear_hovered(responses);
|
||||
return;
|
||||
};
|
||||
|
||||
if document.metadata().selected_layers_contains(hovered_layer) {
|
||||
self.clear_hovered(responses);
|
||||
return;
|
||||
}
|
||||
|
||||
// Updates the overlay, generating a new one if necessary
|
||||
self.hovered_overlay_path = Self::create_outline(hovered_layer, self.hovered_overlay_path.take(), document, responses);
|
||||
if self.hovered_overlay_path.is_none() {
|
||||
self.clear_hovered(responses);
|
||||
}
|
||||
|
||||
self.hovered_layer_path = Some(hovered_layer);
|
||||
}
|
||||
|
||||
/// 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.add(DocumentMessage::Overlays(operation.into()));
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the selected overlays, generating or removing overlays if necessary
|
||||
pub fn update_selected(&mut self, selected: impl Iterator<Item = LayerNodeIdentifier>, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
||||
let mut old_overlay_paths = std::mem::take(&mut self.selected_overlay_paths);
|
||||
|
||||
for layer_identifier in selected {
|
||||
if let Some(overlay_path) = Self::create_outline(layer_identifier, old_overlay_paths.pop(), document, responses) {
|
||||
self.selected_overlay_paths.push(overlay_path);
|
||||
}
|
||||
}
|
||||
for path in old_overlay_paths {
|
||||
let operation = Operation::DeleteLayer { path };
|
||||
responses.add(DocumentMessage::Overlays(operation.into()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,16 @@
|
||||
//! Handler for the pivot overlay visible on the selected layer(s) whilst using the Select tool which controls the center of rotation/scale and origin of the layer.
|
||||
|
||||
use crate::application::generate_uuid;
|
||||
use crate::consts::{COLOR_ACCENT, PIVOT_INNER, PIVOT_OUTER, PIVOT_OUTER_OUTLINE_THICKNESS};
|
||||
use super::graph_modification_utils;
|
||||
use crate::consts::PIVOT_OUTER;
|
||||
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
use document_legacy::document_metadata::LayerNodeIdentifier;
|
||||
use document_legacy::layers::style;
|
||||
use document_legacy::{LayerId, Operation};
|
||||
|
||||
use glam::{DAffine2, DVec2};
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use super::graph_modification_utils;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Pivot {
|
||||
/// Pivot between (0,0) and (1,1)
|
||||
@@ -22,8 +19,6 @@ pub struct Pivot {
|
||||
transform_from_normalized: DAffine2,
|
||||
/// The viewspace pivot position (if applicable)
|
||||
pivot: Option<DVec2>,
|
||||
/// A reference to the previous overlays so we can destroy them
|
||||
pivot_overlay_circles: Option<[Vec<LayerId>; 2]>,
|
||||
/// The old pivot position in the GUI, used to reduce refreshes of the document bar
|
||||
old_pivot_position: PivotPosition,
|
||||
}
|
||||
@@ -34,7 +29,6 @@ impl Default for Pivot {
|
||||
normalized_pivot: DVec2::splat(0.5),
|
||||
transform_from_normalized: Default::default(),
|
||||
pivot: Default::default(),
|
||||
pivot_overlay_circles: Default::default(),
|
||||
old_pivot_position: PivotPosition::Center,
|
||||
}
|
||||
}
|
||||
@@ -87,59 +81,11 @@ impl Pivot {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear_overlays(&mut self, responses: &mut VecDeque<Message>) {
|
||||
if let Some(overlays) = self.pivot_overlay_circles.take() {
|
||||
for path in overlays {
|
||||
responses.add(DocumentMessage::Overlays(Operation::DeleteLayer { path }.into()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn redraw_pivot(&mut self, responses: &mut VecDeque<Message>) {
|
||||
self.clear_overlays(responses);
|
||||
|
||||
let pivot = match self.pivot {
|
||||
Some(pivot) => pivot,
|
||||
None => return,
|
||||
};
|
||||
|
||||
let layer_paths = [vec![generate_uuid()], vec![generate_uuid()]];
|
||||
responses.add(DocumentMessage::Overlays(
|
||||
Operation::AddEllipse {
|
||||
path: layer_paths[0].clone(),
|
||||
transform: DAffine2::IDENTITY.to_cols_array(),
|
||||
style: style::PathStyle::new(
|
||||
Some(style::Stroke::new(Some(COLOR_ACCENT), PIVOT_OUTER_OUTLINE_THICKNESS)),
|
||||
style::Fill::Solid(graphene_core::raster::color::Color::WHITE),
|
||||
),
|
||||
insert_index: -1,
|
||||
}
|
||||
.into(),
|
||||
));
|
||||
responses.add(DocumentMessage::Overlays(
|
||||
Operation::AddEllipse {
|
||||
path: layer_paths[1].clone(),
|
||||
transform: DAffine2::IDENTITY.to_cols_array(),
|
||||
style: style::PathStyle::new(None, style::Fill::Solid(COLOR_ACCENT)),
|
||||
insert_index: -1,
|
||||
}
|
||||
.into(),
|
||||
));
|
||||
|
||||
self.pivot_overlay_circles = Some(layer_paths.clone());
|
||||
let [outer, inner] = layer_paths;
|
||||
|
||||
let pivot_diameter_without_outline = PIVOT_OUTER - PIVOT_OUTER_OUTLINE_THICKNESS;
|
||||
let transform = DAffine2::from_scale_angle_translation(DVec2::splat(pivot_diameter_without_outline), 0., pivot - DVec2::splat(pivot_diameter_without_outline / 2.)).to_cols_array();
|
||||
responses.add(DocumentMessage::Overlays(Operation::TransformLayerInViewport { path: outer, transform }.into()));
|
||||
|
||||
let transform = DAffine2::from_scale_angle_translation(DVec2::splat(PIVOT_INNER), 0., pivot - DVec2::splat(PIVOT_INNER / 2.)).to_cols_array();
|
||||
responses.add(DocumentMessage::Overlays(Operation::TransformLayerInViewport { path: inner, transform }.into()));
|
||||
}
|
||||
|
||||
pub fn update_pivot(&mut self, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
||||
pub fn update_pivot(&mut self, document: &DocumentMessageHandler, overlay_context: &mut OverlayContext) {
|
||||
self.recalculate_pivot(document);
|
||||
self.redraw_pivot(responses);
|
||||
if let Some(pivot) = self.pivot {
|
||||
overlay_context.pivot(pivot);
|
||||
}
|
||||
}
|
||||
|
||||
/// Answers if the pivot widget has changed (so we should refresh the tool bar at the top of the canvas).
|
||||
|
||||
@@ -1,169 +1,19 @@
|
||||
use super::shape_editor::ManipulatorPointInfo;
|
||||
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::consts::{SNAP_AXIS_TOLERANCE, SNAP_POINT_TOLERANCE};
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
use document_legacy::document_metadata::LayerNodeIdentifier;
|
||||
use document_legacy::layers::layer_info::LegacyLayer;
|
||||
use document_legacy::layers::style::{self, Stroke};
|
||||
use document_legacy::{LayerId, Operation};
|
||||
use document_legacy::LayerId;
|
||||
use graphene_core::vector::{ManipulatorPointId, SelectedType};
|
||||
|
||||
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.add(DocumentMessage::Overlays(
|
||||
if is_axis {
|
||||
Operation::AddLine {
|
||||
path: layer_path.clone(),
|
||||
transform,
|
||||
style: style::PathStyle::new(Some(Stroke::new(Some(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(),
|
||||
));
|
||||
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.add(DocumentMessage::Overlays(Operation::SetLayerTransform { path: layer_path.clone(), transform }.into()));
|
||||
layer_path
|
||||
};
|
||||
|
||||
// Then set its opacity to the fade amount
|
||||
if let Some(opacity) = opacity {
|
||||
responses.add(DocumentMessage::Overlays(Operation::SetLayerOpacity { path: layer_path, opacity }.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, snapped_to_point: bool)
|
||||
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;
|
||||
if !snapped_to_point {
|
||||
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.add(DocumentMessage::Overlays(Operation::DeleteLayer { path: overlay_paths.pop().unwrap() }.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);
|
||||
}
|
||||
}
|
||||
use glam::DVec2;
|
||||
|
||||
/// 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,
|
||||
}
|
||||
@@ -193,7 +43,7 @@ impl SnapManager {
|
||||
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, snapped_to_point) = if let Some(min_points) = min_points.filter(|&(_, _, dist)| dist <= SNAP_POINT_TOLERANCE) {
|
||||
let (clamped_closest_distance, _snapped_to_point) = if let Some(min_points) = min_points.filter(|&(_, _, dist)| dist <= SNAP_POINT_TOLERANCE) {
|
||||
(min_points.1, true)
|
||||
} else {
|
||||
// Do not move if over snap tolerance
|
||||
@@ -206,8 +56,7 @@ impl SnapManager {
|
||||
false,
|
||||
)
|
||||
};
|
||||
|
||||
self.snap_overlays.update_overlays(responses, (x_axis, y_axis, points), clamped_closest_distance, snapped_to_point);
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
|
||||
clamped_closest_distance
|
||||
}
|
||||
@@ -336,9 +185,9 @@ impl SnapManager {
|
||||
|
||||
/// 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;
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
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::consts::{BOUNDS_ROTATE_THRESHOLD, BOUNDS_SELECT_THRESHOLD, SELECTION_DRAG_ANGLE};
|
||||
use crate::messages::frontend::utility_types::MouseCursorIcon;
|
||||
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
|
||||
use crate::messages::portfolio::document::utility_types::transformation::OriginalTransforms;
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
use document_legacy::layers::style::{self, Fill, Stroke};
|
||||
use document_legacy::LayerId;
|
||||
use document_legacy::Operation;
|
||||
use graphene_core::raster::color::Color;
|
||||
use graphene_core::renderer::Quad;
|
||||
|
||||
use glam::{DAffine2, DVec2};
|
||||
|
||||
@@ -149,71 +146,6 @@ impl SelectedEdges {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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(Some(COLOR_ACCENT), 1.0)), Fill::None),
|
||||
insert_index: -1,
|
||||
};
|
||||
responses.add(DocumentMessage::Overlays(operation.into()));
|
||||
|
||||
path
|
||||
}
|
||||
|
||||
/// Update the location of a bounding box with no handles
|
||||
pub fn update_bounding_box(pos1: DVec2, pos2: DVec2, layer: &Option<Vec<LayerId>>, responses: &mut VecDeque<Message>) {
|
||||
if let Some(path) = layer.as_ref().cloned() {
|
||||
let transform = transform_from_box(pos1, pos2, DAffine2::IDENTITY).to_cols_array();
|
||||
let operation = Operation::SetLayerTransformInViewport { path, transform };
|
||||
responses.add_front(DocumentMessage::Overlays(operation.into()));
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes the bounding box overlay with no transform handles
|
||||
pub fn remove_bounding_box(layer_path: Option<Vec<LayerId>>, responses: &mut VecDeque<Message>) {
|
||||
if let Some(path) = layer_path {
|
||||
let operation = Operation::DeleteLayer { path };
|
||||
responses.add(DocumentMessage::Overlays(operation.into()));
|
||||
}
|
||||
}
|
||||
|
||||
/// 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(Some(COLOR_ACCENT), 2.0)), Fill::solid(Color::WHITE)),
|
||||
insert_index: -1,
|
||||
};
|
||||
responses.add(DocumentMessage::Overlays(operation.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 {
|
||||
@@ -229,27 +161,17 @@ pub fn axis_align_drag(axis_align: bool, position: DVec2, start: DVec2) -> DVec2
|
||||
|
||||
/// 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 struct BoundingBoxManager {
|
||||
pub bounds: [DVec2; 2],
|
||||
pub transform: DAffine2,
|
||||
pub original_bound_transform: DAffine2,
|
||||
pub selected_edges: Option<SelectedEdges>,
|
||||
pub original_transforms: OriginalTransforms,
|
||||
pub opposite_pivot: DVec2,
|
||||
pub center_of_transformation: 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()
|
||||
}
|
||||
}
|
||||
|
||||
impl BoundingBoxManager {
|
||||
/// 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();
|
||||
@@ -267,20 +189,11 @@ impl BoundingBoxOverlays {
|
||||
}
|
||||
|
||||
/// 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.add(DocumentMessage::Overlays(Operation::SetLayerTransformInViewport { path, transform }.into()));
|
||||
pub fn render_overlays(&mut self, overlay_context: &mut OverlayContext) {
|
||||
overlay_context.quad(self.transform * Quad::from_box(self.bounds));
|
||||
|
||||
// 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.add(DocumentMessage::Overlays(Operation::SetLayerTransformInViewport { path, transform }.into()));
|
||||
for position in self.evaluate_transform_handle_positions() {
|
||||
overlay_context.square(position, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -355,14 +268,4 @@ impl BoundingBoxOverlays {
|
||||
MouseCursorIcon::Default
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes the overlays
|
||||
pub fn delete(self, responses: &mut VecDeque<Message>) {
|
||||
responses.add(DocumentMessage::Overlays(Operation::DeleteLayer { path: self.bounding_box }.into()));
|
||||
responses.extend(
|
||||
self.transform_handles
|
||||
.iter()
|
||||
.map(|path| DocumentMessage::Overlays(Operation::DeleteLayer { path: path.clone() }.into()).into()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user