Make the transform cage show/hide resize grips as space allows (#2209)
* Changes rotation handles to be around overlay squares Fixes https://discord.com/channels/731730685944922173/931942323644928040/1330785941786329209 * Fix zero width objects not being selected by slightly nudging the transform * Follow the categorical limits to render overlay quads As discussed here: https://discord.com/channels/731730685944922173/931942323644928040/1331166336923074600 * Replace area based calculations with edge based calculations * Fix 3rd category vis * Code review * Add missing powi(2) * Fixes to handle logic * Remove single axis prioritisation * Explicitly check for distance to find nearest handle * Replace threshold check based on corner vis bounds * Fix discrepancy at h=12px * Allow grab when box is too small by disabling resizing within bounds * Replace inside resize pixel limit * Code review --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
parent
c5a3c32114
commit
a0f8f89e71
|
|
@ -1,11 +1,11 @@
|
|||
// Graph
|
||||
// GRAPH
|
||||
pub const GRID_SIZE: u32 = 24;
|
||||
pub const EXPORTS_TO_TOP_EDGE_PIXEL_GAP: u32 = 72;
|
||||
pub const EXPORTS_TO_RIGHT_EDGE_PIXEL_GAP: u32 = 120;
|
||||
pub const IMPORTS_TO_TOP_EDGE_PIXEL_GAP: u32 = 72;
|
||||
pub const IMPORTS_TO_LEFT_EDGE_PIXEL_GAP: u32 = 120;
|
||||
|
||||
// Viewport
|
||||
// VIEWPORT
|
||||
pub const VIEWPORT_ZOOM_WHEEL_RATE: f64 = (1. / 600.) * 3.;
|
||||
pub const VIEWPORT_ZOOM_MOUSE_RATE: f64 = 1. / 400.;
|
||||
pub const VIEWPORT_ZOOM_SCALE_MIN: f64 = 0.000_000_1;
|
||||
|
|
@ -17,7 +17,8 @@ pub const VIEWPORT_ZOOM_LEVELS: [f64; 74] = [
|
|||
128., 160., 200., 256., 320., 400., 512., 640., 800., 1024., 1280., 1600., 2048., 2560.,
|
||||
];
|
||||
|
||||
pub const VIEWPORT_GRID_ROUNDING_BIAS: f64 = 0.002; // Helps push values that end in approximately half, plus or minus some floating point imprecision, towards the same side of the round() function
|
||||
/// Helps push values that end in approximately half, plus or minus some floating point imprecision, towards the same side of the round() function.
|
||||
pub const VIEWPORT_GRID_ROUNDING_BIAS: f64 = 0.002;
|
||||
|
||||
pub const VIEWPORT_SCROLL_RATE: f64 = 0.6;
|
||||
|
||||
|
|
@ -28,76 +29,82 @@ pub const VIEWPORT_ZOOM_TO_FIT_PADDING_SCALE_FACTOR: f64 = 0.95;
|
|||
pub const DRAG_BEYOND_VIEWPORT_MAX_OVEREXTENSION_PIXELS: f64 = 50.;
|
||||
pub const DRAG_BEYOND_VIEWPORT_SPEED_FACTOR: f64 = 20.;
|
||||
|
||||
// Snapping point
|
||||
// SNAPPING POINT
|
||||
pub const SNAP_POINT_TOLERANCE: f64 = 5.;
|
||||
pub const MAX_ALIGNMENT_CANDIDATES: usize = 100; // These are layers whose bounding boxes are used for alignment.
|
||||
pub const MAX_SNAP_CANDIDATES: usize = 10; // These are layers that are used for the layer snapper
|
||||
pub const MAX_LAYER_SNAP_POINTS: usize = 100; // These are points (anchors and bounding box corners etc.) in the layer snapper
|
||||
/// These are layers whose bounding boxes are used for alignment.
|
||||
pub const MAX_ALIGNMENT_CANDIDATES: usize = 100;
|
||||
/// These are layers that are used for the layer snapper.
|
||||
pub const MAX_SNAP_CANDIDATES: usize = 10;
|
||||
/// These are points (anchors and bounding box corners etc.) in the layer snapper.
|
||||
pub const MAX_LAYER_SNAP_POINTS: usize = 100;
|
||||
|
||||
pub const DRAG_THRESHOLD: f64 = 1.;
|
||||
|
||||
// Transforming layer
|
||||
// TRANSFORMING LAYER
|
||||
pub const ROTATE_SNAP_ANGLE: f64 = 15.;
|
||||
pub const SCALE_SNAP_INTERVAL: f64 = 0.1;
|
||||
pub const SLOWING_DIVISOR: f64 = 10.;
|
||||
pub const NUDGE_AMOUNT: f64 = 1.;
|
||||
pub const BIG_NUDGE_AMOUNT: f64 = 10.;
|
||||
|
||||
// Tools
|
||||
// TOOLS
|
||||
pub const DEFAULT_STROKE_WIDTH: f64 = 2.;
|
||||
|
||||
// Select tool
|
||||
// SELECT TOOL
|
||||
pub const SELECTION_TOLERANCE: f64 = 5.;
|
||||
pub const SELECTION_DRAG_ANGLE: f64 = 90.;
|
||||
pub const PIVOT_CROSSHAIR_THICKNESS: f64 = 1.;
|
||||
pub const PIVOT_CROSSHAIR_LENGTH: f64 = 9.;
|
||||
pub const PIVOT_DIAMETER: f64 = 5.;
|
||||
|
||||
// Transform overlay
|
||||
// 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
|
||||
// TRANSFORM CAGE
|
||||
pub const BOUNDS_SELECT_THRESHOLD: f64 = 10.;
|
||||
pub const BOUNDS_ROTATE_THRESHOLD: f64 = 20.;
|
||||
pub const MIN_LENGTH_FOR_MIDPOINT_VISIBILITY: f64 = 20.;
|
||||
pub const MIN_LENGTH_FOR_CORNERS_VISIBILITY: f64 = 12.;
|
||||
/// When the width or height of the transform cage is less than this value, only the exterior of the bounding box will act as a click target for resizing.
|
||||
pub const MIN_LENGTH_FOR_RESIZE_TO_INCLUDE_INTERIOR: f64 = 40.;
|
||||
|
||||
// Path tool
|
||||
// PATH TOOL
|
||||
pub const MANIPULATOR_GROUP_MARKER_SIZE: f64 = 6.;
|
||||
pub const SELECTION_THRESHOLD: f64 = 10.;
|
||||
pub const HIDE_HANDLE_DISTANCE: f64 = 3.;
|
||||
pub const INSERT_POINT_ON_SEGMENT_TOO_FAR_DISTANCE: f64 = 50.;
|
||||
pub const HANDLE_ROTATE_SNAP_ANGLE: f64 = 15.;
|
||||
|
||||
// Pen tool
|
||||
// PEN TOOL
|
||||
pub const CREATE_CURVE_THRESHOLD: f64 = 5.;
|
||||
|
||||
// Spline tool
|
||||
// SPLINE TOOL
|
||||
pub const PATH_JOIN_THRESHOLD: f64 = 5.;
|
||||
|
||||
// Line tool
|
||||
// LINE TOOL
|
||||
pub const LINE_ROTATE_SNAP_ANGLE: f64 = 15.;
|
||||
|
||||
// Brush tool
|
||||
// BRUSH TOOL
|
||||
pub const BRUSH_SIZE_CHANGE_KEYBOARD: f64 = 5.;
|
||||
pub const DEFAULT_BRUSH_SIZE: f64 = 20.;
|
||||
|
||||
// Scrollbars
|
||||
// SCROLLBARS
|
||||
pub const SCROLLBAR_SPACING: f64 = 0.1;
|
||||
pub const ASYMPTOTIC_EFFECT: f64 = 0.5;
|
||||
pub const SCALE_EFFECT: f64 = 0.5;
|
||||
|
||||
// Colors
|
||||
// Keep changes to these colors updated with `Editor.svelte`
|
||||
// COLORS
|
||||
pub const COLOR_OVERLAY_BLUE: &str = "#00a8ff";
|
||||
pub const COLOR_OVERLAY_YELLOW: &str = "#ffc848";
|
||||
pub const COLOR_OVERLAY_GREEN: &str = "#63ce63";
|
||||
pub const COLOR_OVERLAY_RED: &str = "#ef5454";
|
||||
pub const COLOR_OVERLAY_GRAY: &str = "#cccccc";
|
||||
pub const COLOR_OVERLAY_WHITE: &str = "#ffffff";
|
||||
pub const COLOR_OVERLAY_SNAP_BACKGROUND: &str = "#000000cc";
|
||||
pub const COLOR_OVERLAY_LABEL_BACKGROUND: &str = "#000000cc";
|
||||
pub const COLOR_OVERLAY_TRANSPARENT: &str = "#ffffff00";
|
||||
|
||||
// Document
|
||||
// DOCUMENT
|
||||
pub const DEFAULT_DOCUMENT_NAME: &str = "Untitled Document";
|
||||
pub const FILE_SAVE_SUFFIX: &str = ".graphite";
|
||||
pub const MAX_UNDO_HISTORY_LEN: usize = 100; // TODO: Add this to user preferences
|
||||
|
|
|
|||
|
|
@ -1296,7 +1296,7 @@ impl NodeNetworkInterface {
|
|||
let clip_input = artboard.unwrap().inputs.get(5).unwrap();
|
||||
if let NodeInput::Value { tagged_value, .. } = clip_input {
|
||||
if tagged_value.to_primitive_string() == "true" {
|
||||
return Some(Quad::constraint_bounds(
|
||||
return Some(Quad::clip(
|
||||
self.document_metadata.bounding_box_document(layer).unwrap_or_default(),
|
||||
self.document_metadata.bounding_box_document(artboard_node_identifier).unwrap_or_default(),
|
||||
));
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ mod layer_snapper;
|
|||
mod snap_results;
|
||||
pub use {alignment_snapper::*, distribution_snapper::*, grid_snapper::*, layer_snapper::*, snap_results::*};
|
||||
|
||||
use crate::consts::{COLOR_OVERLAY_BLUE, COLOR_OVERLAY_SNAP_BACKGROUND, COLOR_OVERLAY_WHITE};
|
||||
use crate::consts::{COLOR_OVERLAY_BLUE, COLOR_OVERLAY_LABEL_BACKGROUND, COLOR_OVERLAY_WHITE};
|
||||
use crate::messages::portfolio::document::overlays::utility_types::{OverlayContext, Pivot};
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use crate::messages::portfolio::document::utility_types::misc::{GridSnapTarget, PathSnapTarget, SnapTarget};
|
||||
|
|
@ -470,7 +470,7 @@ impl SnapManager {
|
|||
if !any_align && ind.distribution_equal_distance_x.is_none() && ind.distribution_equal_distance_y.is_none() {
|
||||
let text = format!("[{}] from [{}]", ind.target, ind.source);
|
||||
let transform = DAffine2::from_translation(viewport - DVec2::new(0., 4.));
|
||||
overlay_context.text(&text, COLOR_OVERLAY_WHITE, Some(COLOR_OVERLAY_SNAP_BACKGROUND), transform, 4., [Pivot::Start, Pivot::End]);
|
||||
overlay_context.text(&text, COLOR_OVERLAY_WHITE, Some(COLOR_OVERLAY_LABEL_BACKGROUND), transform, 4., [Pivot::Start, Pivot::End]);
|
||||
overlay_context.square(viewport, Some(4.), Some(COLOR_OVERLAY_BLUE), Some(COLOR_OVERLAY_BLUE));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
use crate::consts::{BOUNDS_ROTATE_THRESHOLD, BOUNDS_SELECT_THRESHOLD, SELECTION_DRAG_ANGLE};
|
||||
use crate::consts::{
|
||||
BOUNDS_ROTATE_THRESHOLD, BOUNDS_SELECT_THRESHOLD, MIN_LENGTH_FOR_CORNERS_VISIBILITY, MIN_LENGTH_FOR_MIDPOINT_VISIBILITY, MIN_LENGTH_FOR_RESIZE_TO_INCLUDE_INTERIOR, SELECTION_DRAG_ANGLE,
|
||||
};
|
||||
use crate::messages::frontend::utility_types::MouseCursorIcon;
|
||||
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
|
||||
use crate::messages::portfolio::document::utility_types::transformation::OriginalTransforms;
|
||||
|
|
@ -30,6 +32,17 @@ pub struct SelectedEdges {
|
|||
aspect_ratio: f64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
enum HandleDisplayCategory {
|
||||
#[default]
|
||||
Full,
|
||||
ReducedLandscape,
|
||||
ReducedPortrait,
|
||||
ReducedBoth,
|
||||
Narrow,
|
||||
Flat,
|
||||
}
|
||||
|
||||
impl SelectedEdges {
|
||||
pub fn new(top: bool, bottom: bool, left: bool, right: bool, bounds: [DVec2; 2]) -> Self {
|
||||
let size = (bounds[0] - bounds[1]).abs();
|
||||
|
|
@ -287,24 +300,78 @@ impl BoundingBoxManager {
|
|||
let (left, top): (f64, f64) = self.bounds[0].into();
|
||||
let (right, bottom): (f64, f64) = self.bounds[1].into();
|
||||
[
|
||||
self.transform.transform_point2(DVec2::new(left, top)),
|
||||
self.transform.transform_point2(DVec2::new(left, (top + bottom) / 2.)),
|
||||
self.transform.transform_point2(DVec2::new(left, bottom)),
|
||||
self.transform.transform_point2(DVec2::new((left + right) / 2., top)),
|
||||
self.transform.transform_point2(DVec2::new((left + right) / 2., bottom)),
|
||||
self.transform.transform_point2(DVec2::new(right, top)),
|
||||
self.transform.transform_point2(DVec2::new(right, (top + bottom) / 2.)),
|
||||
self.transform.transform_point2(DVec2::new(right, bottom)),
|
||||
DVec2::new(left, top),
|
||||
DVec2::new(left, (top + bottom) / 2.),
|
||||
DVec2::new(left, bottom),
|
||||
DVec2::new((left + right) / 2., top),
|
||||
DVec2::new((left + right) / 2., bottom),
|
||||
DVec2::new(right, top),
|
||||
DVec2::new(right, (top + bottom) / 2.),
|
||||
DVec2::new(right, bottom),
|
||||
]
|
||||
}
|
||||
|
||||
/// Update the position of the bounding box and transform handles
|
||||
pub fn render_overlays(&mut self, overlay_context: &mut OverlayContext) {
|
||||
overlay_context.quad(self.transform * Quad::from_box(self.bounds), None);
|
||||
let quad = self.transform * Quad::from_box(self.bounds);
|
||||
let category = self.overlay_display_category(quad);
|
||||
|
||||
for position in self.evaluate_transform_handle_positions() {
|
||||
overlay_context.square(position, Some(6.), None, None);
|
||||
let horizontal_edges = [quad.top_right().midpoint(quad.bottom_right()), quad.bottom_left().midpoint(quad.top_left())];
|
||||
let vertical_edges = [quad.top_left().midpoint(quad.top_right()), quad.bottom_right().midpoint(quad.bottom_left())];
|
||||
|
||||
// Draw the bounding box rectangle
|
||||
overlay_context.quad(quad, None);
|
||||
|
||||
let mut draw_handle = |point: DVec2| overlay_context.square(point, Some(6.), None, None);
|
||||
|
||||
// Draw the horizontal midpoint drag handles
|
||||
if matches!(category, HandleDisplayCategory::Full | HandleDisplayCategory::Narrow | HandleDisplayCategory::ReducedLandscape) {
|
||||
horizontal_edges.map(&mut draw_handle);
|
||||
}
|
||||
|
||||
// Draw the vertical midpoint drag handles
|
||||
if matches!(category, HandleDisplayCategory::Full | HandleDisplayCategory::Narrow | HandleDisplayCategory::ReducedPortrait) {
|
||||
vertical_edges.map(&mut draw_handle);
|
||||
}
|
||||
|
||||
// Draw the corner drag handles
|
||||
if matches!(
|
||||
category,
|
||||
HandleDisplayCategory::Full | HandleDisplayCategory::ReducedBoth | HandleDisplayCategory::ReducedLandscape | HandleDisplayCategory::ReducedPortrait
|
||||
) {
|
||||
quad.0.map(&mut draw_handle);
|
||||
}
|
||||
|
||||
// Draw the flat line endpoint drag handles
|
||||
if category == HandleDisplayCategory::Flat {
|
||||
draw_handle(self.transform.transform_point2(self.bounds[0]));
|
||||
draw_handle(self.transform.transform_point2(self.bounds[1]));
|
||||
}
|
||||
}
|
||||
|
||||
fn overlay_display_category(&self, quad: Quad) -> HandleDisplayCategory {
|
||||
// Check if the area is essentially zero because either the width or height is smaller than an epsilon
|
||||
if (self.bounds[0] - self.bounds[1]).abs().cmple(DVec2::splat(1e-4)).any() {
|
||||
return HandleDisplayCategory::Flat;
|
||||
}
|
||||
|
||||
let vertical_length = (quad.top_left() - quad.top_right()).length_squared();
|
||||
let horizontal_length = (quad.bottom_left() - quad.top_left()).length_squared();
|
||||
let corners_visible = vertical_length >= MIN_LENGTH_FOR_CORNERS_VISIBILITY.powi(2) && horizontal_length >= MIN_LENGTH_FOR_CORNERS_VISIBILITY.powi(2);
|
||||
|
||||
if corners_visible {
|
||||
let vertical_edge_visible = vertical_length > MIN_LENGTH_FOR_MIDPOINT_VISIBILITY.powi(2);
|
||||
let horizontal_edge_visible = horizontal_length > MIN_LENGTH_FOR_MIDPOINT_VISIBILITY.powi(2);
|
||||
|
||||
return match (vertical_edge_visible, horizontal_edge_visible) {
|
||||
(true, true) => HandleDisplayCategory::Full,
|
||||
(true, false) => HandleDisplayCategory::ReducedPortrait,
|
||||
(false, true) => HandleDisplayCategory::ReducedLandscape,
|
||||
(false, false) => HandleDisplayCategory::ReducedBoth,
|
||||
};
|
||||
}
|
||||
|
||||
HandleDisplayCategory::Narrow
|
||||
}
|
||||
|
||||
/// Compute the threshold in viewport space. This only works with affine transforms as it assumes lines remain parallel.
|
||||
|
|
@ -313,18 +380,27 @@ impl BoundingBoxManager {
|
|||
|
||||
let viewport_x = self.transform.transform_vector2(DVec2::X).normalize_or_zero() * scalar;
|
||||
let viewport_y = self.transform.transform_vector2(DVec2::Y).normalize_or_zero() * scalar;
|
||||
|
||||
let threshold_x = inverse.transform_vector2(viewport_x).length();
|
||||
let threshold_y = inverse.transform_vector2(viewport_y).length();
|
||||
|
||||
[threshold_x, threshold_y]
|
||||
}
|
||||
|
||||
/// Check if the user has selected the edge for dragging (returns which edge in order top, bottom, left, right)
|
||||
/// Check if the user has selected the edge for dragging.
|
||||
///
|
||||
/// Returns which edge in the order:
|
||||
///
|
||||
/// `top, bottom, left, right`
|
||||
pub fn check_selected_edges(&self, cursor: DVec2) -> Option<(bool, bool, bool, bool)> {
|
||||
let cursor = self.transform.inverse().transform_point2(cursor);
|
||||
|
||||
let min = self.bounds[0].min(self.bounds[1]);
|
||||
let max = self.bounds[0].max(self.bounds[1]);
|
||||
|
||||
let [threshold_x, threshold_y] = self.compute_viewport_threshold(BOUNDS_SELECT_THRESHOLD);
|
||||
let [corner_min_x, corner_min_y] = self.compute_viewport_threshold(MIN_LENGTH_FOR_CORNERS_VISIBILITY);
|
||||
let [edge_min_x, edge_min_y] = self.compute_viewport_threshold(MIN_LENGTH_FOR_RESIZE_TO_INCLUDE_INTERIOR);
|
||||
|
||||
if min.x - cursor.x < threshold_x && min.y - cursor.y < threshold_y && cursor.x - max.x < threshold_x && cursor.y - max.y < threshold_y {
|
||||
let mut top = (cursor.y - min.y).abs() < threshold_y;
|
||||
|
|
@ -332,24 +408,33 @@ impl BoundingBoxManager {
|
|||
let mut left = (cursor.x - min.x).abs() < threshold_x;
|
||||
let mut right = (max.x - cursor.x).abs() < threshold_x;
|
||||
|
||||
// Prioritise single axis transformations on very small bounds
|
||||
if cursor.y - min.y + max.y - cursor.y < threshold_y * 2. && (left || right) {
|
||||
top = false;
|
||||
bottom = false;
|
||||
}
|
||||
if cursor.x - min.x + max.x - cursor.x < threshold_x * 2. && (top || bottom) {
|
||||
left = false;
|
||||
right = false;
|
||||
}
|
||||
let width = max.x - min.x;
|
||||
let height = max.y - min.y;
|
||||
|
||||
// On bounds with no width/height, disallow transformation in the relevant axis
|
||||
if (max.x - min.x) < f64::EPSILON * 1000. {
|
||||
left = false;
|
||||
right = false;
|
||||
}
|
||||
if (max.y - min.y) < f64::EPSILON * 1000. {
|
||||
top = false;
|
||||
bottom = false;
|
||||
if width < edge_min_x || height <= edge_min_y {
|
||||
if min.x < cursor.x && cursor.x < max.x && cursor.y < max.y && cursor.y > min.y {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Prioritize single axis transformations on very small bounds
|
||||
if height < corner_min_y && (left || right) {
|
||||
top = false;
|
||||
bottom = false;
|
||||
}
|
||||
if width < corner_min_x && (top || bottom) {
|
||||
left = false;
|
||||
right = false;
|
||||
}
|
||||
|
||||
// On bounds with no width/height, disallow transformation in the relevant axis
|
||||
if width < f64::EPSILON * 1000. {
|
||||
left = false;
|
||||
right = false;
|
||||
}
|
||||
if height < f64::EPSILON * 1000. {
|
||||
top = false;
|
||||
bottom = false;
|
||||
}
|
||||
}
|
||||
|
||||
if top || bottom || left || right {
|
||||
|
|
@ -365,19 +450,19 @@ impl BoundingBoxManager {
|
|||
let cursor = self.transform.inverse().transform_point2(cursor);
|
||||
let [threshold_x, threshold_y] = self.compute_viewport_threshold(BOUNDS_ROTATE_THRESHOLD);
|
||||
|
||||
let min = self.bounds[0].min(self.bounds[1]);
|
||||
let max = self.bounds[0].max(self.bounds[1]);
|
||||
|
||||
let outside_bounds = (min.x > cursor.x || cursor.x > max.x) || (min.y > cursor.y || cursor.y > max.y);
|
||||
let inside_extended_bounds = min.x - cursor.x < threshold_x && min.y - cursor.y < threshold_y && cursor.x - max.x < threshold_x && cursor.y - max.y < threshold_y;
|
||||
|
||||
outside_bounds & inside_extended_bounds
|
||||
let narrow = (self.bounds[0] - self.bounds[1]).abs().cmple(DVec2::splat(1e-4)).any();
|
||||
let within_square_bounds = |center: &DVec2| center.x - threshold_x < cursor.x && cursor.x < center.x + threshold_x && center.y - threshold_y < cursor.y && cursor.y < center.y + threshold_y;
|
||||
if narrow {
|
||||
[self.bounds[0], self.bounds[1]].iter().any(within_square_bounds)
|
||||
} else {
|
||||
self.evaluate_transform_handle_positions().iter().any(within_square_bounds)
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the required mouse cursor to show resizing bounds or optionally rotation
|
||||
pub fn get_cursor(&self, input: &InputPreprocessorMessageHandler, rotate: bool) -> MouseCursorIcon {
|
||||
if let Some(directions) = self.check_selected_edges(input.mouse.position) {
|
||||
match directions {
|
||||
if let Some((top, bottom, left, right)) = self.check_selected_edges(input.mouse.position) {
|
||||
match (top, bottom, left, right) {
|
||||
(true, _, false, false) | (_, true, false, false) => MouseCursorIcon::NSResize,
|
||||
(false, false, true, _) | (false, false, _, true) => MouseCursorIcon::EWResize,
|
||||
(true, _, true, _) | (_, true, _, true) => MouseCursorIcon::NWSEResize,
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ use graphene_core::text::load_face;
|
|||
use graphene_std::renderer::Rect;
|
||||
use graphene_std::vector::misc::BooleanOperation;
|
||||
|
||||
use glam::DMat2;
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Default)]
|
||||
|
|
@ -456,10 +457,13 @@ impl Fsm for SelectToolFsmState {
|
|||
.selected_visible_and_unlocked_layers(&document.network_interface)
|
||||
.find(|layer| !document.network_interface.is_artboard(&layer.to_node(), &[]))
|
||||
.map(|layer| document.metadata().transform_to_viewport(layer));
|
||||
let transform = transform.unwrap_or(DAffine2::IDENTITY);
|
||||
|
||||
// Check if the matrix is not invertible
|
||||
let mut transform = transform.unwrap_or(DAffine2::IDENTITY);
|
||||
if transform.matrix2.determinant() == 0. {
|
||||
return self;
|
||||
transform.matrix2 += DMat2::IDENTITY * 1e-4; // TODO: Is this the cleanest way to handle this?
|
||||
}
|
||||
|
||||
let bounds = document
|
||||
.network_interface
|
||||
.selected_nodes(&[])
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
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::consts::{ANGLE_MEASURE_RADIUS_FACTOR, ARC_MEASURE_RADIUS_FACTOR_RANGE, COLOR_OVERLAY_BLUE, COLOR_OVERLAY_LABEL_BACKGROUND, COLOR_OVERLAY_WHITE, SLOWING_DIVISOR};
|
||||
use crate::messages::input_mapper::utility_types::input_mouse::ViewportPosition;
|
||||
use crate::messages::portfolio::document::overlays::utility_types::{OverlayProvider, Pivot};
|
||||
use crate::messages::portfolio::document::utility_types::transformation::{Axis, OriginalTransforms, Selected, TransformOperation, Typing};
|
||||
|
|
@ -477,7 +477,7 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
|
|||
}
|
||||
}
|
||||
|
||||
overlay_context.text(&grs_value_text, COLOR_OVERLAY_WHITE, Some(COLOR_OVERLAY_SNAP_BACKGROUND), transform, 4., [Pivot::Start, Pivot::End]);
|
||||
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 } => {
|
||||
|
|
|
|||
|
|
@ -105,16 +105,6 @@
|
|||
--color-error-red: #d6536e;
|
||||
--color-error-red-rgb: 214, 83, 110;
|
||||
|
||||
// Keep changes to these colors updated with `editor/src/consts.rs`
|
||||
--color-overlay-blue: #00a8ff;
|
||||
--color-overlay-blue-rgb: 0, 168, 255;
|
||||
--color-overlay-yellow: #ffc848;
|
||||
--color-overlay-yellow-rgb: 255, 200, 72;
|
||||
--color-overlay-white: #ffffff;
|
||||
--color-overlay-white-rgb: 255, 255, 255;
|
||||
--color-overlay-gray: #cccccc;
|
||||
--color-overlay-gray-rgb: 204, 204, 204;
|
||||
|
||||
--color-data-general: #c5c5c5;
|
||||
--color-data-general-dim: #767676;
|
||||
--color-data-raster: #e4bb72;
|
||||
|
|
|
|||
|
|
@ -1500,8 +1500,10 @@
|
|||
.box-selection {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
background: rgba(var(--color-overlay-blue-rgb), 0.05);
|
||||
border: 1px solid var(--color-overlay-blue);
|
||||
z-index: 2;
|
||||
// TODO: This will be removed after box selection, and all of graph rendering, is moved to the backend and this whole file
|
||||
// is removed, but for now this color needs to stay in sync with `COLOR_OVERLAY_BLUE` set in consts.rs of the editor backend.
|
||||
background: rgba(0, 168, 255, 0.05);
|
||||
border: 1px solid #00a8ff;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ use bezier_rs::Subpath;
|
|||
use dyn_any::DynAny;
|
||||
|
||||
use base64::Engine;
|
||||
use glam::{DAffine2, DVec2};
|
||||
use glam::{DAffine2, DMat2, DVec2};
|
||||
use num_traits::Zero;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::fmt::Write;
|
||||
|
|
@ -60,9 +60,11 @@ impl ClickTarget {
|
|||
/// Does the click target intersect the path
|
||||
pub fn intersect_path<It: Iterator<Item = bezier_rs::Bezier>>(&self, mut bezier_iter: impl FnMut() -> It, layer_transform: DAffine2) -> bool {
|
||||
// Check if the matrix is not invertible
|
||||
let mut layer_transform = layer_transform;
|
||||
if layer_transform.matrix2.determinant().abs() <= f64::EPSILON {
|
||||
return false;
|
||||
layer_transform.matrix2 += DMat2::IDENTITY * 1e-4; // TODO: Is this the cleanest way to handle this?
|
||||
}
|
||||
|
||||
let inverse = layer_transform.inverse();
|
||||
let mut bezier_iter = || bezier_iter().map(|bezier| bezier.apply_transformation(|point| inverse.transform_point2(point)));
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +1,38 @@
|
|||
use glam::{DAffine2, DVec2};
|
||||
|
||||
#[derive(Debug, Clone, Default, Copy)]
|
||||
/// A quad defined by four vertices.
|
||||
/// A quad defined by four vertices. Clockwise from the top left:
|
||||
///
|
||||
/// `top_left`, `top_right`, `bottom_right`, `bottom_left`.
|
||||
pub struct Quad(pub [DVec2; 4]);
|
||||
|
||||
impl Quad {
|
||||
/// Create a zero sized quad at the point
|
||||
/// Get the top left corner of the quad.
|
||||
pub fn top_left(&self) -> DVec2 {
|
||||
self.0[0]
|
||||
}
|
||||
|
||||
/// Get the top right corner of the quad.
|
||||
pub fn top_right(&self) -> DVec2 {
|
||||
self.0[1]
|
||||
}
|
||||
|
||||
/// Get the bottom right corner of the quad.
|
||||
pub fn bottom_right(&self) -> DVec2 {
|
||||
self.0[2]
|
||||
}
|
||||
|
||||
/// Get the bottom left corner of the quad.
|
||||
pub fn bottom_left(&self) -> DVec2 {
|
||||
self.0[3]
|
||||
}
|
||||
|
||||
/// Create a zero-sized quad at the point.
|
||||
pub fn from_point(point: DVec2) -> Self {
|
||||
Self([point; 4])
|
||||
}
|
||||
|
||||
/// Convert a box defined by two corner points to a quad.
|
||||
/// Convert a box defined by two corner points to a quad. The points must be given as `minimum (top left)` then `maximum (bottom right)`.
|
||||
pub fn from_box(bbox: [DVec2; 2]) -> Self {
|
||||
let size = bbox[1] - bbox[0];
|
||||
Self([bbox[0], bbox[0] + size * DVec2::X, bbox[1], bbox[0] + size * DVec2::Y])
|
||||
|
|
@ -49,8 +71,8 @@ impl Quad {
|
|||
[a[0].min(b[0]), a[1].max(b[1])]
|
||||
}
|
||||
|
||||
/// "Clip" bounds of 'a' to the limits of 'b'
|
||||
pub fn constraint_bounds(a: [DVec2; 2], b: [DVec2; 2]) -> [DVec2; 2] {
|
||||
/// "Clip" bounds of `a` to the limits of `b`.
|
||||
pub fn clip(a: [DVec2; 2], b: [DVec2; 2]) -> [DVec2; 2] {
|
||||
[
|
||||
a[0].max(b[0]), // Constrain min corner
|
||||
a[1].min(b[1]), // Constrain max corner
|
||||
|
|
@ -59,7 +81,7 @@ impl Quad {
|
|||
|
||||
/// Expand a quad by a certain amount on all sides.
|
||||
///
|
||||
/// Not currently very optimised
|
||||
/// Not currently very optimized
|
||||
pub fn inflate(&self, offset: f64) -> Quad {
|
||||
let offset = |index_before, index, index_after| {
|
||||
let [point_before, point, point_after]: [DVec2; 3] = [self.0[index_before], self.0[index], self.0[index_after]];
|
||||
|
|
|
|||
Loading…
Reference in New Issue