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:
parent
303c1d45f8
commit
fb7eae8f02
|
|
@ -93,7 +93,7 @@ pub fn input_mappings() -> Mapping {
|
|||
entry!(KeyDown(Minus); action_dispatch=TransformLayerMessage::TypeNegate),
|
||||
entry!(KeyDown(Comma); action_dispatch=TransformLayerMessage::TypeDecimalPoint),
|
||||
entry!(KeyDown(Period); action_dispatch=TransformLayerMessage::TypeDecimalPoint),
|
||||
entry!(PointerMove; refresh_keys=[Control, Shift], action_dispatch=TransformLayerMessage::PointerMove { slow_key: Shift, snap_key: Control }),
|
||||
entry!(PointerMove; refresh_keys=[Control, Shift], action_dispatch=TransformLayerMessage::PointerMove { slow_key: Shift, increments_key: Control }),
|
||||
//
|
||||
// SelectToolMessage
|
||||
entry!(PointerMove; refresh_keys=[Control, Alt, Shift], action_dispatch=SelectToolMessage::PointerMove(SelectToolPointerKeys { axis_align: Shift, snap_angle: Control, center: Alt, duplicate: Alt })),
|
||||
|
|
|
|||
|
|
@ -295,6 +295,7 @@ impl fmt::Display for Key {
|
|||
Self::MouseMiddle => "MMB",
|
||||
Self::MouseBack => "Mouse Back",
|
||||
Self::MouseForward => "Mouse Fwd",
|
||||
Self::NumKeys => "0–9",
|
||||
|
||||
_ => key_name.as_str(),
|
||||
};
|
||||
|
|
@ -317,20 +318,6 @@ struct LayoutKey {
|
|||
key: String,
|
||||
label: String,
|
||||
}
|
||||
/*
|
||||
impl Serialize for Key {
|
||||
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||
let key = format!("{:?}", self.0);
|
||||
let label = self.0.to_string();
|
||||
|
||||
assert_eq!(serde_json::to_string(Key::KeyEscape), {"key": KeyEscape, "label": "Esc"});
|
||||
|
||||
let mut state = serializer.serialize_struct("KeyWithLabel", 2)?;
|
||||
state.serialize_field("key", &key)?;
|
||||
state.serialize_field("label", &label)?;
|
||||
state.end()
|
||||
}
|
||||
}*/
|
||||
|
||||
pub const NUMBER_OF_KEYS: usize = Key::NumKeys as usize;
|
||||
|
||||
|
|
|
|||
|
|
@ -143,8 +143,8 @@ pub struct Translation {
|
|||
}
|
||||
|
||||
impl Translation {
|
||||
pub fn to_dvec(self, transform: DAffine2) -> DVec2 {
|
||||
if let Some(value) = self.typed_distance {
|
||||
pub fn to_dvec(self, transform: DAffine2, snap: bool) -> DVec2 {
|
||||
let displacement = if let Some(value) = self.typed_distance {
|
||||
let document_displacement = if self.constraint == Axis::Y { DVec2::new(0., value) } else { DVec2::new(value, 0.) };
|
||||
transform.transform_vector2(document_displacement)
|
||||
} else {
|
||||
|
|
@ -153,6 +153,12 @@ impl Translation {
|
|||
Axis::X => DVec2::new(self.dragged_distance.x, 0.),
|
||||
Axis::Y => DVec2::new(0., self.dragged_distance.y),
|
||||
}
|
||||
};
|
||||
let displacement = transform.inverse().transform_vector2(displacement);
|
||||
if snap {
|
||||
displacement.round()
|
||||
} else {
|
||||
displacement
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -172,6 +178,11 @@ impl Translation {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn negate(self) -> Self {
|
||||
let dragged_distance = -self.dragged_distance;
|
||||
Self { dragged_distance, ..self }
|
||||
}
|
||||
|
||||
pub fn with_constraint(self, target: Axis, local: bool) -> (Self, bool) {
|
||||
let (constraint, local) = self.constraint.contrainted_to_axis(target, local);
|
||||
(Self { constraint, ..self }, local)
|
||||
|
|
@ -297,10 +308,11 @@ impl TransformOperation {
|
|||
if self != &TransformOperation::None {
|
||||
let transformation = match self {
|
||||
TransformOperation::Grabbing(translation) => {
|
||||
let translate = DAffine2::from_translation(transform.transform_vector2(translation.to_dvec(transform, snapping)));
|
||||
if local {
|
||||
DAffine2::from_angle(edge.to_angle()) * DAffine2::from_translation(translation.to_dvec(transform)) * DAffine2::from_angle(-edge.to_angle())
|
||||
DAffine2::from_angle(edge.to_angle()) * translate * DAffine2::from_angle(-edge.to_angle())
|
||||
} else {
|
||||
DAffine2::from_translation(translation.to_dvec(transform))
|
||||
translate
|
||||
}
|
||||
}
|
||||
TransformOperation::Rotating(rotation) => DAffine2::from_angle(rotation.to_f64(snapping)),
|
||||
|
|
@ -315,7 +327,16 @@ impl TransformOperation {
|
|||
};
|
||||
|
||||
selected.update_transforms(transformation);
|
||||
self.hints(selected.responses);
|
||||
self.hints(selected.responses, local);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_typing(&self) -> bool {
|
||||
match self {
|
||||
TransformOperation::None => false,
|
||||
TransformOperation::Grabbing(translation) => translation.typed_distance.is_some(),
|
||||
TransformOperation::Rotating(rotation) => rotation.typed_angle.is_some(),
|
||||
TransformOperation::Scaling(scale) => scale.typed_factor.is_some(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -346,21 +367,61 @@ impl TransformOperation {
|
|||
self.apply_transform_operation(selected, snapping, local, quad, transform);
|
||||
}
|
||||
|
||||
pub fn hints(&self, responses: &mut VecDeque<Message>) {
|
||||
pub fn hints(&self, responses: &mut VecDeque<Message>, local: bool) {
|
||||
use crate::messages::input_mapper::utility_types::input_keyboard::Key;
|
||||
use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo};
|
||||
|
||||
let mut input_hints = Vec::new();
|
||||
input_hints.push(HintInfo::keys([Key::Shift], "Slow Mode"));
|
||||
if matches!(self, TransformOperation::Rotating(_) | TransformOperation::Scaling(_)) {
|
||||
input_hints.push(HintInfo::keys([Key::Control], "Snap"));
|
||||
}
|
||||
if matches!(self, TransformOperation::Grabbing(_) | TransformOperation::Scaling(_)) {
|
||||
input_hints.push(HintInfo::keys([Key::KeyX], "Along X Axis"));
|
||||
input_hints.push(HintInfo::keys([Key::KeyY], "Along Y Axis"));
|
||||
if self.is_typing() {
|
||||
input_hints.push(HintInfo::keys([Key::Minus], "Negate Direction"));
|
||||
input_hints.push(HintInfo::keys([Key::Backspace], "Delete Digit"));
|
||||
input_hints.push(HintInfo::keys([Key::NumKeys], "Enter Number"));
|
||||
} else if matches!(self, TransformOperation::Grabbing(_) | TransformOperation::Scaling(_)) {
|
||||
let axis_constraint = match self {
|
||||
TransformOperation::Grabbing(grabbing) => grabbing.constraint,
|
||||
TransformOperation::Scaling(scaling) => scaling.constraint,
|
||||
_ => Axis::Both,
|
||||
};
|
||||
let clear_constraint = "Clear Constraint";
|
||||
match axis_constraint {
|
||||
Axis::Both => {
|
||||
input_hints.push(HintInfo::keys([Key::KeyX], "Along X Axis"));
|
||||
input_hints.push(HintInfo::keys([Key::KeyY], "Along Y Axis"));
|
||||
}
|
||||
Axis::X => {
|
||||
let x_label = if local { clear_constraint } else { "Along Local X Axis" };
|
||||
input_hints.push(HintInfo::keys([Key::KeyX], x_label));
|
||||
input_hints.push(HintInfo::keys([Key::KeyY], "Along Y Axis"));
|
||||
if !local {
|
||||
input_hints.push(HintInfo::keys([Key::KeyX, Key::KeyX], clear_constraint));
|
||||
}
|
||||
}
|
||||
Axis::Y => {
|
||||
let y_label = if local { clear_constraint } else { "Along Local Y Axis" };
|
||||
input_hints.push(HintInfo::keys([Key::KeyX], "Along X Axis"));
|
||||
input_hints.push(HintInfo::keys([Key::KeyY], y_label));
|
||||
if !local {
|
||||
input_hints.push(HintInfo::keys([Key::KeyY, Key::KeyY], clear_constraint));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let hint_data = HintData(vec![HintGroup(input_hints)]);
|
||||
let grs_hint_group = match self {
|
||||
TransformOperation::None => unreachable!(),
|
||||
TransformOperation::Scaling(_) => HintGroup(vec![HintInfo::multi_keys([[Key::KeyG], [Key::KeyR]], "Grab/Rotate Selected")]),
|
||||
TransformOperation::Grabbing(_) => HintGroup(vec![HintInfo::multi_keys([[Key::KeyR], [Key::KeyS]], "Rotate/Scale Selected")]),
|
||||
TransformOperation::Rotating(_) => HintGroup(vec![HintInfo::multi_keys([[Key::KeyG], [Key::KeyS]], "Grab/Scale Selected")]),
|
||||
};
|
||||
|
||||
let mut hint_groups = vec![grs_hint_group];
|
||||
if !self.is_typing() {
|
||||
let modifiers = vec![HintInfo::keys([Key::Shift], "Slow"), HintInfo::keys([Key::Control], "Increments")];
|
||||
hint_groups.push(HintGroup(modifiers));
|
||||
}
|
||||
hint_groups.push(HintGroup(input_hints));
|
||||
|
||||
let hint_data = HintData(hint_groups);
|
||||
responses.add(FrontendMessage::UpdateInputHints { hint_data });
|
||||
}
|
||||
|
||||
|
|
@ -369,6 +430,7 @@ impl TransformOperation {
|
|||
*self = match self {
|
||||
TransformOperation::Scaling(scale) => TransformOperation::Scaling(scale.negate()),
|
||||
TransformOperation::Rotating(rotation) => TransformOperation::Rotating(rotation.negate()),
|
||||
TransformOperation::Grabbing(translation) => TransformOperation::Grabbing(translation.negate()),
|
||||
_ => *self,
|
||||
};
|
||||
self.apply_transform_operation(selected, snapping, local, quad, transform);
|
||||
|
|
@ -580,6 +642,7 @@ impl<'a> Selected<'a> {
|
|||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub struct Typing {
|
||||
pub digits: Vec<u8>,
|
||||
pub string: String,
|
||||
pub contains_decimal: bool,
|
||||
pub negative: bool,
|
||||
}
|
||||
|
|
@ -589,6 +652,7 @@ const DECIMAL_POINT: u8 = 10;
|
|||
impl Typing {
|
||||
pub fn type_number(&mut self, number: u8) -> Option<f64> {
|
||||
self.digits.push(number);
|
||||
self.string.push((b'0' + number) as char);
|
||||
|
||||
self.evaluate()
|
||||
}
|
||||
|
|
@ -603,7 +667,7 @@ impl Typing {
|
|||
Some(_) => (),
|
||||
None => self.negative = false,
|
||||
}
|
||||
|
||||
self.string.pop();
|
||||
self.evaluate()
|
||||
}
|
||||
|
||||
|
|
@ -611,6 +675,7 @@ impl Typing {
|
|||
if !self.contains_decimal {
|
||||
self.contains_decimal = true;
|
||||
self.digits.push(DECIMAL_POINT);
|
||||
self.string.push('.');
|
||||
}
|
||||
|
||||
self.evaluate()
|
||||
|
|
@ -618,6 +683,11 @@ impl Typing {
|
|||
|
||||
pub fn type_negate(&mut self) -> Option<f64> {
|
||||
self.negative = !self.negative;
|
||||
if self.negative {
|
||||
self.string.insert(0, '-');
|
||||
} else {
|
||||
self.string.remove(0);
|
||||
}
|
||||
|
||||
self.evaluate()
|
||||
}
|
||||
|
|
@ -653,6 +723,7 @@ impl Typing {
|
|||
|
||||
pub fn clear(&mut self) {
|
||||
self.digits.clear();
|
||||
self.string.clear();
|
||||
self.contains_decimal = false;
|
||||
self.negative = false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -429,7 +429,7 @@ impl Fsm for BrushToolFsmState {
|
|||
let hint_data = match self {
|
||||
BrushToolFsmState::Ready => HintData(vec![
|
||||
HintGroup(vec![HintInfo::mouse(MouseMotion::LmbDrag, "Draw")]),
|
||||
HintGroup(vec![HintInfo::keys([Key::BracketLeft, Key::BracketRight], "Shrink/Grow Brush")]),
|
||||
HintGroup(vec![HintInfo::multi_keys([[Key::BracketLeft], [Key::BracketRight]], "Shrink/Grow Brush")]),
|
||||
]),
|
||||
BrushToolFsmState::Drawing => HintData(vec![HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()])]),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1055,7 +1055,7 @@ impl Fsm for PathToolFsmState {
|
|||
HintGroup(vec![HintInfo::mouse(MouseMotion::LmbDouble, "Make Anchor Smooth/Sharp")]),
|
||||
// TODO: Only show the following hints if at least one point is selected
|
||||
HintGroup(vec![HintInfo::mouse(MouseMotion::LmbDrag, "Drag Selected")]),
|
||||
HintGroup(vec![HintInfo::keys([Key::KeyG, Key::KeyR, Key::KeyS], "Grab/Rotate/Scale Selected")]),
|
||||
HintGroup(vec![HintInfo::multi_keys([[Key::KeyG], [Key::KeyR], [Key::KeyS]], "Grab/Rotate/Scale Selected")]),
|
||||
HintGroup(vec![HintInfo::arrow_keys("Nudge Selected"), HintInfo::keys([Key::Shift], "10x").prepend_plus()]),
|
||||
HintGroup(vec![
|
||||
HintInfo::keys([Key::Delete], "Delete Selected"),
|
||||
|
|
|
|||
|
|
@ -1198,7 +1198,7 @@ impl Fsm for SelectToolFsmState {
|
|||
HintInfo::mouse(MouseMotion::LmbDrag, "Select Area"),
|
||||
HintInfo::keys([Key::Shift], "Extend Selection").prepend_plus(),
|
||||
]),
|
||||
HintGroup(vec![HintInfo::keys([Key::KeyG, Key::KeyR, Key::KeyS], "Grab/Rotate/Scale Selected")]),
|
||||
HintGroup(vec![HintInfo::multi_keys([[Key::KeyG], [Key::KeyR], [Key::KeyS]], "Grab/Rotate/Scale Selected")]),
|
||||
HintGroup(vec![
|
||||
HintInfo::arrow_keys("Nudge Selected"),
|
||||
HintInfo::keys([Key::Shift], "10x").prepend_plus(),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -482,8 +482,12 @@ pub struct HintInfo {
|
|||
}
|
||||
|
||||
impl HintInfo {
|
||||
/// Used for a hint where a single key or key stroke is used to perform one action.
|
||||
/// Examples:
|
||||
/// - The Escape key can be used to cancel an action
|
||||
/// - The Ctrl+C key stroke can be used to copy
|
||||
pub fn keys(keys: impl IntoIterator<Item = Key>, label: impl Into<Cow<'static, str>>) -> Self {
|
||||
let keys: Vec<_> = keys.into_iter().collect();
|
||||
let keys = keys.into_iter().collect();
|
||||
Self {
|
||||
key_groups: vec![KeysGroup(keys).into()],
|
||||
key_groups_mac: None,
|
||||
|
|
@ -494,6 +498,10 @@ impl HintInfo {
|
|||
}
|
||||
}
|
||||
|
||||
/// Used for a hint where multiple different individual keys can be used to perform variations of the same action. These keys are represented with a slight separation between them compared to [`Self::keys`].
|
||||
/// Examples:
|
||||
/// - The four arrow keys can be used to nudge a layer in different directions
|
||||
/// - The G, R, and S keys can be used to enter GRS transformation mode
|
||||
pub fn multi_keys(multi_keys: impl IntoIterator<Item = impl IntoIterator<Item = Key>>, label: impl Into<Cow<'static, str>>) -> Self {
|
||||
let key_groups = multi_keys.into_iter().map(|keys| KeysGroup(keys.into_iter().collect()).into()).collect();
|
||||
Self {
|
||||
|
|
@ -529,7 +537,7 @@ impl HintInfo {
|
|||
}
|
||||
|
||||
pub fn keys_and_mouse(keys: impl IntoIterator<Item = Key>, mouse_motion: MouseMotion, label: impl Into<Cow<'static, str>>) -> Self {
|
||||
let keys: Vec<_> = keys.into_iter().collect();
|
||||
let keys = keys.into_iter().collect();
|
||||
Self {
|
||||
key_groups: vec![KeysGroup(keys).into()],
|
||||
key_groups_mac: None,
|
||||
|
|
@ -568,7 +576,7 @@ impl HintInfo {
|
|||
}
|
||||
|
||||
pub fn add_mac_keys(mut self, keys: impl IntoIterator<Item = Key>) -> Self {
|
||||
let mac_keys: Vec<_> = keys.into_iter().collect();
|
||||
let mac_keys = keys.into_iter().collect();
|
||||
self.key_groups_mac = Some(vec![KeysGroup(mac_keys).into()]);
|
||||
self
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue