mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-24 11:28:13 +08:00
Add visualization overlays to G/R/S (#2195)
* Make HintInfo label use Cow<'static, str> instead of String Fixes https://discord.com/channels/731730685944922173/881073965047636018/931942323644928040 tmp, will amend later * Replaces dynamic hint used by transform layer with overlays * Adds a scaling factor accounting for document and viewport scales Also moves whole code to single unit * Make overlays relative to viewport rather than document * Add visualization overlays to G/R/S in the Select tool * Prevents quick measurements from showing up when resizing bounds Fixes https://discord.com/channels/731730685944922173/881073965047636018/1328282633456713762 * Add local axes which activates on double constraints. * Handle the bounding box of a collection of layers as select tool does * Replaced hard coded transparent color and removed debug! which slipped in * Make rotation axes start along local axis * Fix typed distance being in doc space and negatives * Fix missing undo transactions for some actions * Code review --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
co-authored by
Keavon Chambers
parent
3048466e86
commit
de36d4967d
@@ -1,8 +1,9 @@
|
||||
use crate::messages::input_mapper::utility_types::input_keyboard::Key;
|
||||
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
#[impl_message(Message, ToolMessage, TransformLayer)]
|
||||
#[derive(PartialEq, Eq, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub enum TransformLayerMessage {
|
||||
// Messages
|
||||
ApplyTransformOperation,
|
||||
@@ -12,6 +13,7 @@ pub enum TransformLayerMessage {
|
||||
CancelTransformOperation,
|
||||
ConstrainX,
|
||||
ConstrainY,
|
||||
Overlays(OverlayContext),
|
||||
PointerMove { slow_key: Key, snap_key: Key },
|
||||
SelectionChanged,
|
||||
TypeBackspace,
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
use crate::consts::SLOWING_DIVISOR;
|
||||
use crate::consts::{ANGLE_MEASURE_RADIUS_FACTOR, ARC_MEASURE_RADIUS_FACTOR_RANGE, COLOR_OVERLAY_BLUE, COLOR_OVERLAY_SNAP_BACKGROUND, COLOR_OVERLAY_WHITE, SLOWING_DIVISOR};
|
||||
use crate::messages::input_mapper::utility_types::input_mouse::ViewportPosition;
|
||||
use crate::messages::portfolio::document::overlays::utility_types::{OverlayProvider, Pivot};
|
||||
use crate::messages::portfolio::document::utility_types::transformation::{Axis, OriginalTransforms, Selected, TransformOperation, Typing};
|
||||
use crate::messages::prelude::*;
|
||||
use crate::messages::tool::common_functionality::shape_editor::ShapeState;
|
||||
use crate::messages::tool::utility_types::{ToolData, ToolType};
|
||||
|
||||
use graphene_core::renderer::Quad;
|
||||
use graphene_core::vector::ManipulatorPointId;
|
||||
|
||||
use glam::DVec2;
|
||||
use glam::{DAffine2, DVec2};
|
||||
use std::f64::consts::TAU;
|
||||
|
||||
const TRANSFORM_GRS_OVERLAY_PROVIDER: OverlayProvider = |context| TransformLayerMessage::Overlays(context).into();
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct TransformLayerMessageHandler {
|
||||
@@ -15,6 +20,8 @@ pub struct TransformLayerMessageHandler {
|
||||
|
||||
slow: bool,
|
||||
snap: bool,
|
||||
local: bool,
|
||||
fixed_bbox: Quad,
|
||||
typing: Typing,
|
||||
|
||||
mouse_position: ViewportPosition,
|
||||
@@ -30,12 +37,7 @@ impl TransformLayerMessageHandler {
|
||||
}
|
||||
|
||||
pub fn hints(&self, responses: &mut VecDeque<Message>) {
|
||||
let axis_constraint = match self.transform_operation {
|
||||
TransformOperation::Grabbing(grabbing) => grabbing.constraint,
|
||||
TransformOperation::Scaling(scaling) => scaling.constraint,
|
||||
_ => Axis::Both,
|
||||
};
|
||||
self.transform_operation.hints(self.snap, axis_constraint, responses);
|
||||
self.transform_operation.hints(responses);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,6 +101,7 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
|
||||
|
||||
selected.responses.add(DocumentMessage::StartTransaction);
|
||||
};
|
||||
let document_to_viewport = document.metadata().document_to_viewport;
|
||||
|
||||
match message {
|
||||
TransformLayerMessage::ApplyTransformOperation => {
|
||||
@@ -111,6 +114,7 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
|
||||
responses.add(DocumentMessage::EndTransaction);
|
||||
responses.add(ToolMessage::UpdateHints);
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
responses.add(OverlaysMessage::RemoveProvider(TRANSFORM_GRS_OVERLAY_PROVIDER));
|
||||
}
|
||||
TransformLayerMessage::BeginGrab => {
|
||||
if (!using_path_tool && !using_select_tool)
|
||||
@@ -126,8 +130,12 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
|
||||
begin_operation(self.transform_operation, &mut self.typing, &mut self.mouse_position, &mut self.start_mouse);
|
||||
|
||||
self.transform_operation = TransformOperation::Grabbing(Default::default());
|
||||
self.local = false;
|
||||
self.fixed_bbox = selected.bounding_box();
|
||||
|
||||
selected.original_transforms.clear();
|
||||
|
||||
responses.add(OverlaysMessage::AddProvider(TRANSFORM_GRS_OVERLAY_PROVIDER));
|
||||
}
|
||||
TransformLayerMessage::BeginRotate => {
|
||||
let selected_points: Vec<&ManipulatorPointId> = shape_editor.selected_points().collect();
|
||||
@@ -172,7 +180,12 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
|
||||
|
||||
self.transform_operation = TransformOperation::Rotating(Default::default());
|
||||
|
||||
self.local = false;
|
||||
self.fixed_bbox = selected.bounding_box();
|
||||
|
||||
selected.original_transforms.clear();
|
||||
|
||||
responses.add(OverlaysMessage::AddProvider(TRANSFORM_GRS_OVERLAY_PROVIDER));
|
||||
}
|
||||
TransformLayerMessage::BeginScale => {
|
||||
let selected_points: Vec<&ManipulatorPointId> = shape_editor.selected_points().collect();
|
||||
@@ -216,7 +229,12 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
|
||||
|
||||
self.transform_operation = TransformOperation::Scaling(Default::default());
|
||||
|
||||
self.local = false;
|
||||
self.fixed_bbox = selected.bounding_box();
|
||||
|
||||
selected.original_transforms.clear();
|
||||
|
||||
responses.add(OverlaysMessage::AddProvider(TRANSFORM_GRS_OVERLAY_PROVIDER));
|
||||
}
|
||||
TransformLayerMessage::CancelTransformOperation => {
|
||||
selected.revert_operation();
|
||||
@@ -228,21 +246,164 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
|
||||
|
||||
responses.add(DocumentMessage::AbortTransaction);
|
||||
responses.add(ToolMessage::UpdateHints);
|
||||
|
||||
responses.add(OverlaysMessage::RemoveProvider(TRANSFORM_GRS_OVERLAY_PROVIDER));
|
||||
}
|
||||
TransformLayerMessage::ConstrainX => {
|
||||
self.local = self
|
||||
.transform_operation
|
||||
.constrain_axis(Axis::X, &mut selected, self.snap, self.local, self.fixed_bbox, document_to_viewport)
|
||||
}
|
||||
TransformLayerMessage::ConstrainY => {
|
||||
self.local = self
|
||||
.transform_operation
|
||||
.constrain_axis(Axis::Y, &mut selected, self.snap, self.local, self.fixed_bbox, document_to_viewport)
|
||||
}
|
||||
TransformLayerMessage::Overlays(mut overlay_context) => {
|
||||
for layer in document.metadata().all_layers() {
|
||||
if !document.network_interface.is_artboard(&layer.to_node(), &[]) {
|
||||
continue;
|
||||
};
|
||||
|
||||
let viewport_box = input.viewport_bounds.size();
|
||||
let transform = DAffine2::from_translation(DVec2::new(0., viewport_box.y)) * DAffine2::from_scale(DVec2::splat(1.2));
|
||||
|
||||
let axis_constraint = match self.transform_operation {
|
||||
TransformOperation::Grabbing(grabbing) => grabbing.constraint,
|
||||
TransformOperation::Scaling(scaling) => scaling.constraint,
|
||||
_ => Axis::Both,
|
||||
};
|
||||
|
||||
let format_rounded = |value: f64, precision: usize| format!("{:.*}", precision, value).trim_end_matches('0').trim_end_matches('.').to_string();
|
||||
|
||||
let axis_text = |vector: DVec2, separate: bool| match (axis_constraint, separate) {
|
||||
(Axis::Both, false) => format!("by {}", format_rounded(vector.x, 3)),
|
||||
(Axis::Both, true) => format!("by ({}, {})", format_rounded(vector.x, 3), format_rounded(vector.y, 3)),
|
||||
(Axis::X, _) => format!("X by {}", format_rounded(vector.x, 3)),
|
||||
(Axis::Y, _) => format!("Y by {}", format_rounded(vector.y, 3)),
|
||||
};
|
||||
|
||||
let grs_value_text = match self.transform_operation {
|
||||
TransformOperation::None => String::new(),
|
||||
TransformOperation::Grabbing(translation) => format!(
|
||||
"Translating {}",
|
||||
axis_text(document_to_viewport.inverse().transform_vector2(translation.to_dvec(document_to_viewport)), true)
|
||||
),
|
||||
TransformOperation::Rotating(rotation) => format!("Rotating by {}°", format_rounded(rotation.to_f64(self.snap).to_degrees(), 3)),
|
||||
TransformOperation::Scaling(scale) => format!("Scaling {}", axis_text(scale.to_dvec(self.snap), false)),
|
||||
};
|
||||
|
||||
match self.transform_operation {
|
||||
TransformOperation::None => (),
|
||||
TransformOperation::Grabbing(translation) => {
|
||||
let translation = document_to_viewport.inverse().transform_vector2(translation.to_dvec(document_to_viewport));
|
||||
let vec_to_end = self.mouse_position - self.start_mouse;
|
||||
let quad = Quad::from_box([self.pivot, self.pivot + vec_to_end]).0;
|
||||
let e1 = (self.fixed_bbox.0[1] - self.fixed_bbox.0[0]).normalize();
|
||||
|
||||
if matches!(axis_constraint, Axis::Both | Axis::X) {
|
||||
let end = if self.local {
|
||||
(quad[1] - quad[0]).length() * e1 * e1.dot(quad[1] - quad[0]).signum() + quad[0]
|
||||
} else {
|
||||
quad[1]
|
||||
};
|
||||
overlay_context.line(quad[0], end, None);
|
||||
|
||||
let x_transform = DAffine2::from_translation((quad[0] + end) / 2.);
|
||||
overlay_context.text(&format_rounded(translation.x, 3), COLOR_OVERLAY_BLUE, None, x_transform, 4., [Pivot::Middle, Pivot::End]);
|
||||
}
|
||||
|
||||
if matches!(axis_constraint, Axis::Both | Axis::Y) {
|
||||
let end = if self.local {
|
||||
(quad[3] - quad[0]).length() * e1.perp() * e1.perp().dot(quad[3] - quad[0]).signum() + quad[0]
|
||||
} else {
|
||||
quad[3]
|
||||
};
|
||||
overlay_context.line(quad[0], end, None);
|
||||
let x_parameter = vec_to_end.x.clamp(-1., 1.);
|
||||
let y_transform = DAffine2::from_translation((quad[0] + end) / 2. + x_parameter * DVec2::X * 0.);
|
||||
let pivot_selection = if x_parameter > 0. {
|
||||
Pivot::Start
|
||||
} else if x_parameter == 0. {
|
||||
Pivot::Middle
|
||||
} else {
|
||||
Pivot::End
|
||||
};
|
||||
overlay_context.text(&format_rounded(translation.y, 2), COLOR_OVERLAY_BLUE, None, y_transform, 3., [pivot_selection, Pivot::Middle]);
|
||||
}
|
||||
if matches!(axis_constraint, Axis::Both) {
|
||||
overlay_context.dashed_line(quad[1], quad[2], None, Some(2.), Some(2.), Some(0.5));
|
||||
overlay_context.dashed_line(quad[3], quad[2], None, Some(2.), Some(2.), Some(0.5));
|
||||
}
|
||||
}
|
||||
TransformOperation::Scaling(scale) => {
|
||||
let scale = scale.to_f64(self.snap);
|
||||
let text = format!("{}x", format_rounded(scale, 3));
|
||||
let extension_vector = self.mouse_position - self.start_mouse;
|
||||
let local_edge = self.start_mouse - self.pivot;
|
||||
let quad = self.fixed_bbox.0;
|
||||
let local_edge = match axis_constraint {
|
||||
Axis::X => {
|
||||
if self.local {
|
||||
local_edge.project_onto(quad[1] - quad[0])
|
||||
} else {
|
||||
local_edge.with_y(0.)
|
||||
}
|
||||
}
|
||||
Axis::Y => {
|
||||
if self.local {
|
||||
local_edge.project_onto(quad[3] - quad[0])
|
||||
} else {
|
||||
local_edge.with_x(0.)
|
||||
}
|
||||
}
|
||||
_ => local_edge,
|
||||
};
|
||||
let boundary_point = local_edge + self.pivot;
|
||||
let projected_pointer = extension_vector.project_onto(local_edge);
|
||||
let dashed_till = if extension_vector.dot(local_edge) < 0. { local_edge + projected_pointer } else { local_edge };
|
||||
let lined_till = projected_pointer + boundary_point;
|
||||
if dashed_till.dot(local_edge) > 0. {
|
||||
overlay_context.dashed_line(self.pivot, self.pivot + dashed_till, None, Some(4.), Some(4.), Some(0.5));
|
||||
}
|
||||
overlay_context.line(boundary_point, lined_till, None);
|
||||
|
||||
let transform = DAffine2::from_translation(boundary_point.midpoint(self.pivot) + local_edge.perp().normalize() * local_edge.element_product().signum() * 24.);
|
||||
overlay_context.text(&text, COLOR_OVERLAY_BLUE, None, transform, 16., [Pivot::Middle, Pivot::Middle]);
|
||||
}
|
||||
TransformOperation::Rotating(rotation) => {
|
||||
let angle = rotation.to_f64(self.snap);
|
||||
let quad = self.fixed_bbox.0;
|
||||
let offset_angle = (quad[1] - quad[0]).to_angle();
|
||||
let width = viewport_box.max_element();
|
||||
let radius = self.start_mouse.distance(self.pivot);
|
||||
let arc_radius = ANGLE_MEASURE_RADIUS_FACTOR * width;
|
||||
let radius = radius.clamp(ARC_MEASURE_RADIUS_FACTOR_RANGE.0 * width, ARC_MEASURE_RADIUS_FACTOR_RANGE.1 * width);
|
||||
let text = format!("{}°", format_rounded(angle.to_degrees(), 2));
|
||||
let text_texture_width = overlay_context.get_width(&text) / 2.;
|
||||
let text_texture_height = 12.;
|
||||
let text_angle_on_unit_circle = DVec2::from_angle((angle % TAU) / 2. + offset_angle);
|
||||
let text_texture_position = DVec2::new(
|
||||
(arc_radius + 4. + text_texture_width) * text_angle_on_unit_circle.x,
|
||||
(arc_radius + text_texture_height) * text_angle_on_unit_circle.y,
|
||||
);
|
||||
let transform = DAffine2::from_translation(text_texture_position + self.pivot);
|
||||
overlay_context.draw_angle(self.pivot, radius, arc_radius, offset_angle, angle);
|
||||
overlay_context.text(&text, COLOR_OVERLAY_BLUE, None, transform, 16., [Pivot::Middle, Pivot::Middle]);
|
||||
}
|
||||
}
|
||||
|
||||
overlay_context.text(&grs_value_text, COLOR_OVERLAY_WHITE, Some(COLOR_OVERLAY_SNAP_BACKGROUND), transform, 4., [Pivot::Start, Pivot::End]);
|
||||
}
|
||||
}
|
||||
TransformLayerMessage::ConstrainX => self.transform_operation.constrain_axis(Axis::X, &mut selected, self.snap),
|
||||
TransformLayerMessage::ConstrainY => self.transform_operation.constrain_axis(Axis::Y, &mut selected, self.snap),
|
||||
TransformLayerMessage::PointerMove { slow_key, snap_key } => {
|
||||
self.slow = input.keyboard.get(slow_key as usize);
|
||||
|
||||
let new_snap = input.keyboard.get(snap_key as usize);
|
||||
if new_snap != self.snap {
|
||||
self.snap = new_snap;
|
||||
let axis_constraint = match self.transform_operation {
|
||||
TransformOperation::Grabbing(grabbing) => grabbing.constraint,
|
||||
TransformOperation::Scaling(scaling) => scaling.constraint,
|
||||
_ => Axis::Both,
|
||||
};
|
||||
self.transform_operation.apply_transform_operation(&mut selected, self.snap, axis_constraint);
|
||||
self.transform_operation
|
||||
.apply_transform_operation(&mut selected, self.snap, self.local, self.fixed_bbox, document_to_viewport);
|
||||
}
|
||||
|
||||
if self.typing.digits.is_empty() {
|
||||
@@ -252,9 +413,9 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
|
||||
TransformOperation::None => unreachable!(),
|
||||
TransformOperation::Grabbing(translation) => {
|
||||
let change = if self.slow { delta_pos / SLOWING_DIVISOR } else { delta_pos };
|
||||
let axis_constraint = translation.constraint;
|
||||
self.transform_operation = TransformOperation::Grabbing(translation.increment_amount(change));
|
||||
self.transform_operation.apply_transform_operation(&mut selected, self.snap, axis_constraint);
|
||||
self.transform_operation
|
||||
.apply_transform_operation(&mut selected, self.snap, self.local, self.fixed_bbox, document_to_viewport);
|
||||
}
|
||||
TransformOperation::Rotating(rotation) => {
|
||||
let start_offset = *selected.pivot - self.mouse_position;
|
||||
@@ -264,7 +425,8 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
|
||||
let change = if self.slow { angle / SLOWING_DIVISOR } else { angle };
|
||||
|
||||
self.transform_operation = TransformOperation::Rotating(rotation.increment_amount(change));
|
||||
self.transform_operation.apply_transform_operation(&mut selected, self.snap, Axis::Both);
|
||||
self.transform_operation
|
||||
.apply_transform_operation(&mut selected, self.snap, self.local, self.fixed_bbox, document_to_viewport);
|
||||
}
|
||||
TransformOperation::Scaling(scale) => {
|
||||
let change = {
|
||||
@@ -274,11 +436,17 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
|
||||
|
||||
(current_frame_dist - previous_frame_dist) / start_transform_dist
|
||||
};
|
||||
|
||||
let region_negate = (self.start_mouse - *selected.pivot).dot(self.mouse_position - *selected.pivot) < 0.;
|
||||
let change = if self.slow { change / SLOWING_DIVISOR } else { change };
|
||||
let axis_constraint = scale.constraint;
|
||||
let change = change * scale.dragged_factor.signum();
|
||||
self.transform_operation = TransformOperation::Scaling(scale.increment_amount(change));
|
||||
self.transform_operation.apply_transform_operation(&mut selected, self.snap, axis_constraint);
|
||||
if region_negate {
|
||||
let tmp_operation = TransformOperation::Scaling(scale.negate());
|
||||
tmp_operation.apply_transform_operation(&mut selected, self.snap, self.local, self.fixed_bbox, document_to_viewport);
|
||||
} else {
|
||||
self.transform_operation
|
||||
.apply_transform_operation(&mut selected, self.snap, self.local, self.fixed_bbox, document_to_viewport);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -289,10 +457,24 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
|
||||
let target_layers = document.network_interface.selected_nodes(&[]).unwrap().selected_layers(document.metadata()).collect();
|
||||
shape_editor.set_selected_layers(target_layers);
|
||||
}
|
||||
TransformLayerMessage::TypeBackspace => self.transform_operation.grs_typed(self.typing.type_backspace(), &mut selected, self.snap),
|
||||
TransformLayerMessage::TypeDecimalPoint => self.transform_operation.grs_typed(self.typing.type_decimal_point(), &mut selected, self.snap),
|
||||
TransformLayerMessage::TypeDigit { digit } => self.transform_operation.grs_typed(self.typing.type_number(digit), &mut selected, self.snap),
|
||||
TransformLayerMessage::TypeNegate => self.transform_operation.grs_typed(self.typing.type_negate(), &mut selected, self.snap),
|
||||
TransformLayerMessage::TypeBackspace => self
|
||||
.transform_operation
|
||||
.grs_typed(self.typing.type_backspace(), &mut selected, self.snap, self.local, self.fixed_bbox, document_to_viewport),
|
||||
TransformLayerMessage::TypeDecimalPoint => {
|
||||
self.transform_operation
|
||||
.grs_typed(self.typing.type_decimal_point(), &mut selected, self.snap, self.local, self.fixed_bbox, document_to_viewport)
|
||||
}
|
||||
TransformLayerMessage::TypeDigit { digit } => {
|
||||
self.transform_operation
|
||||
.grs_typed(self.typing.type_number(digit), &mut selected, self.snap, self.local, self.fixed_bbox, document_to_viewport)
|
||||
}
|
||||
TransformLayerMessage::TypeNegate => {
|
||||
if self.typing.digits.is_empty() {
|
||||
self.transform_operation.negate(&mut selected, self.snap, self.local, self.fixed_bbox, document_to_viewport)
|
||||
}
|
||||
self.transform_operation
|
||||
.grs_typed(self.typing.type_negate(), &mut selected, self.snap, self.local, self.fixed_bbox, document_to_viewport)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user