diff --git a/editor/src/consts.rs b/editor/src/consts.rs index ec9e9602..2df02163 100644 --- a/editor/src/consts.rs +++ b/editor/src/consts.rs @@ -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 diff --git a/editor/src/messages/portfolio/document/utility_types/network_interface.rs b/editor/src/messages/portfolio/document/utility_types/network_interface.rs index 56af91c9..9aea0ba1 100644 --- a/editor/src/messages/portfolio/document/utility_types/network_interface.rs +++ b/editor/src/messages/portfolio/document/utility_types/network_interface.rs @@ -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(), )); diff --git a/editor/src/messages/tool/common_functionality/snapping.rs b/editor/src/messages/tool/common_functionality/snapping.rs index 858cdd50..e20e02c3 100644 --- a/editor/src/messages/tool/common_functionality/snapping.rs +++ b/editor/src/messages/tool/common_functionality/snapping.rs @@ -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)); } } diff --git a/editor/src/messages/tool/common_functionality/transformation_cage.rs b/editor/src/messages/tool/common_functionality/transformation_cage.rs index bd12e5dc..32be2277 100644 --- a/editor/src/messages/tool/common_functionality/transformation_cage.rs +++ b/editor/src/messages/tool/common_functionality/transformation_cage.rs @@ -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, diff --git a/editor/src/messages/tool/tool_messages/select_tool.rs b/editor/src/messages/tool/tool_messages/select_tool.rs index 21ee5281..677acfae 100644 --- a/editor/src/messages/tool/tool_messages/select_tool.rs +++ b/editor/src/messages/tool/tool_messages/select_tool.rs @@ -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(&[]) diff --git a/editor/src/messages/tool/transform_layer/transform_layer_message_handler.rs b/editor/src/messages/tool/transform_layer/transform_layer_message_handler.rs index 7b3e7672..8f616006 100644 --- a/editor/src/messages/tool/transform_layer/transform_layer_message_handler.rs +++ b/editor/src/messages/tool/transform_layer/transform_layer_message_handler.rs @@ -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> 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 } => { diff --git a/frontend/src/components/Editor.svelte b/frontend/src/components/Editor.svelte index 577aa44d..36c0c6a0 100644 --- a/frontend/src/components/Editor.svelte +++ b/frontend/src/components/Editor.svelte @@ -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; diff --git a/frontend/src/components/views/Graph.svelte b/frontend/src/components/views/Graph.svelte index 7d50b74f..0186f605 100644 --- a/frontend/src/components/views/Graph.svelte +++ b/frontend/src/components/views/Graph.svelte @@ -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; } diff --git a/node-graph/gcore/src/graphic_element/renderer.rs b/node-graph/gcore/src/graphic_element/renderer.rs index 98b7fd02..7d543246 100644 --- a/node-graph/gcore/src/graphic_element/renderer.rs +++ b/node-graph/gcore/src/graphic_element/renderer.rs @@ -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>(&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))); diff --git a/node-graph/gcore/src/graphic_element/renderer/quad.rs b/node-graph/gcore/src/graphic_element/renderer/quad.rs index fa905804..6c460fab 100644 --- a/node-graph/gcore/src/graphic_element/renderer/quad.rs +++ b/node-graph/gcore/src/graphic_element/renderer/quad.rs @@ -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]];