mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-30 09:18: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
@@ -1029,9 +1029,8 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
|
||||
self.graph_fade_artwork_percentage = percentage;
|
||||
responses.add(FrontendMessage::UpdateGraphFadeArtwork { percentage });
|
||||
}
|
||||
|
||||
DocumentMessage::SetNodePinned { node_id, pinned } => {
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
responses.add(DocumentMessage::AddTransaction);
|
||||
responses.add(NodeGraphMessage::SetPinned { node_id, pinned });
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
responses.add(NodeGraphMessage::SelectedNodesUpdated);
|
||||
@@ -1059,6 +1058,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
|
||||
DocumentMessage::SetToNodeOrLayer { node_id, is_layer } => {
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
responses.add(NodeGraphMessage::SetToNodeOrLayer { node_id, is_layer });
|
||||
responses.add(DocumentMessage::EndTransaction);
|
||||
}
|
||||
DocumentMessage::SetViewMode { 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_mac: None,
|
||||
mouse: None,
|
||||
label: String::from("Snap 15°"),
|
||||
label: "Snap 15°".into(),
|
||||
plus: false,
|
||||
slash: false,
|
||||
}]),
|
||||
@@ -129,7 +129,7 @@ impl MessageHandler<NavigationMessage, NavigationMessageData<'_>> for Navigation
|
||||
key_groups: vec![KeysGroup(vec![Key::Control]).into()],
|
||||
key_groups_mac: None,
|
||||
mouse: None,
|
||||
label: String::from("Increments"),
|
||||
label: "Increments".into(),
|
||||
plus: false,
|
||||
slash: false,
|
||||
}]),
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
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 bezier_rs::{Bezier, Subpath};
|
||||
@@ -188,6 +190,65 @@ impl OverlayContext {
|
||||
self.render_context.fill();
|
||||
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) {
|
||||
let (x, y) = (position.round() - DVec2::splat(0.5)).into();
|
||||
|
||||
@@ -300,6 +361,10 @@ impl OverlayContext {
|
||||
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]) {
|
||||
let metrics = self.render_context.measure_text(text).expect("Failed to measure the text dimensions");
|
||||
let x = match pivot[0] {
|
||||
|
||||
@@ -122,14 +122,15 @@ pub enum Axis {
|
||||
}
|
||||
|
||||
impl Axis {
|
||||
pub fn set_or_toggle(&mut self, target: Axis) {
|
||||
// If constrained to an axis and target is requesting the same axis, toggle back to Both
|
||||
if *self == target {
|
||||
*self = Axis::Both;
|
||||
pub fn contrainted_to_axis(self, target: Axis, local: bool) -> (Self, bool) {
|
||||
if self != target {
|
||||
return (target, false);
|
||||
}
|
||||
// If current axis is different from the target axis, switch to the target
|
||||
else {
|
||||
*self = target;
|
||||
|
||||
if local {
|
||||
(Axis::Both, false)
|
||||
} else {
|
||||
(self, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -142,20 +143,17 @@ pub struct 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 self.constraint == Axis::Y {
|
||||
return DVec2::new(0., value);
|
||||
} else {
|
||||
return DVec2::new(value, 0.);
|
||||
let document_displacement = if self.constraint == Axis::Y { DVec2::new(0., value) } else { DVec2::new(value, 0.) };
|
||||
transform.transform_vector2(document_displacement)
|
||||
} else {
|
||||
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]
|
||||
@@ -173,6 +171,11 @@ impl Translation {
|
||||
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)]
|
||||
@@ -206,6 +209,11 @@ impl Rotation {
|
||||
typed_angle: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn negate(self) -> Self {
|
||||
let dragged_angle = -self.dragged_angle;
|
||||
Self { dragged_angle, ..self }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Copy)]
|
||||
@@ -226,9 +234,17 @@ impl Default for 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 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 {
|
||||
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]
|
||||
pub fn increment_amount(self, delta: f64) -> Self {
|
||||
Self {
|
||||
@@ -253,6 +274,11 @@ impl Scale {
|
||||
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)]
|
||||
@@ -265,32 +291,51 @@ pub enum 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 {
|
||||
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::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!(),
|
||||
};
|
||||
|
||||
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) {
|
||||
match self {
|
||||
TransformOperation::None => (),
|
||||
TransformOperation::Grabbing(translation) => translation.constraint.set_or_toggle(axis),
|
||||
TransformOperation::Rotating(_) => (),
|
||||
TransformOperation::Scaling(scale) => scale.constraint.set_or_toggle(axis),
|
||||
pub fn constrain_axis(&mut self, axis: Axis, selected: &mut Selected, snapping: bool, mut local: bool, quad: Quad, transform: DAffine2) -> bool {
|
||||
(*self, local) = match self {
|
||||
TransformOperation::Grabbing(translation) => {
|
||||
let (translation, local) = translation.with_constraint(axis, local);
|
||||
(TransformOperation::Grabbing(translation), local)
|
||||
}
|
||||
TransformOperation::Scaling(scale) => {
|
||||
let (scale, local) = scale.with_constraint(axis, local);
|
||||
(TransformOperation::Scaling(scale), local)
|
||||
}
|
||||
_ => (*self, false),
|
||||
};
|
||||
|
||||
self.apply_transform_operation(selected, snapping, axis);
|
||||
self.apply_transform_operation(selected, snapping, local, quad, transform);
|
||||
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 {
|
||||
TransformOperation::None => (),
|
||||
TransformOperation::Grabbing(translation) => translation.typed_distance = typed,
|
||||
@@ -298,16 +343,10 @@ impl TransformOperation {
|
||||
TransformOperation::Scaling(scale) => scale.typed_factor = typed,
|
||||
};
|
||||
|
||||
let axis_constraint = match self {
|
||||
TransformOperation::Grabbing(grabbing) => grabbing.constraint,
|
||||
TransformOperation::Scaling(scaling) => scaling.constraint,
|
||||
_ => Axis::Both,
|
||||
};
|
||||
|
||||
self.apply_transform_operation(selected, snapping, axis_constraint);
|
||||
self.apply_transform_operation(selected, snapping, local, quad, transform);
|
||||
}
|
||||
|
||||
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::tool::utility_types::{HintData, HintGroup, HintInfo};
|
||||
|
||||
@@ -321,25 +360,20 @@ impl TransformOperation {
|
||||
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 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)]);
|
||||
let hint_data = HintData(vec![HintGroup(input_hints)]);
|
||||
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> {
|
||||
@@ -402,6 +436,32 @@ impl<'a> Selected<'a> {
|
||||
(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>) {
|
||||
let Some(&original_transform) = original_transform else { return };
|
||||
let to = document_metadata.downstream_transform_to_viewport(layer);
|
||||
|
||||
Reference in New Issue
Block a user