mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-10-02 03:28:11 +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
@@ -53,6 +53,10 @@ pub const PIVOT_CROSSHAIR_THICKNESS: f64 = 1.;
|
|||||||
pub const PIVOT_CROSSHAIR_LENGTH: f64 = 9.;
|
pub const PIVOT_CROSSHAIR_LENGTH: f64 = 9.;
|
||||||
pub const PIVOT_DIAMETER: f64 = 5.;
|
pub const PIVOT_DIAMETER: f64 = 5.;
|
||||||
|
|
||||||
|
// Transform overlay
|
||||||
|
pub const ANGLE_MEASURE_RADIUS_FACTOR: f64 = 0.04;
|
||||||
|
pub const ARC_MEASURE_RADIUS_FACTOR_RANGE: (f64, f64) = (0.05, 0.15);
|
||||||
|
|
||||||
// Transformation cage
|
// Transformation cage
|
||||||
pub const BOUNDS_SELECT_THRESHOLD: f64 = 10.;
|
pub const BOUNDS_SELECT_THRESHOLD: f64 = 10.;
|
||||||
pub const BOUNDS_ROTATE_THRESHOLD: f64 = 20.;
|
pub const BOUNDS_ROTATE_THRESHOLD: f64 = 20.;
|
||||||
@@ -88,6 +92,7 @@ pub const COLOR_OVERLAY_RED: &str = "#ef5454";
|
|||||||
pub const COLOR_OVERLAY_GRAY: &str = "#cccccc";
|
pub const COLOR_OVERLAY_GRAY: &str = "#cccccc";
|
||||||
pub const COLOR_OVERLAY_WHITE: &str = "#ffffff";
|
pub const COLOR_OVERLAY_WHITE: &str = "#ffffff";
|
||||||
pub const COLOR_OVERLAY_SNAP_BACKGROUND: &str = "#000000cc";
|
pub const COLOR_OVERLAY_SNAP_BACKGROUND: &str = "#000000cc";
|
||||||
|
pub const COLOR_OVERLAY_TRANSPARENT: &str = "#ffffff00";
|
||||||
|
|
||||||
// Document
|
// Document
|
||||||
pub const DEFAULT_DOCUMENT_NAME: &str = "Untitled Document";
|
pub const DEFAULT_DOCUMENT_NAME: &str = "Untitled Document";
|
||||||
|
|||||||
@@ -1029,9 +1029,8 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
|
|||||||
self.graph_fade_artwork_percentage = percentage;
|
self.graph_fade_artwork_percentage = percentage;
|
||||||
responses.add(FrontendMessage::UpdateGraphFadeArtwork { percentage });
|
responses.add(FrontendMessage::UpdateGraphFadeArtwork { percentage });
|
||||||
}
|
}
|
||||||
|
|
||||||
DocumentMessage::SetNodePinned { node_id, pinned } => {
|
DocumentMessage::SetNodePinned { node_id, pinned } => {
|
||||||
responses.add(DocumentMessage::StartTransaction);
|
responses.add(DocumentMessage::AddTransaction);
|
||||||
responses.add(NodeGraphMessage::SetPinned { node_id, pinned });
|
responses.add(NodeGraphMessage::SetPinned { node_id, pinned });
|
||||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||||
responses.add(NodeGraphMessage::SelectedNodesUpdated);
|
responses.add(NodeGraphMessage::SelectedNodesUpdated);
|
||||||
@@ -1059,6 +1058,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
|
|||||||
DocumentMessage::SetToNodeOrLayer { node_id, is_layer } => {
|
DocumentMessage::SetToNodeOrLayer { node_id, is_layer } => {
|
||||||
responses.add(DocumentMessage::StartTransaction);
|
responses.add(DocumentMessage::StartTransaction);
|
||||||
responses.add(NodeGraphMessage::SetToNodeOrLayer { node_id, is_layer });
|
responses.add(NodeGraphMessage::SetToNodeOrLayer { node_id, is_layer });
|
||||||
|
responses.add(DocumentMessage::EndTransaction);
|
||||||
}
|
}
|
||||||
DocumentMessage::SetViewMode { view_mode } => {
|
DocumentMessage::SetViewMode { view_mode } => {
|
||||||
self.view_mode = view_mode;
|
self.view_mode = view_mode;
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ impl MessageHandler<NavigationMessage, NavigationMessageData<'_>> for Navigation
|
|||||||
key_groups: vec![KeysGroup(vec![Key::Control]).into()],
|
key_groups: vec![KeysGroup(vec![Key::Control]).into()],
|
||||||
key_groups_mac: None,
|
key_groups_mac: None,
|
||||||
mouse: None,
|
mouse: None,
|
||||||
label: String::from("Snap 15°"),
|
label: "Snap 15°".into(),
|
||||||
plus: false,
|
plus: false,
|
||||||
slash: false,
|
slash: false,
|
||||||
}]),
|
}]),
|
||||||
@@ -129,7 +129,7 @@ impl MessageHandler<NavigationMessage, NavigationMessageData<'_>> for Navigation
|
|||||||
key_groups: vec![KeysGroup(vec![Key::Control]).into()],
|
key_groups: vec![KeysGroup(vec![Key::Control]).into()],
|
||||||
key_groups_mac: None,
|
key_groups_mac: None,
|
||||||
mouse: None,
|
mouse: None,
|
||||||
label: String::from("Increments"),
|
label: "Increments".into(),
|
||||||
plus: false,
|
plus: false,
|
||||||
slash: false,
|
slash: false,
|
||||||
}]),
|
}]),
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
use super::utility_functions::overlay_canvas_context;
|
use super::utility_functions::overlay_canvas_context;
|
||||||
use crate::consts::{COLOR_OVERLAY_BLUE, COLOR_OVERLAY_WHITE, COLOR_OVERLAY_YELLOW, MANIPULATOR_GROUP_MARKER_SIZE, PIVOT_CROSSHAIR_LENGTH, PIVOT_CROSSHAIR_THICKNESS, PIVOT_DIAMETER};
|
use crate::consts::{
|
||||||
|
COLOR_OVERLAY_BLUE, COLOR_OVERLAY_TRANSPARENT, COLOR_OVERLAY_WHITE, COLOR_OVERLAY_YELLOW, MANIPULATOR_GROUP_MARKER_SIZE, PIVOT_CROSSHAIR_LENGTH, PIVOT_CROSSHAIR_THICKNESS, PIVOT_DIAMETER,
|
||||||
|
};
|
||||||
use crate::messages::prelude::Message;
|
use crate::messages::prelude::Message;
|
||||||
|
|
||||||
use bezier_rs::{Bezier, Subpath};
|
use bezier_rs::{Bezier, Subpath};
|
||||||
@@ -188,6 +190,65 @@ impl OverlayContext {
|
|||||||
self.render_context.fill();
|
self.render_context.fill();
|
||||||
self.render_context.stroke();
|
self.render_context.stroke();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn draw_arc(&mut self, center: DVec2, radius: f64, start_from: f64, end_at: f64) {
|
||||||
|
let segments = ((end_at - start_from).abs() / (std::f64::consts::PI / 4.)).ceil() as usize;
|
||||||
|
let step = (end_at - start_from) / segments as f64;
|
||||||
|
let half_step = step / 2.;
|
||||||
|
let factor = 4. / 3. * half_step.sin() / (1. + half_step.cos());
|
||||||
|
|
||||||
|
self.render_context.begin_path();
|
||||||
|
|
||||||
|
for i in 0..segments {
|
||||||
|
let start_angle = start_from + step * i as f64;
|
||||||
|
let end_angle = start_angle + step;
|
||||||
|
let start_vec = DVec2::from_angle(start_angle);
|
||||||
|
let end_vec = DVec2::from_angle(end_angle);
|
||||||
|
|
||||||
|
let start = center + radius * start_vec;
|
||||||
|
let end = center + radius * end_vec;
|
||||||
|
|
||||||
|
let handle_start = start + start_vec.perp() * radius * factor;
|
||||||
|
let handle_end = end - end_vec.perp() * radius * factor;
|
||||||
|
|
||||||
|
let bezier = Bezier {
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
handles: bezier_rs::BezierHandles::Cubic { handle_start, handle_end },
|
||||||
|
};
|
||||||
|
|
||||||
|
self.bezier_command(bezier, DAffine2::IDENTITY, i == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.render_context.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn draw_angle(&mut self, pivot: DVec2, radius: f64, arc_radius: f64, offset_angle: f64, angle: f64) {
|
||||||
|
let color_line = COLOR_OVERLAY_BLUE;
|
||||||
|
|
||||||
|
let end_point1 = pivot + radius * DVec2::from_angle(angle + offset_angle);
|
||||||
|
let end_point2 = pivot + radius * DVec2::from_angle(offset_angle);
|
||||||
|
self.line(pivot, end_point1, Some(color_line));
|
||||||
|
self.line(pivot, end_point2, Some(color_line));
|
||||||
|
|
||||||
|
self.draw_arc(pivot, arc_radius, offset_angle, (angle) % TAU + offset_angle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn draw_scale(&mut self, start: DVec2, scale: f64, radius: f64, text: &str) {
|
||||||
|
let sign = scale.signum();
|
||||||
|
self.line(start + DVec2::X * radius * sign, start + DVec2::X * (radius * scale), None);
|
||||||
|
self.circle(start, radius, Some(COLOR_OVERLAY_TRANSPARENT), None);
|
||||||
|
self.circle(start, radius * scale.abs(), Some(COLOR_OVERLAY_TRANSPARENT), None);
|
||||||
|
self.text(
|
||||||
|
text,
|
||||||
|
COLOR_OVERLAY_BLUE,
|
||||||
|
None,
|
||||||
|
DAffine2::from_translation(start + sign * DVec2::X * radius * (1. + scale.abs()) / 2.),
|
||||||
|
2.,
|
||||||
|
[Pivot::Middle, Pivot::End],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn pivot(&mut self, position: DVec2) {
|
pub fn pivot(&mut self, position: DVec2) {
|
||||||
let (x, y) = (position.round() - DVec2::splat(0.5)).into();
|
let (x, y) = (position.round() - DVec2::splat(0.5)).into();
|
||||||
|
|
||||||
@@ -300,6 +361,10 @@ impl OverlayContext {
|
|||||||
self.render_context.stroke();
|
self.render_context.stroke();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_width(&self, text: &str) -> f64 {
|
||||||
|
self.render_context.measure_text(text).expect("Failed to measure text dimensions").width()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn text(&self, text: &str, font_color: &str, background_color: Option<&str>, transform: DAffine2, padding: f64, pivot: [Pivot; 2]) {
|
pub fn text(&self, text: &str, font_color: &str, background_color: Option<&str>, transform: DAffine2, padding: f64, pivot: [Pivot; 2]) {
|
||||||
let metrics = self.render_context.measure_text(text).expect("Failed to measure the text dimensions");
|
let metrics = self.render_context.measure_text(text).expect("Failed to measure the text dimensions");
|
||||||
let x = match pivot[0] {
|
let x = match pivot[0] {
|
||||||
|
|||||||
@@ -122,14 +122,15 @@ pub enum Axis {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Axis {
|
impl Axis {
|
||||||
pub fn set_or_toggle(&mut self, target: Axis) {
|
pub fn contrainted_to_axis(self, target: Axis, local: bool) -> (Self, bool) {
|
||||||
// If constrained to an axis and target is requesting the same axis, toggle back to Both
|
if self != target {
|
||||||
if *self == target {
|
return (target, false);
|
||||||
*self = Axis::Both;
|
|
||||||
}
|
}
|
||||||
// If current axis is different from the target axis, switch to the target
|
|
||||||
else {
|
if local {
|
||||||
*self = target;
|
(Axis::Both, false)
|
||||||
|
} else {
|
||||||
|
(self, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -142,20 +143,17 @@ pub struct Translation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Translation {
|
impl Translation {
|
||||||
pub fn to_dvec(self) -> DVec2 {
|
pub fn to_dvec(self, transform: DAffine2) -> DVec2 {
|
||||||
if let Some(value) = self.typed_distance {
|
if let Some(value) = self.typed_distance {
|
||||||
if self.constraint == Axis::Y {
|
let document_displacement = if self.constraint == Axis::Y { DVec2::new(0., value) } else { DVec2::new(value, 0.) };
|
||||||
return DVec2::new(0., value);
|
transform.transform_vector2(document_displacement)
|
||||||
} else {
|
} else {
|
||||||
return DVec2::new(value, 0.);
|
match self.constraint {
|
||||||
|
Axis::Both => self.dragged_distance,
|
||||||
|
Axis::X => DVec2::new(self.dragged_distance.x, 0.),
|
||||||
|
Axis::Y => DVec2::new(0., self.dragged_distance.y),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
match self.constraint {
|
|
||||||
Axis::Both => self.dragged_distance,
|
|
||||||
Axis::X => DVec2::new(self.dragged_distance.x, 0.),
|
|
||||||
Axis::Y => DVec2::new(0., self.dragged_distance.y),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
@@ -173,6 +171,11 @@ impl Translation {
|
|||||||
constraint: self.constraint,
|
constraint: self.constraint,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, PartialEq, Copy)]
|
#[derive(Default, Debug, Clone, PartialEq, Copy)]
|
||||||
@@ -206,6 +209,11 @@ impl Rotation {
|
|||||||
typed_angle: None,
|
typed_angle: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn negate(self) -> Self {
|
||||||
|
let dragged_angle = -self.dragged_angle;
|
||||||
|
Self { dragged_angle, ..self }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Copy)]
|
#[derive(Debug, Clone, PartialEq, Copy)]
|
||||||
@@ -226,9 +234,17 @@ impl Default for Scale {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Scale {
|
impl Scale {
|
||||||
pub fn to_dvec(self, snap: bool) -> DVec2 {
|
pub fn to_f64(self, snap: bool) -> f64 {
|
||||||
let factor = if let Some(value) = self.typed_factor { value } else { self.dragged_factor };
|
let factor = if let Some(value) = self.typed_factor { value } else { self.dragged_factor };
|
||||||
let factor = if snap { (factor / SCALE_SNAP_INTERVAL).round() * SCALE_SNAP_INTERVAL } else { factor };
|
if snap {
|
||||||
|
(factor / SCALE_SNAP_INTERVAL).round() * SCALE_SNAP_INTERVAL
|
||||||
|
} else {
|
||||||
|
factor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_dvec(self, snap: bool) -> DVec2 {
|
||||||
|
let factor = self.to_f64(snap);
|
||||||
|
|
||||||
match self.constraint {
|
match self.constraint {
|
||||||
Axis::Both => DVec2::splat(factor),
|
Axis::Both => DVec2::splat(factor),
|
||||||
@@ -237,6 +253,11 @@ impl Scale {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn negate(self) -> Self {
|
||||||
|
let dragged_factor = -self.dragged_factor;
|
||||||
|
Self { dragged_factor, ..self }
|
||||||
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn increment_amount(self, delta: f64) -> Self {
|
pub fn increment_amount(self, delta: f64) -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -253,6 +274,11 @@ impl Scale {
|
|||||||
constraint: self.constraint,
|
constraint: self.constraint,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, PartialEq, Copy)]
|
#[derive(Default, Debug, Clone, PartialEq, Copy)]
|
||||||
@@ -265,32 +291,51 @@ pub enum TransformOperation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl TransformOperation {
|
impl TransformOperation {
|
||||||
pub fn apply_transform_operation(&self, selected: &mut Selected, snapping: bool, axis_constraint: Axis) {
|
pub fn apply_transform_operation(&self, selected: &mut Selected, snapping: bool, local: bool, quad: Quad, transform: DAffine2) {
|
||||||
|
let quad = quad.0;
|
||||||
|
let edge = quad[1] - quad[0];
|
||||||
if self != &TransformOperation::None {
|
if self != &TransformOperation::None {
|
||||||
let transformation = match self {
|
let transformation = match self {
|
||||||
TransformOperation::Grabbing(translation) => DAffine2::from_translation(translation.to_dvec()),
|
TransformOperation::Grabbing(translation) => {
|
||||||
|
if local {
|
||||||
|
DAffine2::from_angle(edge.to_angle()) * DAffine2::from_translation(translation.to_dvec(transform)) * DAffine2::from_angle(-edge.to_angle())
|
||||||
|
} else {
|
||||||
|
DAffine2::from_translation(translation.to_dvec(transform))
|
||||||
|
}
|
||||||
|
}
|
||||||
TransformOperation::Rotating(rotation) => DAffine2::from_angle(rotation.to_f64(snapping)),
|
TransformOperation::Rotating(rotation) => DAffine2::from_angle(rotation.to_f64(snapping)),
|
||||||
TransformOperation::Scaling(scale) => DAffine2::from_scale(scale.to_dvec(snapping)),
|
TransformOperation::Scaling(scale) => {
|
||||||
|
if local {
|
||||||
|
DAffine2::from_angle(edge.to_angle()) * DAffine2::from_scale(scale.to_dvec(snapping)) * DAffine2::from_angle(-edge.to_angle())
|
||||||
|
} else {
|
||||||
|
DAffine2::from_scale(scale.to_dvec(snapping))
|
||||||
|
}
|
||||||
|
}
|
||||||
TransformOperation::None => unreachable!(),
|
TransformOperation::None => unreachable!(),
|
||||||
};
|
};
|
||||||
|
|
||||||
selected.update_transforms(transformation);
|
selected.update_transforms(transformation);
|
||||||
self.hints(snapping, axis_constraint, selected.responses);
|
self.hints(selected.responses);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn constrain_axis(&mut self, axis: Axis, selected: &mut Selected, snapping: bool) {
|
pub fn constrain_axis(&mut self, axis: Axis, selected: &mut Selected, snapping: bool, mut local: bool, quad: Quad, transform: DAffine2) -> bool {
|
||||||
match self {
|
(*self, local) = match self {
|
||||||
TransformOperation::None => (),
|
TransformOperation::Grabbing(translation) => {
|
||||||
TransformOperation::Grabbing(translation) => translation.constraint.set_or_toggle(axis),
|
let (translation, local) = translation.with_constraint(axis, local);
|
||||||
TransformOperation::Rotating(_) => (),
|
(TransformOperation::Grabbing(translation), local)
|
||||||
TransformOperation::Scaling(scale) => scale.constraint.set_or_toggle(axis),
|
}
|
||||||
|
TransformOperation::Scaling(scale) => {
|
||||||
|
let (scale, local) = scale.with_constraint(axis, local);
|
||||||
|
(TransformOperation::Scaling(scale), local)
|
||||||
|
}
|
||||||
|
_ => (*self, false),
|
||||||
};
|
};
|
||||||
|
self.apply_transform_operation(selected, snapping, local, quad, transform);
|
||||||
self.apply_transform_operation(selected, snapping, axis);
|
local
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn grs_typed(&mut self, typed: Option<f64>, selected: &mut Selected, snapping: bool) {
|
pub fn grs_typed(&mut self, typed: Option<f64>, selected: &mut Selected, snapping: bool, local: bool, quad: Quad, transform: DAffine2) {
|
||||||
match self {
|
match self {
|
||||||
TransformOperation::None => (),
|
TransformOperation::None => (),
|
||||||
TransformOperation::Grabbing(translation) => translation.typed_distance = typed,
|
TransformOperation::Grabbing(translation) => translation.typed_distance = typed,
|
||||||
@@ -298,16 +343,10 @@ impl TransformOperation {
|
|||||||
TransformOperation::Scaling(scale) => scale.typed_factor = typed,
|
TransformOperation::Scaling(scale) => scale.typed_factor = typed,
|
||||||
};
|
};
|
||||||
|
|
||||||
let axis_constraint = match self {
|
self.apply_transform_operation(selected, snapping, local, quad, transform);
|
||||||
TransformOperation::Grabbing(grabbing) => grabbing.constraint,
|
|
||||||
TransformOperation::Scaling(scaling) => scaling.constraint,
|
|
||||||
_ => Axis::Both,
|
|
||||||
};
|
|
||||||
|
|
||||||
self.apply_transform_operation(selected, snapping, axis_constraint);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn hints(&self, snapping: bool, axis_constraint: Axis, responses: &mut VecDeque<Message>) {
|
pub fn hints(&self, responses: &mut VecDeque<Message>) {
|
||||||
use crate::messages::input_mapper::utility_types::input_keyboard::Key;
|
use crate::messages::input_mapper::utility_types::input_keyboard::Key;
|
||||||
use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo};
|
use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo};
|
||||||
|
|
||||||
@@ -321,25 +360,20 @@ impl TransformOperation {
|
|||||||
input_hints.push(HintInfo::keys([Key::KeyY], "Along Y Axis"));
|
input_hints.push(HintInfo::keys([Key::KeyY], "Along Y Axis"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Eventually, move this somewhere else (maybe an overlay in the corner of the viewport, design is TBD) since servicable but not ideal for UI design consistency to have it in the hints bar
|
let hint_data = HintData(vec![HintGroup(input_hints)]);
|
||||||
let axis_text = |vector: DVec2, separate: bool| match (axis_constraint, separate) {
|
|
||||||
(Axis::Both, false) => format!("by {:.3}", vector.x),
|
|
||||||
(Axis::Both, true) => format!("by {:.3}, {:.3}", vector.x, vector.y),
|
|
||||||
(Axis::X, _) => format!("X by {:.3}", vector.x),
|
|
||||||
(Axis::Y, _) => format!("Y by {:.3}", vector.y),
|
|
||||||
};
|
|
||||||
let grs_value_text = match self {
|
|
||||||
TransformOperation::None => String::new(),
|
|
||||||
// TODO: Fix that the translation is showing numbers in viewport space, not document space
|
|
||||||
TransformOperation::Grabbing(translation) => format!("Translating {}", axis_text(translation.to_dvec(), true)),
|
|
||||||
TransformOperation::Rotating(rotation) => format!("Rotating by {:.3}°", rotation.to_f64(snapping) * 360. / std::f64::consts::TAU),
|
|
||||||
TransformOperation::Scaling(scale) => format!("Scaling {}", axis_text(scale.to_dvec(snapping), false)),
|
|
||||||
};
|
|
||||||
let grs_value = vec![HintInfo::label(grs_value_text)];
|
|
||||||
|
|
||||||
let hint_data = HintData(vec![HintGroup(input_hints), HintGroup(grs_value)]);
|
|
||||||
responses.add(FrontendMessage::UpdateInputHints { hint_data });
|
responses.add(FrontendMessage::UpdateInputHints { hint_data });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn negate(&mut self, selected: &mut Selected, snapping: bool, local: bool, quad: Quad, transform: DAffine2) {
|
||||||
|
if *self != TransformOperation::None {
|
||||||
|
*self = match self {
|
||||||
|
TransformOperation::Scaling(scale) => TransformOperation::Scaling(scale.negate()),
|
||||||
|
TransformOperation::Rotating(rotation) => TransformOperation::Rotating(rotation.negate()),
|
||||||
|
_ => *self,
|
||||||
|
};
|
||||||
|
self.apply_transform_operation(selected, snapping, local, quad, transform);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Selected<'a> {
|
pub struct Selected<'a> {
|
||||||
@@ -402,6 +436,32 @@ impl<'a> Selected<'a> {
|
|||||||
(min + max) / 2.
|
(min + max) / 2.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn bounding_box(&mut self) -> Quad {
|
||||||
|
let metadata = self.network_interface.document_metadata();
|
||||||
|
|
||||||
|
let transform = self
|
||||||
|
.network_interface
|
||||||
|
.selected_nodes(&[])
|
||||||
|
.unwrap()
|
||||||
|
.selected_visible_and_unlocked_layers(self.network_interface)
|
||||||
|
.find(|layer| !self.network_interface.is_artboard(&layer.to_node(), &[]))
|
||||||
|
.map(|layer| metadata.transform_to_viewport(layer))
|
||||||
|
.unwrap_or(DAffine2::IDENTITY);
|
||||||
|
|
||||||
|
if transform.matrix2.determinant() == 0. {
|
||||||
|
return Default::default();
|
||||||
|
}
|
||||||
|
|
||||||
|
let bounds = self
|
||||||
|
.selected
|
||||||
|
.iter()
|
||||||
|
.filter_map(|&layer| metadata.bounding_box_with_transform(layer, transform.inverse() * metadata.transform_to_viewport(layer)))
|
||||||
|
.reduce(Quad::combine_bounds)
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
transform * Quad::from_box(bounds)
|
||||||
|
}
|
||||||
|
|
||||||
fn transform_layer(document_metadata: &DocumentMetadata, layer: LayerNodeIdentifier, original_transform: Option<&DAffine2>, transformation: DAffine2, responses: &mut VecDeque<Message>) {
|
fn transform_layer(document_metadata: &DocumentMetadata, layer: LayerNodeIdentifier, original_transform: Option<&DAffine2>, transformation: DAffine2, responses: &mut VecDeque<Message>) {
|
||||||
let Some(&original_transform) = original_transform else { return };
|
let Some(&original_transform) = original_transform else { return };
|
||||||
let to = document_metadata.downstream_transform_to_viewport(layer);
|
let to = document_metadata.downstream_transform_to_viewport(layer);
|
||||||
|
|||||||
@@ -878,9 +878,7 @@ impl ShapeState {
|
|||||||
|
|
||||||
pub fn break_path_at_selected_point(&self, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
pub fn break_path_at_selected_point(&self, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
||||||
for (&layer, state) in &self.selected_shape_state {
|
for (&layer, state) in &self.selected_shape_state {
|
||||||
let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else {
|
let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else { continue };
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
for &delete in &state.selected_points {
|
for &delete in &state.selected_points {
|
||||||
let Some(point) = delete.get_anchor(&vector_data) else { continue };
|
let Some(point) = delete.get_anchor(&vector_data) else { continue };
|
||||||
|
|||||||
@@ -971,7 +971,9 @@ impl Fsm for PathToolFsmState {
|
|||||||
if nearest_point.is_some() {
|
if nearest_point.is_some() {
|
||||||
// Flip the selected point between smooth and sharp
|
// Flip the selected point between smooth and sharp
|
||||||
if !tool_data.double_click_handled && tool_data.drag_start_pos.distance(input.mouse.position) <= DRAG_THRESHOLD {
|
if !tool_data.double_click_handled && tool_data.drag_start_pos.distance(input.mouse.position) <= DRAG_THRESHOLD {
|
||||||
|
responses.add(DocumentMessage::StartTransaction);
|
||||||
shape_editor.flip_smooth_sharp(&document.network_interface, input.mouse.position, SELECTION_TOLERANCE, responses);
|
shape_editor.flip_smooth_sharp(&document.network_interface, input.mouse.position, SELECTION_TOLERANCE, responses);
|
||||||
|
responses.add(DocumentMessage::EndTransaction);
|
||||||
responses.add(PathToolMessage::SelectedPointUpdated);
|
responses.add(PathToolMessage::SelectedPointUpdated);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -502,7 +502,7 @@ impl Fsm for SelectToolFsmState {
|
|||||||
|
|
||||||
// Measure with Alt held down
|
// Measure with Alt held down
|
||||||
// TODO: Don't use `Key::Alt` directly, instead take it as a variable from the input mappings list like in all other places
|
// TODO: Don't use `Key::Alt` directly, instead take it as a variable from the input mappings list like in all other places
|
||||||
if input.keyboard.get(Key::Alt as usize) {
|
if !matches!(self, Self::ResizingBounds { .. }) && input.keyboard.get(Key::Alt as usize) {
|
||||||
let hovered_bounds = document
|
let hovered_bounds = document
|
||||||
.metadata()
|
.metadata()
|
||||||
.bounding_box_with_transform(layer, transform.inverse() * document.metadata().transform_to_viewport(layer));
|
.bounding_box_with_transform(layer, transform.inverse() * document.metadata().transform_to_viewport(layer));
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
use crate::messages::input_mapper::utility_types::input_keyboard::Key;
|
use crate::messages::input_mapper::utility_types::input_keyboard::Key;
|
||||||
|
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
|
||||||
use crate::messages::prelude::*;
|
use crate::messages::prelude::*;
|
||||||
|
|
||||||
#[impl_message(Message, ToolMessage, TransformLayer)]
|
#[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 {
|
pub enum TransformLayerMessage {
|
||||||
// Messages
|
// Messages
|
||||||
ApplyTransformOperation,
|
ApplyTransformOperation,
|
||||||
@@ -12,6 +13,7 @@ pub enum TransformLayerMessage {
|
|||||||
CancelTransformOperation,
|
CancelTransformOperation,
|
||||||
ConstrainX,
|
ConstrainX,
|
||||||
ConstrainY,
|
ConstrainY,
|
||||||
|
Overlays(OverlayContext),
|
||||||
PointerMove { slow_key: Key, snap_key: Key },
|
PointerMove { slow_key: Key, snap_key: Key },
|
||||||
SelectionChanged,
|
SelectionChanged,
|
||||||
TypeBackspace,
|
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::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::portfolio::document::utility_types::transformation::{Axis, OriginalTransforms, Selected, TransformOperation, Typing};
|
||||||
use crate::messages::prelude::*;
|
use crate::messages::prelude::*;
|
||||||
use crate::messages::tool::common_functionality::shape_editor::ShapeState;
|
use crate::messages::tool::common_functionality::shape_editor::ShapeState;
|
||||||
use crate::messages::tool::utility_types::{ToolData, ToolType};
|
use crate::messages::tool::utility_types::{ToolData, ToolType};
|
||||||
|
|
||||||
|
use graphene_core::renderer::Quad;
|
||||||
use graphene_core::vector::ManipulatorPointId;
|
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)]
|
#[derive(Debug, Clone, Default)]
|
||||||
pub struct TransformLayerMessageHandler {
|
pub struct TransformLayerMessageHandler {
|
||||||
@@ -15,6 +20,8 @@ pub struct TransformLayerMessageHandler {
|
|||||||
|
|
||||||
slow: bool,
|
slow: bool,
|
||||||
snap: bool,
|
snap: bool,
|
||||||
|
local: bool,
|
||||||
|
fixed_bbox: Quad,
|
||||||
typing: Typing,
|
typing: Typing,
|
||||||
|
|
||||||
mouse_position: ViewportPosition,
|
mouse_position: ViewportPosition,
|
||||||
@@ -30,12 +37,7 @@ impl TransformLayerMessageHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn hints(&self, responses: &mut VecDeque<Message>) {
|
pub fn hints(&self, responses: &mut VecDeque<Message>) {
|
||||||
let axis_constraint = match self.transform_operation {
|
self.transform_operation.hints(responses);
|
||||||
TransformOperation::Grabbing(grabbing) => grabbing.constraint,
|
|
||||||
TransformOperation::Scaling(scaling) => scaling.constraint,
|
|
||||||
_ => Axis::Both,
|
|
||||||
};
|
|
||||||
self.transform_operation.hints(self.snap, axis_constraint, responses);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,6 +101,7 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
|
|||||||
|
|
||||||
selected.responses.add(DocumentMessage::StartTransaction);
|
selected.responses.add(DocumentMessage::StartTransaction);
|
||||||
};
|
};
|
||||||
|
let document_to_viewport = document.metadata().document_to_viewport;
|
||||||
|
|
||||||
match message {
|
match message {
|
||||||
TransformLayerMessage::ApplyTransformOperation => {
|
TransformLayerMessage::ApplyTransformOperation => {
|
||||||
@@ -111,6 +114,7 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
|
|||||||
responses.add(DocumentMessage::EndTransaction);
|
responses.add(DocumentMessage::EndTransaction);
|
||||||
responses.add(ToolMessage::UpdateHints);
|
responses.add(ToolMessage::UpdateHints);
|
||||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||||
|
responses.add(OverlaysMessage::RemoveProvider(TRANSFORM_GRS_OVERLAY_PROVIDER));
|
||||||
}
|
}
|
||||||
TransformLayerMessage::BeginGrab => {
|
TransformLayerMessage::BeginGrab => {
|
||||||
if (!using_path_tool && !using_select_tool)
|
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);
|
begin_operation(self.transform_operation, &mut self.typing, &mut self.mouse_position, &mut self.start_mouse);
|
||||||
|
|
||||||
self.transform_operation = TransformOperation::Grabbing(Default::default());
|
self.transform_operation = TransformOperation::Grabbing(Default::default());
|
||||||
|
self.local = false;
|
||||||
|
self.fixed_bbox = selected.bounding_box();
|
||||||
|
|
||||||
selected.original_transforms.clear();
|
selected.original_transforms.clear();
|
||||||
|
|
||||||
|
responses.add(OverlaysMessage::AddProvider(TRANSFORM_GRS_OVERLAY_PROVIDER));
|
||||||
}
|
}
|
||||||
TransformLayerMessage::BeginRotate => {
|
TransformLayerMessage::BeginRotate => {
|
||||||
let selected_points: Vec<&ManipulatorPointId> = shape_editor.selected_points().collect();
|
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.transform_operation = TransformOperation::Rotating(Default::default());
|
||||||
|
|
||||||
|
self.local = false;
|
||||||
|
self.fixed_bbox = selected.bounding_box();
|
||||||
|
|
||||||
selected.original_transforms.clear();
|
selected.original_transforms.clear();
|
||||||
|
|
||||||
|
responses.add(OverlaysMessage::AddProvider(TRANSFORM_GRS_OVERLAY_PROVIDER));
|
||||||
}
|
}
|
||||||
TransformLayerMessage::BeginScale => {
|
TransformLayerMessage::BeginScale => {
|
||||||
let selected_points: Vec<&ManipulatorPointId> = shape_editor.selected_points().collect();
|
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.transform_operation = TransformOperation::Scaling(Default::default());
|
||||||
|
|
||||||
|
self.local = false;
|
||||||
|
self.fixed_bbox = selected.bounding_box();
|
||||||
|
|
||||||
selected.original_transforms.clear();
|
selected.original_transforms.clear();
|
||||||
|
|
||||||
|
responses.add(OverlaysMessage::AddProvider(TRANSFORM_GRS_OVERLAY_PROVIDER));
|
||||||
}
|
}
|
||||||
TransformLayerMessage::CancelTransformOperation => {
|
TransformLayerMessage::CancelTransformOperation => {
|
||||||
selected.revert_operation();
|
selected.revert_operation();
|
||||||
@@ -228,21 +246,164 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
|
|||||||
|
|
||||||
responses.add(DocumentMessage::AbortTransaction);
|
responses.add(DocumentMessage::AbortTransaction);
|
||||||
responses.add(ToolMessage::UpdateHints);
|
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 } => {
|
TransformLayerMessage::PointerMove { slow_key, snap_key } => {
|
||||||
self.slow = input.keyboard.get(slow_key as usize);
|
self.slow = input.keyboard.get(slow_key as usize);
|
||||||
|
|
||||||
let new_snap = input.keyboard.get(snap_key as usize);
|
let new_snap = input.keyboard.get(snap_key as usize);
|
||||||
if new_snap != self.snap {
|
if new_snap != self.snap {
|
||||||
self.snap = new_snap;
|
self.snap = new_snap;
|
||||||
let axis_constraint = match self.transform_operation {
|
self.transform_operation
|
||||||
TransformOperation::Grabbing(grabbing) => grabbing.constraint,
|
.apply_transform_operation(&mut selected, self.snap, self.local, self.fixed_bbox, document_to_viewport);
|
||||||
TransformOperation::Scaling(scaling) => scaling.constraint,
|
|
||||||
_ => Axis::Both,
|
|
||||||
};
|
|
||||||
self.transform_operation.apply_transform_operation(&mut selected, self.snap, axis_constraint);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.typing.digits.is_empty() {
|
if self.typing.digits.is_empty() {
|
||||||
@@ -252,9 +413,9 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
|
|||||||
TransformOperation::None => unreachable!(),
|
TransformOperation::None => unreachable!(),
|
||||||
TransformOperation::Grabbing(translation) => {
|
TransformOperation::Grabbing(translation) => {
|
||||||
let change = if self.slow { delta_pos / SLOWING_DIVISOR } else { delta_pos };
|
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 = 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) => {
|
TransformOperation::Rotating(rotation) => {
|
||||||
let start_offset = *selected.pivot - self.mouse_position;
|
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 };
|
let change = if self.slow { angle / SLOWING_DIVISOR } else { angle };
|
||||||
|
|
||||||
self.transform_operation = TransformOperation::Rotating(rotation.increment_amount(change));
|
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) => {
|
TransformOperation::Scaling(scale) => {
|
||||||
let change = {
|
let change = {
|
||||||
@@ -274,11 +436,17 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
|
|||||||
|
|
||||||
(current_frame_dist - previous_frame_dist) / start_transform_dist
|
(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 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 = 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();
|
let target_layers = document.network_interface.selected_nodes(&[]).unwrap().selected_layers(document.metadata()).collect();
|
||||||
shape_editor.set_selected_layers(target_layers);
|
shape_editor.set_selected_layers(target_layers);
|
||||||
}
|
}
|
||||||
TransformLayerMessage::TypeBackspace => self.transform_operation.grs_typed(self.typing.type_backspace(), &mut selected, self.snap),
|
TransformLayerMessage::TypeBackspace => self
|
||||||
TransformLayerMessage::TypeDecimalPoint => self.transform_operation.grs_typed(self.typing.type_decimal_point(), &mut selected, self.snap),
|
.transform_operation
|
||||||
TransformLayerMessage::TypeDigit { digit } => self.transform_operation.grs_typed(self.typing.type_number(digit), &mut selected, self.snap),
|
.grs_typed(self.typing.type_backspace(), &mut selected, self.snap, self.local, self.fixed_bbox, document_to_viewport),
|
||||||
TransformLayerMessage::TypeNegate => self.transform_operation.grs_typed(self.typing.type_negate(), &mut selected, self.snap),
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ use crate::node_graph_executor::NodeGraphExecutor;
|
|||||||
use graphene_core::raster::color::Color;
|
use graphene_core::raster::color::Color;
|
||||||
use graphene_core::text::FontCache;
|
use graphene_core::text::FontCache;
|
||||||
|
|
||||||
|
use std::borrow::Cow;
|
||||||
use std::fmt::{self, Debug};
|
use std::fmt::{self, Debug};
|
||||||
|
|
||||||
pub struct ToolActionHandlerData<'a> {
|
pub struct ToolActionHandlerData<'a> {
|
||||||
@@ -492,7 +493,7 @@ pub struct HintInfo {
|
|||||||
/// No such icon is shown if `None` is given, and it can be combined with `key_groups` if desired.
|
/// No such icon is shown if `None` is given, and it can be combined with `key_groups` if desired.
|
||||||
pub mouse: Option<MouseMotion>,
|
pub mouse: Option<MouseMotion>,
|
||||||
/// The text describing what occurs with this input combination.
|
/// The text describing what occurs with this input combination.
|
||||||
pub label: String,
|
pub label: Cow<'static, str>,
|
||||||
/// Draws a prepended "+" symbol which indicates that this is a refinement upon a previous hint in the group.
|
/// Draws a prepended "+" symbol which indicates that this is a refinement upon a previous hint in the group.
|
||||||
pub plus: bool,
|
pub plus: bool,
|
||||||
/// Draws a prepended "/" symbol which indicates that this is an alternative to a previous hint in the group.
|
/// Draws a prepended "/" symbol which indicates that this is an alternative to a previous hint in the group.
|
||||||
@@ -500,7 +501,7 @@ pub struct HintInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl HintInfo {
|
impl HintInfo {
|
||||||
pub fn keys(keys: impl IntoIterator<Item = Key>, label: impl Into<String>) -> Self {
|
pub fn keys(keys: impl IntoIterator<Item = Key>, label: impl Into<Cow<'static, str>>) -> Self {
|
||||||
let keys: Vec<_> = keys.into_iter().collect();
|
let keys: Vec<_> = keys.into_iter().collect();
|
||||||
Self {
|
Self {
|
||||||
key_groups: vec![KeysGroup(keys).into()],
|
key_groups: vec![KeysGroup(keys).into()],
|
||||||
@@ -512,7 +513,7 @@ impl HintInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn multi_keys(multi_keys: impl IntoIterator<Item = impl IntoIterator<Item = Key>>, label: impl Into<String>) -> Self {
|
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();
|
let key_groups = multi_keys.into_iter().map(|keys| KeysGroup(keys.into_iter().collect()).into()).collect();
|
||||||
Self {
|
Self {
|
||||||
key_groups,
|
key_groups,
|
||||||
@@ -524,7 +525,7 @@ impl HintInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn mouse(mouse_motion: MouseMotion, label: impl Into<String>) -> Self {
|
pub fn mouse(mouse_motion: MouseMotion, label: impl Into<Cow<'static, str>>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
key_groups: vec![],
|
key_groups: vec![],
|
||||||
key_groups_mac: None,
|
key_groups_mac: None,
|
||||||
@@ -535,7 +536,7 @@ impl HintInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn label(label: impl Into<String>) -> Self {
|
pub fn label(label: impl Into<Cow<'static, str>>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
key_groups: vec![],
|
key_groups: vec![],
|
||||||
key_groups_mac: None,
|
key_groups_mac: None,
|
||||||
@@ -546,7 +547,7 @@ impl HintInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn keys_and_mouse(keys: impl IntoIterator<Item = Key>, mouse_motion: MouseMotion, label: impl Into<String>) -> Self {
|
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: Vec<_> = keys.into_iter().collect();
|
||||||
Self {
|
Self {
|
||||||
key_groups: vec![KeysGroup(keys).into()],
|
key_groups: vec![KeysGroup(keys).into()],
|
||||||
@@ -558,7 +559,7 @@ impl HintInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn multi_keys_and_mouse(multi_keys: impl IntoIterator<Item = impl IntoIterator<Item = Key>>, mouse_motion: MouseMotion, label: impl Into<String>) -> Self {
|
pub fn multi_keys_and_mouse(multi_keys: impl IntoIterator<Item = impl IntoIterator<Item = Key>>, mouse_motion: MouseMotion, label: impl Into<Cow<'static, str>>) -> Self {
|
||||||
let key_groups = multi_keys.into_iter().map(|keys| KeysGroup(keys.into_iter().collect()).into()).collect();
|
let key_groups = multi_keys.into_iter().map(|keys| KeysGroup(keys.into_iter().collect()).into()).collect();
|
||||||
Self {
|
Self {
|
||||||
key_groups,
|
key_groups,
|
||||||
@@ -570,7 +571,7 @@ impl HintInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn arrow_keys(label: impl Into<String>) -> Self {
|
pub fn arrow_keys(label: impl Into<Cow<'static, str>>) -> Self {
|
||||||
let multi_keys = [[Key::ArrowUp], [Key::ArrowRight], [Key::ArrowDown], [Key::ArrowLeft]];
|
let multi_keys = [[Key::ArrowUp], [Key::ArrowRight], [Key::ArrowDown], [Key::ArrowLeft]];
|
||||||
Self::multi_keys(multi_keys, label)
|
Self::multi_keys(multi_keys, label)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user