mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-21 23:48:12 +08:00
Polish the G/R/S feature behavior, visualizations, and hints (#2229)
* Fix GRS overlays bugs
- [ ] Another change: when typing values, we shouldn't round the number to 2 decimal places, instead it should show the exact number the user types, including trailing zeros if the user types those.
- [ ] For Scale, the solid vs. dashed line that follows the cursor is following the projected location of the cursor onto that line. It should not be doing that, instead it should be displaying the actual scale ratio. 0 means at the center. 1 means at the starting point of the Sscale. 0.5 means half is solid, half is dashed. Notice in my video below, I'm able to make the solid line's end go all the way to the center, which should occur at a scale factor of 0, without ever having the scale factor even go below 0.5. Because currently the two values aren't related. Furthermore, this is necessary to visualize snapping when Ctrl is pressed and to properly handle slowing when Shift is pressed.
https://files.keavon.com/-/QuirkyYummyGrayfox/capture_38_.mp4
- [ ] Implement integer-value snapping for Grab when Ctrl is pressed? It should work whether we're freely moving or constrained to X or Y (but still mouse controlled; Ctrl doesn't affect typed numbers). So when Ctrl is held, we shouldn't see decimal values. You'll need to zoom in past 100% to see decimal values.
- [ ] We don't enter G, R, or S mode until after pressing one of those keys and then moving the mouse cursor. It should happen immediately upon pressing the key, before needing to move the cursor.
- [ ] The hints need to be updated to work dynamically with the state of global vs. local X and Y constraint, and include - negation, numbers, and Backspace if numbers are typed.
- [ ] In the Path tool, we actually do want the Rotation to start from the direction of the cursor rather than the local rotation of the layer. So if you can undo that change specifically for the Path tool, but keep it for the Select tool, that would be ideal.
- [ ] When G is constrained to Y in both Path and Select tools, the number label shouldn't swap sides based on moving the mouse left and right offset by the pivot-to-start-point X offset distance. Alternatively, you could keep this behavior but make it swap to the side that the mouse is currently on (removing the pivot-to-start-point X offset from the current mouse position). When solving this, just be sure you don't break the correct behavior for G when unconstrained to an axis, since we like how the Y component label swaps sides. Please also double-check that you're not drawing any extra (overlapping) overlay lines than the necessary 1 when in X or Y constraint mode, or when X or Y happen to be precisely 0. https://files.keavon.com/-/EachWeeArcticseal/capture_39_.mp4
- [ ] Remove the bottom-left viewport overlay labels for the G/R/S status
* Remove bottomleft GRS overlay
* Fix backspace minus
* Implement integer-value snapping for Grab when Ctrl
* Use built-in coordinates for translation
* Show the exact value in Typing including trailing zeros
* Make rotation overlay start along the cursor rather than the local rotation of the layer in path tool
* Properly handle slow down and other effects on scale
* Start the transformation overlay immediately rather than after first change
* Hints for typing and global/local axes
* Prevent drawing overlapping if X or Y is 0
* Fix number being replaced by - when in negated transformation
* Fix fixes
* Invert scale delta in inverted mode
* Code review
---------
Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
co-authored by
Keavon Chambers
parent
303c1d45f8
commit
fb7eae8f02
@@ -7,6 +7,9 @@ use glam::DVec2;
|
||||
#[impl_message(Message, ToolMessage, TransformLayer)]
|
||||
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub enum TransformLayerMessage {
|
||||
// Overlays
|
||||
Overlays(OverlayContext),
|
||||
|
||||
// Messages
|
||||
ApplyTransformOperation,
|
||||
BeginGrab,
|
||||
@@ -18,8 +21,7 @@ pub enum TransformLayerMessage {
|
||||
CancelTransformOperation,
|
||||
ConstrainX,
|
||||
ConstrainY,
|
||||
Overlays(OverlayContext),
|
||||
PointerMove { slow_key: Key, snap_key: Key },
|
||||
PointerMove { slow_key: Key, increments_key: Key },
|
||||
SelectionChanged,
|
||||
TypeBackspace,
|
||||
TypeDecimalPoint,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::consts::{ANGLE_MEASURE_RADIUS_FACTOR, ARC_MEASURE_RADIUS_FACTOR_RANGE, COLOR_OVERLAY_BLUE, COLOR_OVERLAY_LABEL_BACKGROUND, COLOR_OVERLAY_WHITE, SLOWING_DIVISOR};
|
||||
use crate::consts::{ANGLE_MEASURE_RADIUS_FACTOR, ARC_MEASURE_RADIUS_FACTOR_RANGE, COLOR_OVERLAY_BLUE, 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};
|
||||
@@ -16,12 +16,16 @@ use std::f64::consts::TAU;
|
||||
|
||||
const TRANSFORM_GRS_OVERLAY_PROVIDER: OverlayProvider = |context| TransformLayerMessage::Overlays(context).into();
|
||||
|
||||
// TODO: Get these from the input mapper
|
||||
const SLOW_KEY: Key = Key::Shift;
|
||||
const INCREMENTS_KEY: Key = Key::Control;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct TransformLayerMessageHandler {
|
||||
pub transform_operation: TransformOperation,
|
||||
|
||||
slow: bool,
|
||||
snap: bool,
|
||||
increments: bool,
|
||||
local: bool,
|
||||
fixed_bbox: Quad,
|
||||
typing: Typing,
|
||||
@@ -45,7 +49,7 @@ impl TransformLayerMessageHandler {
|
||||
}
|
||||
|
||||
pub fn hints(&self, responses: &mut VecDeque<Message>) {
|
||||
self.transform_operation.hints(responses);
|
||||
self.transform_operation.hints(responses, self.local);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,6 +78,27 @@ fn calculate_pivot(selected_points: &Vec<&ManipulatorPointId>, vector_data: &Vec
|
||||
}
|
||||
}
|
||||
|
||||
fn project_edge_to_quad(edge: DVec2, quad: &Quad, local: bool, axis_constraint: Axis) -> DVec2 {
|
||||
let quad = quad.0;
|
||||
match axis_constraint {
|
||||
Axis::X => {
|
||||
if local {
|
||||
edge.project_onto(quad[1] - quad[0])
|
||||
} else {
|
||||
edge.with_y(0.)
|
||||
}
|
||||
}
|
||||
Axis::Y => {
|
||||
if local {
|
||||
edge.project_onto(quad[3] - quad[0])
|
||||
} else {
|
||||
edge.with_x(0.)
|
||||
}
|
||||
}
|
||||
_ => edge,
|
||||
}
|
||||
}
|
||||
|
||||
type TransformData<'a> = (&'a DocumentMessageHandler, &'a InputPreprocessorMessageHandler, &'a ToolData, &'a mut ShapeState);
|
||||
impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayerMessageHandler {
|
||||
fn process_message(&mut self, message: TransformLayerMessage, responses: &mut VecDeque<Message>, (document, input, tool_data, shape_editor): TransformData) {
|
||||
@@ -144,6 +169,121 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
|
||||
let document_to_viewport = document.metadata().document_to_viewport;
|
||||
|
||||
match message {
|
||||
// Overlays
|
||||
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 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| {
|
||||
if self.typing.digits.is_empty() {
|
||||
format!("{:.*}", precision, value).trim_end_matches('0').trim_end_matches('.').to_string()
|
||||
} else {
|
||||
self.typing.string.clone()
|
||||
}
|
||||
};
|
||||
|
||||
match self.transform_operation {
|
||||
TransformOperation::None => (),
|
||||
TransformOperation::Grabbing(translation) => {
|
||||
let translation = translation.to_dvec(document_to_viewport, self.increments);
|
||||
let viewport_translate = document_to_viewport.transform_vector2(translation);
|
||||
let quad = Quad::from_box([self.grab_target, self.grab_target + viewport_translate]).0;
|
||||
let e1 = (self.fixed_bbox.0[1] - self.fixed_bbox.0[0]).normalize();
|
||||
|
||||
if matches!(axis_constraint, Axis::Both | Axis::X) && translation.x != 0. {
|
||||
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) && translation.y != 0. {
|
||||
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 = viewport_translate.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 { Pivot::End };
|
||||
if axis_constraint != Axis::Both || self.typing.digits.is_empty() {
|
||||
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) && translation.x != 0. && translation.y != 0. {
|
||||
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 to_mouse_final = self.mouse_position - self.pivot;
|
||||
let to_mouse_start = self.start_mouse - self.pivot;
|
||||
|
||||
let to_mouse_final = project_edge_to_quad(to_mouse_final, &self.fixed_bbox, self.local, axis_constraint);
|
||||
let to_mouse_start = project_edge_to_quad(to_mouse_start, &self.fixed_bbox, self.local, axis_constraint);
|
||||
|
||||
let scale = scale.to_f64(self.increments) * to_mouse_final.dot(to_mouse_start).signum();
|
||||
let text = format!("{}x", format_rounded(scale, 3));
|
||||
let local_edge = self.start_mouse - self.pivot;
|
||||
let local_edge = project_edge_to_quad(local_edge, &self.fixed_bbox, self.local, axis_constraint);
|
||||
let boundary_point = self.pivot + local_edge * scale.min(1.);
|
||||
let end_point = self.pivot + local_edge * scale.max(1.);
|
||||
|
||||
if scale > 0. {
|
||||
overlay_context.dashed_line(self.pivot, boundary_point, None, Some(4.), Some(4.), Some(0.5));
|
||||
}
|
||||
overlay_context.line(boundary_point, end_point, 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.increments);
|
||||
let quad = self.fixed_bbox.0;
|
||||
let offset_angle = if self.grs_pen_handle {
|
||||
self.handle - self.last_point
|
||||
} else if using_path_tool {
|
||||
self.start_mouse - self.pivot
|
||||
} else {
|
||||
quad[1] - quad[0]
|
||||
};
|
||||
let offset_angle = offset_angle.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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Messages
|
||||
TransformLayerMessage::ApplyTransformOperation => {
|
||||
selected.original_transforms.clear();
|
||||
self.typing.clear();
|
||||
@@ -190,6 +330,11 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
|
||||
};
|
||||
|
||||
responses.add(OverlaysMessage::AddProvider(TRANSFORM_GRS_OVERLAY_PROVIDER));
|
||||
// Find a way better than this hack
|
||||
responses.add(TransformLayerMessage::PointerMove {
|
||||
slow_key: SLOW_KEY,
|
||||
increments_key: INCREMENTS_KEY,
|
||||
});
|
||||
}
|
||||
TransformLayerMessage::BeginGrab => {
|
||||
if (!using_path_tool && !using_select_tool && !using_pen_tool)
|
||||
@@ -211,6 +356,10 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
|
||||
selected.original_transforms.clear();
|
||||
|
||||
responses.add(OverlaysMessage::AddProvider(TRANSFORM_GRS_OVERLAY_PROVIDER));
|
||||
responses.add(TransformLayerMessage::PointerMove {
|
||||
slow_key: SLOW_KEY,
|
||||
increments_key: INCREMENTS_KEY,
|
||||
});
|
||||
}
|
||||
TransformLayerMessage::BeginRotate => {
|
||||
let selected_points: Vec<&ManipulatorPointId> = shape_editor.selected_points().collect();
|
||||
@@ -261,6 +410,10 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
|
||||
selected.original_transforms.clear();
|
||||
|
||||
responses.add(OverlaysMessage::AddProvider(TRANSFORM_GRS_OVERLAY_PROVIDER));
|
||||
responses.add(TransformLayerMessage::PointerMove {
|
||||
slow_key: SLOW_KEY,
|
||||
increments_key: INCREMENTS_KEY,
|
||||
});
|
||||
}
|
||||
TransformLayerMessage::BeginScale => {
|
||||
let selected_points: Vec<&ManipulatorPointId> = shape_editor.selected_points().collect();
|
||||
@@ -310,6 +463,10 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
|
||||
selected.original_transforms.clear();
|
||||
|
||||
responses.add(OverlaysMessage::AddProvider(TRANSFORM_GRS_OVERLAY_PROVIDER));
|
||||
responses.add(TransformLayerMessage::PointerMove {
|
||||
slow_key: SLOW_KEY,
|
||||
increments_key: INCREMENTS_KEY,
|
||||
});
|
||||
}
|
||||
TransformLayerMessage::CancelTransformOperation => {
|
||||
if using_pen_tool {
|
||||
@@ -335,159 +492,21 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
|
||||
TransformLayerMessage::ConstrainX => {
|
||||
self.local = self
|
||||
.transform_operation
|
||||
.constrain_axis(Axis::X, &mut selected, self.snap, self.local, self.fixed_bbox, document_to_viewport)
|
||||
.constrain_axis(Axis::X, &mut selected, self.increments, 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)
|
||||
.constrain_axis(Axis::Y, &mut selected, self.increments, 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.grab_target, self.grab_target + 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 = if self.grs_pen_handle { self.handle - self.last_point } else { quad[1] - quad[0] };
|
||||
let offset_angle = offset_angle.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_LABEL_BACKGROUND), transform, 4., [Pivot::Start, Pivot::End]);
|
||||
}
|
||||
}
|
||||
TransformLayerMessage::PointerMove { slow_key, snap_key } => {
|
||||
TransformLayerMessage::PointerMove { slow_key, increments_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 new_increments = input.keyboard.get(increments_key as usize);
|
||||
if new_increments != self.increments {
|
||||
self.increments = new_increments;
|
||||
self.transform_operation
|
||||
.apply_transform_operation(&mut selected, self.snap, self.local, self.fixed_bbox, document_to_viewport);
|
||||
.apply_transform_operation(&mut selected, self.increments, self.local, self.fixed_bbox, document_to_viewport);
|
||||
}
|
||||
|
||||
if self.typing.digits.is_empty() {
|
||||
@@ -499,7 +518,7 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
|
||||
let change = if self.slow { delta_pos / SLOWING_DIVISOR } else { delta_pos };
|
||||
self.transform_operation = TransformOperation::Grabbing(translation.increment_amount(change));
|
||||
self.transform_operation
|
||||
.apply_transform_operation(&mut selected, self.snap, self.local, self.fixed_bbox, document_to_viewport);
|
||||
.apply_transform_operation(&mut selected, self.increments, self.local, self.fixed_bbox, document_to_viewport);
|
||||
}
|
||||
TransformOperation::Rotating(rotation) => {
|
||||
let start_offset = *selected.pivot - self.mouse_position;
|
||||
@@ -510,22 +529,33 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
|
||||
|
||||
self.transform_operation = TransformOperation::Rotating(rotation.increment_amount(change));
|
||||
self.transform_operation
|
||||
.apply_transform_operation(&mut selected, self.snap, self.local, self.fixed_bbox, document_to_viewport);
|
||||
.apply_transform_operation(&mut selected, self.increments, self.local, self.fixed_bbox, document_to_viewport);
|
||||
}
|
||||
TransformOperation::Scaling(scale) => {
|
||||
let axis_constraint = scale.constraint;
|
||||
let to_mouse_final = self.mouse_position - *selected.pivot;
|
||||
let to_mouse_final_old = input.mouse.position - *selected.pivot;
|
||||
let to_mouse_start = self.start_mouse - *selected.pivot;
|
||||
|
||||
let to_mouse_final = project_edge_to_quad(to_mouse_final, &self.fixed_bbox, self.local, axis_constraint);
|
||||
let to_mouse_final_old = project_edge_to_quad(to_mouse_final_old, &self.fixed_bbox, self.local, axis_constraint);
|
||||
let to_mouse_start = project_edge_to_quad(to_mouse_start, &self.fixed_bbox, self.local, axis_constraint);
|
||||
|
||||
let change = {
|
||||
let previous_frame_dist = (self.mouse_position - *selected.pivot).length();
|
||||
let current_frame_dist = (input.mouse.position - *selected.pivot).length();
|
||||
let start_transform_dist = (self.start_mouse - *selected.pivot).length();
|
||||
let previous_frame_dist = to_mouse_final.length();
|
||||
let current_frame_dist = to_mouse_final_old.length();
|
||||
let start_transform_dist = to_mouse_start.length();
|
||||
|
||||
(current_frame_dist - previous_frame_dist) / start_transform_dist
|
||||
};
|
||||
let change = if self.slow { change / SLOWING_DIVISOR } else { change };
|
||||
|
||||
self.transform_operation = TransformOperation::Scaling(scale.increment_amount(change));
|
||||
let sign = to_mouse_final.dot(to_mouse_start).signum();
|
||||
let scale = scale.increment_amount(change * scale.to_f64(self.increments).signum());
|
||||
self.transform_operation = TransformOperation::Scaling(scale);
|
||||
|
||||
self.transform_operation
|
||||
.apply_transform_operation(&mut selected, self.snap, self.local, self.fixed_bbox, document_to_viewport);
|
||||
let op = TransformOperation::Scaling(if sign > 0. { scale } else { scale.negate() });
|
||||
op.apply_transform_operation(&mut selected, self.increments, self.local, self.fixed_bbox, document_to_viewport);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -536,23 +566,32 @@ 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, self.local, self.fixed_bbox, document_to_viewport),
|
||||
TransformLayerMessage::TypeDecimalPoint => {
|
||||
TransformLayerMessage::TypeBackspace => {
|
||||
if self.typing.digits.is_empty() && self.typing.negative {
|
||||
self.transform_operation.negate(&mut selected, self.increments, self.local, self.fixed_bbox, document_to_viewport);
|
||||
self.typing.type_negate();
|
||||
}
|
||||
self.transform_operation
|
||||
.grs_typed(self.typing.type_decimal_point(), &mut selected, self.snap, self.local, self.fixed_bbox, document_to_viewport)
|
||||
.grs_typed(self.typing.type_backspace(), &mut selected, self.increments, self.local, self.fixed_bbox, document_to_viewport);
|
||||
}
|
||||
TransformLayerMessage::TypeDecimalPoint => {
|
||||
if self.typing.digits.is_empty() {
|
||||
self.typing.negative = false;
|
||||
} else {
|
||||
self.transform_operation
|
||||
.grs_typed(self.typing.type_decimal_point(), &mut selected, self.increments, 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)
|
||||
.grs_typed(self.typing.type_number(digit), &mut selected, self.increments, 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.negate(&mut selected, self.increments, 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)
|
||||
.grs_typed(self.typing.type_negate(), &mut selected, self.increments, self.local, self.fixed_bbox, document_to_viewport)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user