diff --git a/.vscode/settings.json b/.vscode/settings.json index 7f48a1c56a..4b736574e8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -52,5 +52,6 @@ "files.insertFinalNewline": true, "files.associations": { "*.graphite": "json" - } + }, + "rust-analyzer.checkOnSave": true } diff --git a/editor/src/consts.rs b/editor/src/consts.rs index 7c55c05a68..8a32fb7dad 100644 --- a/editor/src/consts.rs +++ b/editor/src/consts.rs @@ -127,6 +127,7 @@ pub const POINT_RADIUS_HANDLE_SEGMENT_THRESHOLD: f64 = 7.9; pub const NUMBER_OF_POINTS_DIAL_SPOKE_EXTENSION: f64 = 1.2; pub const NUMBER_OF_POINTS_DIAL_SPOKE_LENGTH: f64 = 10.; pub const GIZMO_HIDE_THRESHOLD: f64 = 20.; +pub const GRID_ROW_COLUMN_GIZMO_OFFSET: f64 = 15.; // SCROLLBARS pub const SCROLLBAR_SPACING: f64 = 0.1; @@ -153,3 +154,21 @@ pub const AUTO_SAVE_TIMEOUT_SECONDS: u64 = 15; // INPUT pub const DOUBLE_CLICK_MILLISECONDS: u64 = 500; + +// GRID INPUT INDICES + +// grid_type: GridType, +// #[unit(" px")] +// #[hard_min(0.)] +// #[default(10)] +// #[implementations(f64, DVec2)] +// spacing: T, +// #[default(10)] columns: u32, +// #[default(10)] rows: u32, +// #[default(30., 30.)] angles: DVec2, + +pub const GRID_TYPE_INDEX: usize = 1; +pub const GRID_SPACING_INDEX: usize = 2; +pub const GRID_COLUMNS_INDEX: usize = 3; +pub const GRID_ROW_INDEX: usize = 4; +pub const GRID_ANGLE_INDEX: usize = 5; diff --git a/editor/src/messages/portfolio/document/utility_types/transformation.rs b/editor/src/messages/portfolio/document/utility_types/transformation.rs index cefcb61418..c4f908c7c5 100644 --- a/editor/src/messages/portfolio/document/utility_types/transformation.rs +++ b/editor/src/messages/portfolio/document/utility_types/transformation.rs @@ -518,7 +518,7 @@ impl<'a> Selected<'a> { pen_handle: Option<&'a mut DVec2>, ) -> Self { // If user is using the Select tool then use the original layer transforms - if (*tool_type == ToolType::Select) && (*original_transforms == OriginalTransforms::Path(HashMap::new())) { + if (*tool_type == ToolType::Select || *tool_type == ToolType::Shape) && (*original_transforms == OriginalTransforms::Path(HashMap::new())) { *original_transforms = OriginalTransforms::Layer(HashMap::new()); } diff --git a/editor/src/messages/tool/common_functionality/gizmos/gizmo_manager.rs b/editor/src/messages/tool/common_functionality/gizmos/gizmo_manager.rs index 703c85e14d..e4fc9f54a1 100644 --- a/editor/src/messages/tool/common_functionality/gizmos/gizmo_manager.rs +++ b/editor/src/messages/tool/common_functionality/gizmos/gizmo_manager.rs @@ -4,6 +4,7 @@ use crate::messages::portfolio::document::utility_types::document_metadata::Laye use crate::messages::prelude::{DocumentMessageHandler, InputPreprocessorMessageHandler}; use crate::messages::tool::common_functionality::graph_modification_utils; use crate::messages::tool::common_functionality::shape_editor::ShapeState; +use crate::messages::tool::common_functionality::shapes::grid_shape::GridGizmoHandler; use crate::messages::tool::common_functionality::shapes::polygon_shape::PolygonGizmoHandler; use crate::messages::tool::common_functionality::shapes::shape_utility::ShapeGizmoHandler; use crate::messages::tool::common_functionality::shapes::star_shape::StarGizmoHandler; @@ -23,6 +24,7 @@ pub enum ShapeGizmoHandlers { None, Star(StarGizmoHandler), Polygon(PolygonGizmoHandler), + Grid(GridGizmoHandler), } impl ShapeGizmoHandlers { @@ -32,6 +34,7 @@ impl ShapeGizmoHandlers { match self { Self::Star(_) => "star", Self::Polygon(_) => "polygon", + Self::Grid(_) => "grid", Self::None => "none", } } @@ -41,6 +44,7 @@ impl ShapeGizmoHandlers { match self { Self::Star(h) => h.handle_state(layer, mouse_position, document, responses), Self::Polygon(h) => h.handle_state(layer, mouse_position, document, responses), + Self::Grid(h) => h.handle_state(layer, mouse_position, document, responses), Self::None => {} } } @@ -50,6 +54,7 @@ impl ShapeGizmoHandlers { match self { Self::Star(h) => h.is_any_gizmo_hovered(), Self::Polygon(h) => h.is_any_gizmo_hovered(), + Self::Grid(h) => h.is_any_gizmo_hovered(), Self::None => false, } } @@ -59,6 +64,7 @@ impl ShapeGizmoHandlers { match self { Self::Star(h) => h.handle_click(), Self::Polygon(h) => h.handle_click(), + Self::Grid(h) => h.handle_click(), Self::None => {} } } @@ -68,6 +74,7 @@ impl ShapeGizmoHandlers { match self { Self::Star(h) => h.handle_update(drag_start, document, input, responses), Self::Polygon(h) => h.handle_update(drag_start, document, input, responses), + Self::Grid(h) => h.handle_update(drag_start, document, input, responses), Self::None => {} } } @@ -77,6 +84,7 @@ impl ShapeGizmoHandlers { match self { Self::Star(h) => h.cleanup(), Self::Polygon(h) => h.cleanup(), + Self::Grid(h) => h.cleanup(), Self::None => {} } } @@ -94,6 +102,7 @@ impl ShapeGizmoHandlers { match self { Self::Star(h) => h.overlays(document, layer, input, shape_editor, mouse_position, overlay_context), Self::Polygon(h) => h.overlays(document, layer, input, shape_editor, mouse_position, overlay_context), + Self::Grid(h) => h.overlays(document, layer, input, shape_editor, mouse_position, overlay_context), Self::None => {} } } @@ -110,6 +119,7 @@ impl ShapeGizmoHandlers { match self { Self::Star(h) => h.dragging_overlays(document, input, shape_editor, mouse_position, overlay_context), Self::Polygon(h) => h.dragging_overlays(document, input, shape_editor, mouse_position, overlay_context), + Self::Grid(h) => h.dragging_overlays(document, input, shape_editor, mouse_position, overlay_context), Self::None => {} } } @@ -147,6 +157,11 @@ impl GizmoManager { return Some(ShapeGizmoHandlers::Polygon(PolygonGizmoHandler::default())); } + // Grid + if graph_modification_utils::get_grid_id(layer, &document.network_interface).is_some() { + return Some(ShapeGizmoHandlers::Grid(GridGizmoHandler::default())); + } + None } diff --git a/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/grid_row_columns_gizmo.rs b/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/grid_row_columns_gizmo.rs new file mode 100644 index 0000000000..0ed69f66ce --- /dev/null +++ b/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/grid_row_columns_gizmo.rs @@ -0,0 +1,469 @@ +use crate::consts::{GRID_COLUMNS_INDEX, GRID_ROW_COLUMN_GIZMO_OFFSET, GRID_ROW_INDEX}; +use crate::messages::frontend::utility_types::MouseCursorIcon; +use crate::messages::message::Message; +use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn; +use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; +use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; +use crate::messages::portfolio::document::utility_types::network_interface::InputConnector; +use crate::messages::prelude::{DocumentMessageHandler, FrontendMessage, InputPreprocessorMessageHandler, NodeGraphMessage}; +use crate::messages::prelude::{GraphOperationMessage, Responses}; +use crate::messages::tool::common_functionality::graph_modification_utils; +use crate::messages::tool::common_functionality::shape_editor::ShapeState; +use crate::messages::tool::common_functionality::shapes::shape_utility::extract_grid_parameters; +use glam::{DAffine2, DVec2}; +use graph_craft::document::NodeInput; +use graph_craft::document::value::TaggedValue; +use graphene_std::vector::misc::{GridType, dvec2_to_point, get_line_endpoints}; +use kurbo::{Line, ParamCurveNearest}; +use std::collections::VecDeque; + +#[derive(Clone, Debug, Default, PartialEq)] +pub enum RowColumnGizmoState { + #[default] + Inactive, + Hover, + Dragging, +} + +#[derive(Clone, Debug, Default)] +pub struct RowColumnGizmo { + pub layer: Option, + pub gizmo_type: RowColumnGizmoType, + initial_rows: u32, + initial_columns: u32, + spacing: DVec2, + initial_mouse_start: Option, + gizmo_state: RowColumnGizmoState, +} + +impl RowColumnGizmo { + pub fn cleanup(&mut self) { + self.layer = None; + self.gizmo_state = RowColumnGizmoState::Inactive; + self.initial_mouse_start = None; + } + + pub fn update_state(&mut self, state: RowColumnGizmoState) { + self.gizmo_state = state; + } + + pub fn is_hovered(&self) -> bool { + self.gizmo_state == RowColumnGizmoState::Hover + } + + pub fn is_dragging(&self) -> bool { + self.gizmo_state == RowColumnGizmoState::Dragging + } + + fn initial_dimension(&self) -> u32 { + match &self.gizmo_type { + RowColumnGizmoType::Top | RowColumnGizmoType::Down => self.initial_rows, + RowColumnGizmoType::Right | RowColumnGizmoType::Left => self.initial_columns, + RowColumnGizmoType::None => panic!("RowColumnGizmoType::None does not have a mouse_icon"), + } + } + + pub fn handle_actions(&mut self, layer: LayerNodeIdentifier, mouse_position: DVec2, document: &DocumentMessageHandler, responses: &mut VecDeque) { + let Some((grid_type, spacing, columns, rows, angles)) = extract_grid_parameters(layer, document) else { + return; + }; + let viewport = document.metadata().transform_to_viewport(layer); + if let Some(gizmo_type) = check_if_over_gizmo(grid_type, columns, rows, spacing, angles, mouse_position, viewport) { + self.layer = Some(layer); + self.gizmo_type = gizmo_type; + self.initial_rows = rows; + self.initial_columns = columns; + self.spacing = spacing; + self.initial_mouse_start = None; + self.update_state(RowColumnGizmoState::Hover); + responses.add(FrontendMessage::UpdateMouseCursor { cursor: self.gizmo_type.mouse_icon() }); + } + } + + pub fn overlays(&self, document: &DocumentMessageHandler, layer: Option, _shape_editor: &mut &mut ShapeState, _mouse_position: DVec2, overlay_context: &mut OverlayContext) { + let Some(layer) = layer.or(self.layer) else { return }; + let Some((grid_type, spacing, columns, rows, angles)) = extract_grid_parameters(layer, document) else { + return; + }; + let viewport = document.metadata().transform_to_viewport(layer); + + if !matches!(self.gizmo_state, RowColumnGizmoState::Inactive) { + let line = self.gizmo_type.line(grid_type, columns, rows, spacing, angles, viewport); + let (p0, p1) = get_line_endpoints(line); + overlay_context.dashed_line(p0, p1, None, None, Some(5.), Some(5.), Some(0.5)); + } + } + + pub fn update(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque, drag_start: DVec2) { + let Some(layer) = self.layer else { return }; + let viewport = document.metadata().transform_to_viewport(layer); + + let Some((grid_type, spacing, columns, rows, angles)) = extract_grid_parameters(layer, document) else { + return; + }; + let direction = self.gizmo_type.direction(grid_type, spacing, angles, viewport); + let delta_vector = input.mouse.position - self.initial_mouse_start.unwrap_or(drag_start); + + let viewport_spacing = get_viewport_grid_spacing(grid_type, angles, self.spacing, viewport); + let delta = delta_vector.dot(direction); + + let dimensions_to_add = (delta / (self.gizmo_type.spacing(viewport_spacing))).floor() as i32; + let new_dimension = (self.initial_dimension() as i32 + dimensions_to_add).max(1) as u32; + + let Some(node_id) = graph_modification_utils::get_grid_id(layer, &document.network_interface) else { + return; + }; + + let dimensions_delta = new_dimension as i32 - self.gizmo_type.initial_dimension(rows, columns) as i32; + let transform = self.transform_grid(dimensions_delta, grid_type, viewport_spacing, angles, viewport); + + responses.add(NodeGraphMessage::SetInput { + input_connector: InputConnector::node(node_id, self.gizmo_type.index()), + input: NodeInput::value(TaggedValue::U32((self.initial_dimension() as i32 + dimensions_to_add).max(1) as u32), false), + }); + + responses.add(GraphOperationMessage::TransformChange { + layer, + transform: transform, + transform_in: TransformIn::Viewport, + skip_rerender: false, + }); + + responses.add(NodeGraphMessage::RunDocumentGraph); + + if self.initial_dimension() as i32 + dimensions_to_add < 1 { + self.initial_mouse_start = Some(input.mouse.position); + self.gizmo_type = self.gizmo_type.opposite_gizmo_type(); + self.initial_rows = 1; + self.initial_columns = 1; + } + } + + fn transform_grid(&self, dimensions_delta: i32, grid_type: GridType, spacing: DVec2, angles: DVec2, viewport: DAffine2) -> DAffine2 { + match self.gizmo_type { + RowColumnGizmoType::Top => { + let move_up_by = self.gizmo_type.direction(grid_type, spacing, angles, viewport) * dimensions_delta as f64 * spacing.y; + DAffine2::from_translation(move_up_by) + } + RowColumnGizmoType::Left => { + let move_left_by = self.gizmo_type.direction(grid_type, spacing, angles, viewport) * dimensions_delta as f64 * spacing.x; + DAffine2::from_translation(move_left_by) + } + RowColumnGizmoType::Down | RowColumnGizmoType::Right | RowColumnGizmoType::None => DAffine2::IDENTITY, + } + } +} + +fn check_if_over_gizmo(grid_type: GridType, columns: u32, rows: u32, spacing: DVec2, angles: DVec2, mouse_position: DVec2, viewport: DAffine2) -> Option { + let mouse_point = dvec2_to_point(mouse_position); + let accuracy = 1e-6; + let threshold = 20.; + + for gizmo_type in RowColumnGizmoType::all() { + let line = gizmo_type.line(grid_type, columns, rows, spacing, angles, viewport); + if line.nearest(mouse_point, accuracy).distance_sq < threshold { + return Some(gizmo_type); + } + } + + None +} + +pub fn convert_to_gizmo_line(p0: DVec2, p1: DVec2) -> Line { + Line { + p0: dvec2_to_point(p0), + p1: dvec2_to_point(p1), + } +} + +/// Get corners of the rectangular-grid. +/// Returns a tuple of (topleft,topright,bottomright,bottomleft) +fn get_corners(columns: u32, rows: u32, spacing: DVec2) -> (DVec2, DVec2, DVec2, DVec2) { + let (width, height) = (spacing.x, spacing.y); + + let x_distance = (columns - 1) as f64 * width; + let y_distance = (rows - 1) as f64 * height; + + let point0 = DVec2::ZERO; + let point1 = DVec2::new(x_distance, 0.); + let point2 = DVec2::new(x_distance, y_distance); + let point3 = DVec2::new(0., y_distance); + + (point0, point1, point2, point3) +} + +pub fn get_viewport_grid_spacing(grid_type: GridType, angles: DVec2, spacing: DVec2, viewport: DAffine2) -> DVec2 { + match grid_type { + GridType::Rectangular => { + let p0 = DVec2::ZERO; + let p1 = DVec2::new(spacing.x, 0.); + let p2 = DVec2::new(0., spacing.y); + + let viewport_spacing_x = (viewport.transform_point2(p0) - viewport.transform_point2(p1)).length(); + let viewport_spacing_y = (viewport.transform_point2(p0) - viewport.transform_point2(p2)).length(); + + DVec2::new(viewport_spacing_x, viewport_spacing_y) + } + GridType::Isometric => { + let p0 = calculate_isometric_point(0, 0, angles, spacing); + let p1 = calculate_isometric_point(1, 0, angles, spacing); + let p2 = calculate_isometric_point(0, 1, angles, spacing); + + let viewport_spacing_x = viewport.transform_point2(p0).x - viewport.transform_point2(p1).x; + let viewport_spacing_y = viewport.transform_point2(p0).y - viewport.transform_point2(p2).y; + + DVec2::new(viewport_spacing_x.abs(), viewport_spacing_y.abs()) + } + } +} + +pub fn get_rectangle_top_line_points(columns: u32, rows: u32, spacing: DVec2) -> (DVec2, DVec2) { + let (top_left, top_right, _, _) = get_corners(columns, rows, spacing); + let offset = if columns == 1 || rows == 1 { DVec2::ZERO } else { DVec2::new(spacing.x * 0.5, 0.) }; + + (top_left + offset, top_right - offset) +} + +pub fn get_rectangle_bottom_line_points(columns: u32, rows: u32, spacing: DVec2) -> (DVec2, DVec2) { + let (_, _, bottom_right, bottom_left) = get_corners(columns, rows, spacing); + let offset = if columns == 1 || rows == 1 { DVec2::ZERO } else { DVec2::new(spacing.x * 0.5, 0.) }; + + (bottom_left + offset, bottom_right - offset) +} + +pub fn get_rectangle_right_line_points(columns: u32, rows: u32, spacing: DVec2) -> (DVec2, DVec2) { + let (_, top_right, bottom_right, _) = get_corners(columns, rows, spacing); + let offset = if columns == 1 || rows == 1 { DVec2::ZERO } else { DVec2::new(0., -spacing.y * 0.5) }; + + (top_right - offset, bottom_right + offset) +} + +pub fn get_rectangle_left_line_points(columns: u32, rows: u32, spacing: DVec2) -> (DVec2, DVec2) { + let (top_left, _, _, bottom_left) = get_corners(columns, rows, spacing); + let offset = if columns == 1 || rows == 1 { DVec2::ZERO } else { DVec2::new(0., -spacing.y * 0.5) }; + + (top_left - offset, bottom_left + offset) +} + +fn calculate_isometric_point(column: u32, row: u32, angles: DVec2, spacing: DVec2) -> DVec2 { + let tan_a = angles.x.to_radians().tan(); + let tan_b = angles.y.to_radians().tan(); + + let spacing = DVec2::new(spacing.y / (tan_a + tan_b), spacing.y); + + let a_angles_eaten = column.div_ceil(2) as f64; + let b_angles_eaten = (column / 2) as f64; + + let offset_y_fraction = b_angles_eaten * tan_b - a_angles_eaten * tan_a; + + DVec2::new(spacing.x * column as f64, spacing.y * row as f64 + offset_y_fraction * spacing.x) +} + +fn calculate_isometric_top_line_points(columns: u32, rows: u32, spacing: DVec2, angles: DVec2) -> (DVec2, DVec2) { + let top_left = calculate_isometric_point(0, 0, angles, spacing); + let top_right = calculate_isometric_point(columns - 1, 0, angles, spacing); + + let offset = if columns == 1 || rows == 1 { DVec2::ZERO } else { DVec2::new(spacing.x * 0.5, 0.) }; + let isometric_spacing = calculate_isometric_offset(spacing, angles); + let isometric_offset = DVec2::new(0., isometric_spacing.y); + let end_isometric_offset = if columns % 2 == 0 { DVec2::ZERO } else { DVec2::new(0., isometric_spacing.y) }; + + (top_left + offset - isometric_offset, top_right - offset - end_isometric_offset) +} + +fn calculate_isometric_bottom_line_points(columns: u32, rows: u32, spacing: DVec2, angles: DVec2) -> (DVec2, DVec2) { + let bottom_left = calculate_isometric_point(0, rows - 1, angles, spacing); + let bottom_right = calculate_isometric_point(columns - 1, rows - 1, angles, spacing); + + let offset = if columns == 1 || rows == 1 { DVec2::ZERO } else { DVec2::new(spacing.x * 0.5, 0.) }; + let isometric_offset = if columns % 2 == 0 { + let offset = calculate_isometric_offset(spacing, angles); + DVec2::new(0., offset.y) + } else { + DVec2::ZERO + }; + + (bottom_left + offset, bottom_right - offset + isometric_offset) +} + +pub fn calculate_isometric_offset(spacing: DVec2, angles: DVec2) -> DVec2 { + let first_point = calculate_isometric_point(0, 0, angles, spacing); + let second_point = calculate_isometric_point(1, 0, angles, spacing); + + DVec2::new(first_point.x - second_point.x, first_point.y - second_point.y) +} + +pub fn calculate_isometric_right_line_points(columns: u32, rows: u32, spacing: DVec2, angles: DVec2) -> (DVec2, DVec2) { + let top_right = calculate_isometric_point(columns - 1, 0, angles, spacing); + let bottom_right = calculate_isometric_point(columns - 1, rows - 1, angles, spacing); + + let offset = if columns == 1 || rows == 1 { DVec2::ZERO } else { DVec2::new(0., -spacing.y * 0.5) }; + + (top_right - offset, bottom_right + offset) +} + +pub fn calculate_isometric_left_line_points(columns: u32, rows: u32, spacing: DVec2, angles: DVec2) -> (DVec2, DVec2) { + let top_left = calculate_isometric_point(0, 0, angles, spacing); + let bottom_left = calculate_isometric_point(0, rows - 1, angles, spacing); + + let offset = if columns == 1 || rows == 1 { DVec2::ZERO } else { DVec2::new(0., -spacing.y * 0.5) }; + + (top_left - offset, bottom_left + offset) +} + +pub fn calculate_rectangle_side_direction(spacing: DVec2, viewport: DAffine2) -> DVec2 { + let p0 = DVec2::ZERO; + let p1 = DVec2::new(spacing.x, 0.); + (viewport.transform_point2(p1) - viewport.transform_point2(p0)).normalize() +} + +pub fn calculate_rectangle_top_direction(spacing: DVec2, viewport: DAffine2) -> DVec2 { + let p0 = DVec2::ZERO; + let p1 = DVec2::new(0., spacing.y); + (viewport.transform_point2(p0) - viewport.transform_point2(p1)).try_normalize().unwrap_or(DVec2::ZERO) +} + +fn calculate_isometric_side_direction(angles: DVec2, spacing: DVec2, viewport: Option) -> DVec2 { + let first_point = calculate_isometric_point(0, 0, angles, spacing); + let first_row_last_column = calculate_isometric_point(2, 0, angles, spacing); + + if let Some(viewport) = viewport { + return (viewport.transform_point2(first_row_last_column) - viewport.transform_point2(first_point)) + .try_normalize() + .unwrap_or_default(); + } + + (first_row_last_column - first_point).try_normalize().unwrap_or_default() +} + +fn calculate_isometric_top_direction(angles: DVec2, spacing: DVec2, viewport: Option) -> DVec2 { + let first_point = calculate_isometric_point(0, 0, angles, spacing); + let first_row_last_column = calculate_isometric_point(0, 1, angles, spacing); + + if let Some(viewport) = viewport { + return (viewport.transform_point2(first_row_last_column) - viewport.transform_point2(first_point)) + .try_normalize() + .unwrap_or_default(); + } + + (first_point - first_row_last_column).try_normalize().unwrap_or_default() +} + +#[derive(Clone, Debug, Default, PartialEq)] +pub enum RowColumnGizmoType { + #[default] + None, + Top, + Down, + Left, + Right, +} + +impl RowColumnGizmoType { + pub fn get_line_points(&self, grid_type: GridType, columns: u32, rows: u32, spacing: DVec2, angles: DVec2) -> (DVec2, DVec2) { + match grid_type { + GridType::Rectangular => match self { + Self::Top => get_rectangle_top_line_points(columns, rows, spacing), + Self::Right => get_rectangle_right_line_points(columns, rows, spacing), + Self::Down => get_rectangle_bottom_line_points(columns, rows, spacing), + Self::Left => get_rectangle_left_line_points(columns, rows, spacing), + Self::None => panic!("RowColumnGizmoType::None does not have line points"), + }, + GridType::Isometric => match self { + Self::Top => calculate_isometric_top_line_points(columns, rows, spacing, angles), + Self::Right => calculate_isometric_right_line_points(columns, rows, spacing, angles), + Self::Down => calculate_isometric_bottom_line_points(columns, rows, spacing, angles), + Self::Left => calculate_isometric_left_line_points(columns, rows, spacing, angles), + Self::None => panic!("RowColumnGizmoType::None does not have line points"), + }, + } + } + + fn line(&self, grid_type: GridType, columns: u32, rows: u32, spacing: DVec2, angles: DVec2, viewport: DAffine2) -> Line { + let (p0, p1) = self.get_line_points(grid_type, columns, rows, spacing, angles); + let direction = self.direction(grid_type, spacing, angles, viewport); + let gap = GRID_ROW_COLUMN_GIZMO_OFFSET * direction; + + convert_to_gizmo_line(viewport.transform_point2(p0) + gap, viewport.transform_point2(p1) + gap) + } + + fn opposite_gizmo_type(&self) -> Self { + return match self { + Self::Top => Self::Down, + Self::Right => Self::Left, + Self::Down => Self::Top, + Self::Left => Self::Right, + Self::None => panic!("RowColumnGizmoType::None does not have opposite"), + }; + } + + fn direction(&self, grid_type: GridType, spacing: DVec2, angles: DVec2, viewport: DAffine2) -> DVec2 { + match self { + RowColumnGizmoType::Top => { + if grid_type == GridType::Rectangular { + calculate_rectangle_top_direction(spacing, viewport) + } else { + -calculate_isometric_top_direction(angles, spacing, Some(viewport)) + } + } + RowColumnGizmoType::Down => { + if grid_type == GridType::Rectangular { + -calculate_rectangle_top_direction(spacing, viewport) + } else { + calculate_isometric_top_direction(angles, spacing, Some(viewport)) + } + } + RowColumnGizmoType::Right => { + if grid_type == GridType::Rectangular { + calculate_rectangle_side_direction(spacing, viewport) + } else { + calculate_isometric_side_direction(angles, spacing, Some(viewport)) + } + } + RowColumnGizmoType::Left => { + if grid_type == GridType::Rectangular { + -calculate_rectangle_side_direction(spacing, viewport) + } else { + -calculate_isometric_side_direction(angles, spacing, Some(viewport)) + } + } + RowColumnGizmoType::None => panic!("RowColumnGizmoType::None does not have a line"), + } + } + + fn initial_dimension(&self, rows: u32, columns: u32) -> u32 { + match self { + RowColumnGizmoType::Top | RowColumnGizmoType::Down => rows, + RowColumnGizmoType::Right | RowColumnGizmoType::Left => columns, + RowColumnGizmoType::None => panic!("RowColumnGizmoType::None does not have a mouse_icon"), + } + } + + fn spacing(&self, spacing: DVec2) -> f64 { + match self { + RowColumnGizmoType::Top | RowColumnGizmoType::Down => spacing.y, + RowColumnGizmoType::Right | RowColumnGizmoType::Left => spacing.x, + RowColumnGizmoType::None => panic!("RowColumnGizmoType::None does not have a mouse_icon"), + } + } + + fn index(&self) -> usize { + match self { + RowColumnGizmoType::Top | RowColumnGizmoType::Down => GRID_ROW_INDEX, + RowColumnGizmoType::Right | RowColumnGizmoType::Left => GRID_COLUMNS_INDEX, + RowColumnGizmoType::None => panic!("RowColumnGizmoType::None does not have a mouse_icon"), + } + } + + fn mouse_icon(&self) -> MouseCursorIcon { + match self { + RowColumnGizmoType::Top | RowColumnGizmoType::Down => MouseCursorIcon::NSResize, + RowColumnGizmoType::Right | RowColumnGizmoType::Left => MouseCursorIcon::EWResize, + RowColumnGizmoType::None => panic!("RowColumnGizmoType::None does not have a mouse_icon"), + } + } + + pub fn all() -> [Self; 4] { + [Self::Top, Self::Right, Self::Down, Self::Left] + } +} diff --git a/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/grid_spacing_gizmos.rs b/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/grid_spacing_gizmos.rs new file mode 100644 index 0000000000..e924b61824 --- /dev/null +++ b/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/grid_spacing_gizmos.rs @@ -0,0 +1,749 @@ +use crate::consts::{GRID_ANGLE_INDEX, GRID_SPACING_INDEX}; +use crate::messages::frontend::utility_types::MouseCursorIcon; +use crate::messages::message::Message; +use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn; +use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; +use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; +use crate::messages::portfolio::document::utility_types::network_interface::InputConnector; +use crate::messages::prelude::{DocumentMessageHandler, FrontendMessage, InputPreprocessorMessageHandler, NodeGraphMessage}; +use crate::messages::prelude::{GraphOperationMessage, Responses}; +use crate::messages::tool::common_functionality::gizmos::shape_gizmos::grid_row_columns_gizmo::convert_to_gizmo_line; +use crate::messages::tool::common_functionality::graph_modification_utils; +use crate::messages::tool::common_functionality::shape_editor::ShapeState; +use crate::messages::tool::common_functionality::shapes::shape_utility::extract_grid_parameters; +use glam::{DAffine2, DVec2}; +use graph_craft::document::NodeInput; +use graph_craft::document::value::TaggedValue; +use graphene_std::uuid::NodeId; +use graphene_std::vector::misc::{GridType, dvec2_to_point, get_line_endpoints}; +use kurbo::{Line, ParamCurveNearest, Rect, Shape, Triangle}; +use std::collections::VecDeque; + +#[derive(Clone, Debug, Default, PartialEq)] +pub enum GridSpacingGizmoState { + #[default] + Inactive, + Hover, + Dragging, +} + +#[derive(Clone, Debug, Default)] +pub struct GridSpacingGizmo { + pub layer: Option, + gizmo_state: GridSpacingGizmoState, + column_index: u32, + row_index: u32, + initial_spacing: DVec2, + angles: DVec2, + gizmo_type: Option, +} + +impl GridSpacingGizmo { + pub fn cleanup(&mut self) { + self.layer = None; + self.gizmo_state = GridSpacingGizmoState::Inactive; + } + + pub fn update_state(&mut self, state: GridSpacingGizmoState) { + self.gizmo_state = state; + } + + pub fn is_hovered(&self) -> bool { + self.gizmo_state == GridSpacingGizmoState::Hover + } + + pub fn is_dragging(&self) -> bool { + self.gizmo_state == GridSpacingGizmoState::Dragging + } + + pub fn handle_actions(&mut self, layer: LayerNodeIdentifier, mouse_position: DVec2, document: &DocumentMessageHandler, responses: &mut VecDeque) { + let Some((grid_type, spacing, columns, rows, angles)) = extract_grid_parameters(layer, document) else { + return; + }; + let stroke_width = graph_modification_utils::get_stroke_width(layer, &document.network_interface); + let viewport = document.metadata().transform_to_viewport(layer); + if let Some((col, row)) = check_if_over_gizmo(grid_type, columns, rows, spacing, angles, mouse_position, viewport) { + self.layer = Some(layer); + self.column_index = col; + self.row_index = row; + self.initial_spacing = spacing; + self.angles = angles; + self.update_state(GridSpacingGizmoState::Hover); + let closest_gizmo = GridSpacingGizmoType::get_closest_line(grid_type, mouse_position, col, row, spacing, angles, viewport, stroke_width); + responses.add(FrontendMessage::UpdateMouseCursor { cursor: closest_gizmo.mouse_icon() }); + self.gizmo_type = Some(closest_gizmo); + } + } + + pub fn overlays(&self, document: &DocumentMessageHandler, layer: Option, _shape_editor: &mut &mut ShapeState, _mouse_position: DVec2, overlay_context: &mut OverlayContext) { + let Some(layer) = layer.or(self.layer) else { return }; + let Some((_grid_type, spacing, _columns, _rows, angles)) = extract_grid_parameters(layer, document) else { + return; + }; + let viewport = document.metadata().transform_to_viewport(layer); + let stroke_width = graph_modification_utils::get_stroke_width(layer, &document.network_interface); + + match self.gizmo_state { + GridSpacingGizmoState::Inactive => {} + GridSpacingGizmoState::Hover | GridSpacingGizmoState::Dragging => { + if let Some(gizmo_type) = &self.gizmo_type { + let line = gizmo_type.line(self.column_index, self.row_index, angles, spacing, viewport, stroke_width); + let (p0, p1) = get_line_endpoints(line); + overlay_context.dashed_line(p0, p1, None, None, Some(5.), Some(5.), Some(0.5)); + } + } + } + } + + pub fn update_rectangle_grid( + &self, + node_id: NodeId, + layer: LayerNodeIdentifier, + gizmo_type: &GridSpacingGizmoType, + current_spacing: DVec2, + angles: DVec2, + delta: f64, + viewport: DAffine2, + responses: &mut VecDeque, + ) { + let direction = gizmo_type.direction(self.column_index, self.row_index, angles, self.initial_spacing, viewport); + let new_spacing = gizmo_type.new_spacing(delta, self.initial_spacing); + let spacing_delta = new_spacing - current_spacing; + + responses.add(NodeGraphMessage::SetInput { + input_connector: InputConnector::node(node_id, GRID_SPACING_INDEX), + input: NodeInput::value(TaggedValue::DVec2(new_spacing), false), + }); + + let transform = gizmo_type.transform_grid(spacing_delta, direction, self.column_index, self.row_index); + + responses.add(GraphOperationMessage::TransformChange { + layer, + transform, + transform_in: TransformIn::Viewport, + skip_rerender: false, + }); + } + + pub fn update(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque, drag_start: DVec2) { + let Some(layer) = self.layer else { return }; + let viewport = document.metadata().transform_to_viewport(layer); + + let Some((grid_type, spacing, _columns, _rows, angles)) = extract_grid_parameters(layer, document) else { + return; + }; + + let Some(gizmo_type) = &self.gizmo_type else { return }; + let direction = gizmo_type.direction(self.column_index, self.row_index, angles, self.initial_spacing, viewport); + let delta_vector = input.mouse.position - drag_start; + + let delta = delta_vector.dot(direction); + + let Some(node_id) = graph_modification_utils::get_grid_id(layer, &document.network_interface) else { + return; + }; + + if grid_type == GridType::Rectangular { + self.update_rectangle_grid(node_id, layer, gizmo_type, spacing, angles, delta, viewport, responses); + } else { + match gizmo_type { + GridSpacingGizmoType::Rect(_) => unreachable!(), + GridSpacingGizmoType::Iso(h) => { + if *h == IsometricGizmoType::Right || *h == IsometricGizmoType::Left { + self.update_isometric_x_spacing(layer, delta_vector, node_id, spacing, angles, gizmo_type, h, viewport, responses); + } else { + self.update_isometric_y_spacing(layer, delta_vector, node_id, spacing, angles, gizmo_type, viewport, responses); + } + } + }; + } + responses.add(NodeGraphMessage::RunDocumentGraph); + } + + fn update_isometric_y_spacing( + &self, + layer: LayerNodeIdentifier, + delta: DVec2, + node_id: NodeId, + spacing: DVec2, + angles: DVec2, + gizmo_type: &GridSpacingGizmoType, + viewport: DAffine2, + responses: &mut VecDeque, + ) { + let (a, b) = self.angles.into(); + let (tan_a_old, tan_b_old) = (a.to_radians().tan(), b.to_radians().tan()); + let direction = gizmo_type.direction(self.column_index, self.row_index, self.angles, spacing, viewport); + + let ((old_prev_row, old_prev_col), sign) = match gizmo_type { + GridSpacingGizmoType::Rect(_) => unreachable!(), + GridSpacingGizmoType::Iso(h) => (h.old_row_col_index(self.row_index, self.column_index), h.delta_sign()), + }; + let projection = viewport.inverse().transform_vector2(sign * delta.project_onto(direction)); + let a = (self.column_index + 1).div_ceil(2) as f64; + let b = ((self.column_index + 1) / 2) as f64; + + let p = self.initial_spacing.y / (tan_a_old + tan_b_old); // spacing_x, must stay constant + + let y = self.row_index as f64; + let delta = projection.y; + + // 1) Put the whole vertical move into y-spacing (for y>0): + let new_y_spacing = if y > 0.0 { + (self.initial_spacing.y + delta / y).abs() + } else { + (self.initial_spacing.y + delta).abs() + }; + + // 2) S' = sum of new tans required to keep spacing_x (=p) constant: + let s_prime = new_y_spacing / p; + + // 3) R = b*tb - a*ta (OLD values) + let r = b * tan_b_old - a * tan_a_old; + + // 4) Solve for new tangents: + let denom = a + b; // safe when col > 0 + let tan_a_new = (b * s_prime - r) / denom; + let tan_b_new = (r + a * s_prime) / denom; + + // 5) Convert to degrees and set: + let angle_a_new_deg = tan_a_new.atan().to_degrees(); + let angle_b_new_deg = tan_b_new.atan().to_degrees(); + + responses.add(NodeGraphMessage::SetInput { + input_connector: InputConnector::node(node_id, GRID_ANGLE_INDEX), + input: NodeInput::value(TaggedValue::DVec2((angle_a_new_deg, angle_b_new_deg).into()), false), + }); + + let old_position = isometric_point_position(old_prev_row, old_prev_col, spacing, angles); + let new_position = isometric_point_position(old_prev_row, old_prev_col, (new_y_spacing, new_y_spacing).into(), (angle_a_new_deg, angle_b_new_deg).into()); + + responses.add(GraphOperationMessage::TransformChange { + layer, + transform: DAffine2::from_translation(-DVec2::new(0., viewport.transform_vector2(new_position - old_position).y)), + transform_in: TransformIn::Viewport, + skip_rerender: false, + }); + + responses.add(NodeGraphMessage::SetInput { + input_connector: InputConnector::node(node_id, GRID_SPACING_INDEX), + input: NodeInput::value(TaggedValue::DVec2((new_y_spacing, new_y_spacing).into()), false), + }); + } + + fn update_isometric_x_spacing( + &self, + layer: LayerNodeIdentifier, + delta: DVec2, + node_id: NodeId, + spacing: DVec2, + angles: DVec2, + gizmo_type: &GridSpacingGizmoType, + iso_gizmo_type: &IsometricGizmoType, + viewport: DAffine2, + responses: &mut VecDeque, + ) { + let (row, column) = if *iso_gizmo_type == IsometricGizmoType::Right { + (self.row_index + 1, self.column_index + 1) + } else { + (self.row_index, self.column_index) + }; + + let (a, b) = self.angles.into(); + let (tan_a_old, tan_b_old) = (a.to_radians().tan(), b.to_radians().tan()); + let direction = gizmo_type.direction(column, row, self.angles, spacing, viewport); + + let (old_prev_row, old_prev_col) = iso_gizmo_type.old_row_col_index(self.row_index, self.column_index); + let sign = if *iso_gizmo_type == IsometricGizmoType::Left && column == 0 { -1. } else { 1. }; + let projection = viewport.inverse().transform_vector2(sign * delta.project_onto(direction)); + let old_spacing_x = spacing.y / (tan_a_old + tan_b_old); + + let a_steps = ((column) as f64 / 2.0).ceil(); + let b_steps = ((column) / 2) as f64; + + let old_offset_y_fraction = b_steps * tan_b_old - a_steps * tan_a_old; + + let old_x_pos = old_spacing_x * (column) as f64; + let old_y_pos = spacing.y * (row) as f64 + old_offset_y_fraction * old_spacing_x; + + // --- Step 1: Apply delta to get new position --- + let new_x_pos = old_x_pos + projection.x; + let new_y_pos = old_y_pos + projection.y; + + // --- Step 2: New spacing.x from horizontal position --- + let spacing_x_new = if (column) != 0 { + new_x_pos / (column) as f64 + } else { + old_spacing_x + projection.x // Can't deduce from vertical column + }; + + // --- Step 3: Sum of tangents --- + let sum_tan = spacing.y / spacing_x_new; + + // --- Step 4: RHS from vertical position --- + let rhs = (new_y_pos - spacing.y * row as f64) / spacing_x_new; + + // --- Step 5: Difference of tangents --- + let denom = b_steps + a_steps; + let diff_tan = if denom.abs() > f64::EPSILON { (2.0 * rhs - (b_steps - a_steps) * sum_tan) / denom } else { 0.0 }; + + // --- Step 6: Compute tangents and angles --- + let tan_a_new = (sum_tan - diff_tan) / 2.0; + let tan_b_new = (sum_tan + diff_tan) / 2.0; + + let new_angles = DVec2::new(tan_a_new.atan().to_degrees(), tan_b_new.atan().to_degrees()); + + responses.add(NodeGraphMessage::SetInput { + input_connector: InputConnector::node(node_id, GRID_ANGLE_INDEX), + input: NodeInput::value(TaggedValue::DVec2(new_angles), false), + }); + + let new_point = isometric_point_position(old_prev_row, old_prev_col, spacing, new_angles); + let old_point = isometric_point_position(old_prev_row, old_prev_col, spacing, angles); + + if column == 0 { + let transform = self + .gizmo_type + .as_ref() + .unwrap() + .transform_grid(viewport.transform_vector2(new_point - old_point), direction, self.column_index, self.row_index); + + responses.add(GraphOperationMessage::TransformChange { + layer, + transform, + transform_in: TransformIn::Viewport, + skip_rerender: false, + }); + } + } +} + +fn check_if_over_gizmo(grid_type: GridType, columns: u32, rows: u32, spacing: DVec2, angles: DVec2, mouse_position: DVec2, viewport: DAffine2) -> Option<(u32, u32)> { + let layer_mouse = viewport.inverse().transform_point2(mouse_position); + match grid_type { + GridType::Rectangular => { + for column in 0..columns - 1 { + for row in 0..rows - 1 { + let p0 = DVec2::new(spacing.x * column as f64, spacing.y * row as f64); + let p1 = DVec2::new((1 + column) as f64 * spacing.x, (1 + row) as f64 * spacing.y); + let rect = Rect::from_points(dvec2_to_point(p0), dvec2_to_point(p1)); + + if rect.contains(dvec2_to_point(layer_mouse)) { + return Some((column, row)); + }; + } + } + } + GridType::Isometric => { + for column in 0..columns - 1 { + for row in 0..rows - 1 { + let p0 = isometric_point_position(row, column, spacing, angles); + let p1 = isometric_point_position(row, column + 1, spacing, angles); + let p2 = isometric_point_position(row + 1, column + 1, spacing, angles); + let p4 = isometric_point_position(row + 1, column, spacing, angles); + + let triangle1 = Triangle::new(dvec2_to_point(p0), dvec2_to_point(p1), dvec2_to_point(p2)); + let triangle2 = Triangle::new(dvec2_to_point(p0), dvec2_to_point(p2), dvec2_to_point(p4)); + + if triangle2.contains(dvec2_to_point(layer_mouse)) { + return Some((column, row)); + } + + if triangle1.contains(dvec2_to_point(layer_mouse)) { + return Some((column, row)); + } + } + } + } + } + + None +} + +fn get_rectangular_top_points(column_index: u32, row_index: u32, spacing: DVec2, stroke_width: Option) -> (DVec2, DVec2) { + let stroke_width = stroke_width.unwrap_or_default(); + let p0 = DVec2::new(column_index as f64 * spacing.x, row_index as f64 * spacing.y) + DVec2::new(stroke_width, stroke_width); + let p1 = p0 + DVec2::new(spacing.x - 2. * stroke_width, 0.); + + (p0, p1) +} + +fn get_rectangular_right_points(column_index: u32, row_index: u32, spacing: DVec2, stroke_width: Option) -> (DVec2, DVec2) { + let stroke_width = stroke_width.unwrap_or_default(); + let p0 = DVec2::new((1 + column_index) as f64 * spacing.x, row_index as f64 * spacing.y) + DVec2::new(-stroke_width, stroke_width); + let p1 = p0 + DVec2::new(0., spacing.y - 2. * stroke_width); + + (p0, p1) +} + +fn get_rectangular_down_points(column_index: u32, row_index: u32, spacing: DVec2, stroke_width: Option) -> (DVec2, DVec2) { + let stroke_width = stroke_width.unwrap_or_default(); + + let p0 = DVec2::new(column_index as f64 * spacing.x, (1 + row_index) as f64 * spacing.y) + DVec2::new(stroke_width, -stroke_width); + let p1 = p0 + DVec2::new(spacing.x - 2. * stroke_width, 0.); + + (p0, p1) +} + +fn get_rectangular_left_points(column_index: u32, row_index: u32, spacing: DVec2, stroke_width: Option) -> (DVec2, DVec2) { + let stroke_width = stroke_width.unwrap_or_default(); + + let p0 = DVec2::new(column_index as f64 * spacing.x, row_index as f64 * spacing.y) + DVec2::new(stroke_width, stroke_width); + let p1 = p0 + DVec2::new(0., spacing.y - 2. * stroke_width); + + (p0, p1) +} + +fn isometric_point_position(row: u32, col: u32, spacing: DVec2, angles: DVec2) -> DVec2 { + let (angle_a, angle_b) = angles.into(); + let tan_a = angle_a.to_radians().tan(); + let tan_b = angle_b.to_radians().tan(); + + let spacing = DVec2::new(spacing.y / (tan_a + tan_b), spacing.y); + + let a_angles_eaten = col.div_ceil(2) as f64; + let b_angles_eaten = (col / 2) as f64; + let offset_y_fraction = b_angles_eaten * tan_b - a_angles_eaten * tan_a; + + DVec2::new(spacing.x * col as f64, spacing.y * row as f64 + offset_y_fraction * spacing.x) +} + +fn apply_gizmo_padding_and_offset(x0: DVec2, x1: DVec2, stroke_width: f64, inward: bool) -> (DVec2, DVec2) { + let Some(direction) = (x1 - x0).try_normalize() else { + // No valid direction, return original points unchanged + return (x0, x1); + }; + + // Apply normal padding and offset logic + let padding = (x1 - x0).length() * 0.1 * direction; + let push_out = calculate_gap_vector(direction, stroke_width); + let push_out_vector = if inward { -push_out } else { push_out }; + + (x0 + push_out_vector + padding, x1 + push_out_vector - padding) +} + +fn get_isometric_top_points(column_index: u32, row_index: u32, angles: DVec2, spacing: DVec2, stroke_width: Option) -> (DVec2, DVec2) { + let stroke_width = stroke_width.unwrap_or_default(); + + let x0 = isometric_point_position(row_index, column_index, spacing, angles); + let x1 = isometric_point_position(row_index, column_index + 1, spacing, angles); + apply_gizmo_padding_and_offset(x0, x1, stroke_width, false) // push_out outward +} + +fn get_isometric_right_points(column_index: u32, row_index: u32, angles: DVec2, spacing: DVec2, stroke_width: Option) -> (DVec2, DVec2) { + let stroke_width = stroke_width.unwrap_or_default(); + + let x0 = isometric_point_position(row_index, column_index + 1, spacing, angles); + let x1 = isometric_point_position(row_index + 1, column_index + 1, spacing, angles); + apply_gizmo_padding_and_offset(x0, x1, stroke_width, false) // push_out outward +} + +fn get_isometric_down_points(column_index: u32, row_index: u32, angles: DVec2, spacing: DVec2, stroke_width: Option) -> (DVec2, DVec2) { + let stroke_width = stroke_width.unwrap_or_default(); + + let x0 = isometric_point_position(row_index + 1, column_index, spacing, angles); + let x1 = isometric_point_position(row_index + 1, column_index + 1, spacing, angles); + apply_gizmo_padding_and_offset(x0, x1, stroke_width, true) // push_out inward +} + +fn get_isometric_left_points(column_index: u32, row_index: u32, angles: DVec2, spacing: DVec2, stroke_width: Option) -> (DVec2, DVec2) { + let stroke_width = stroke_width.unwrap_or_default(); + + let x0 = isometric_point_position(row_index, column_index, spacing, angles); + let x1 = isometric_point_position(row_index + 1, column_index, spacing, angles); + apply_gizmo_padding_and_offset(x0, x1, stroke_width, true) // push_out inward +} + +fn get_isometric_middle_up_points(column_index: u32, row_index: u32, angles: DVec2, spacing: DVec2, stroke_width: Option) -> (DVec2, DVec2) { + let stroke_width = stroke_width.unwrap_or_default(); + + let (x0, x1) = if column_index % 2 == 0 { + ( + isometric_point_position(row_index, column_index, spacing, angles), + isometric_point_position(row_index + 1, column_index + 1, spacing, angles), + ) + } else { + // ref point is changed + ( + isometric_point_position(row_index + 1, column_index, spacing, angles), + isometric_point_position(row_index, column_index + 1, spacing, angles), + ) + }; + apply_gizmo_padding_and_offset(x0, x1, stroke_width, true) // push_out inward +} + +fn get_isometric_middle_down_points(column_index: u32, row_index: u32, angles: DVec2, spacing: DVec2, stroke_width: Option) -> (DVec2, DVec2) { + let stroke_width = stroke_width.unwrap_or_default(); + + let (x0, x1) = if column_index % 2 == 0 { + ( + isometric_point_position(row_index, column_index, spacing, angles), + isometric_point_position(row_index + 1, column_index + 1, spacing, angles), + ) + } else { + // ref point is changed + ( + isometric_point_position(row_index + 1, column_index, spacing, angles), + isometric_point_position(row_index, column_index + 1, spacing, angles), + ) + }; + apply_gizmo_padding_and_offset(x0, x1, stroke_width, false) // push_out inward +} + +fn calculate_gap_vector(direction: DVec2, stroke_width: f64) -> DVec2 { + let perp = direction.perp().normalize(); + (stroke_width + 1.) * perp +} + +#[derive(Clone, Copy, Debug, Default, PartialEq)] +pub enum RectangularGizmoType { + #[default] + Top, + Right, + Down, + Left, +} + +#[derive(Clone, Copy, Debug, Default, PartialEq)] +pub enum IsometricGizmoType { + #[default] + Top, + Right, + Down, + Left, + IsometricMiddleUp, + IsometricMiddleDown, +} + +#[derive(Clone, Debug, PartialEq)] +pub enum GridSpacingGizmoType { + Rect(RectangularGizmoType), + Iso(IsometricGizmoType), +} + +pub fn get_line_points_for_rect(gizmo: RectangularGizmoType, column_index: u32, row_index: u32, spacing: DVec2, stroke: Option) -> (DVec2, DVec2) { + match gizmo { + RectangularGizmoType::Top => get_rectangular_top_points(column_index, row_index, spacing, stroke), + RectangularGizmoType::Right => get_rectangular_right_points(column_index, row_index, spacing, stroke), + RectangularGizmoType::Down => get_rectangular_down_points(column_index, row_index, spacing, stroke), + RectangularGizmoType::Left => get_rectangular_left_points(column_index, row_index, spacing, stroke), + } +} + +pub fn get_line_points_for_iso(gizmo: IsometricGizmoType, column_index: u32, row_index: u32, angles: DVec2, spacing: DVec2, stroke: Option) -> (DVec2, DVec2) { + match gizmo { + IsometricGizmoType::Top => get_isometric_top_points(column_index, row_index, angles, spacing, stroke), + IsometricGizmoType::Right => get_isometric_right_points(column_index, row_index, angles, spacing, stroke), + IsometricGizmoType::Down => get_isometric_down_points(column_index, row_index, angles, spacing, stroke), + IsometricGizmoType::Left => get_isometric_left_points(column_index, row_index, angles, spacing, stroke), + IsometricGizmoType::IsometricMiddleUp => get_isometric_middle_up_points(column_index, row_index, angles, spacing, stroke), + IsometricGizmoType::IsometricMiddleDown => get_isometric_middle_down_points(column_index, row_index, angles, spacing, stroke), + } +} + +// Builds a Line after viewport transform +pub fn gizmo_line_from_points(p0: DVec2, p1: DVec2, viewport: DAffine2) -> Line { + convert_to_gizmo_line(viewport.transform_point2(p0), viewport.transform_point2(p1)) +} + +pub fn gizmo_new_spacing_rect(g: RectangularGizmoType, delta: f64, spacing: DVec2) -> DVec2 { + match g { + RectangularGizmoType::Top | RectangularGizmoType::Down => DVec2::new(spacing.x, spacing.y + delta), + RectangularGizmoType::Right | RectangularGizmoType::Left => DVec2::new(spacing.x + delta, spacing.y), + } +} + +pub fn gizmo_new_spacing_iso(g: IsometricGizmoType, delta: f64, spacing: DVec2) -> DVec2 { + match g { + IsometricGizmoType::Top | IsometricGizmoType::Down => DVec2::new(spacing.x, spacing.y + delta), + IsometricGizmoType::Right | IsometricGizmoType::Left => DVec2::new(spacing.x + delta, spacing.y), + IsometricGizmoType::IsometricMiddleUp | IsometricGizmoType::IsometricMiddleDown => DVec2::new(spacing.x + delta, spacing.y + delta), + } +} + +pub fn gizmo_direction_rect(g: RectangularGizmoType, spacing: DVec2, viewport: DAffine2) -> DVec2 { + match g { + RectangularGizmoType::Top => viewport.transform_vector2(DVec2::Y), + RectangularGizmoType::Down => -viewport.transform_vector2(DVec2::Y), + RectangularGizmoType::Right => viewport.transform_vector2(DVec2::X), + RectangularGizmoType::Left => -viewport.transform_vector2(-DVec2::X), + } +} + +pub fn gizmo_direction_iso(g: IsometricGizmoType, column_index: u32, row_index: u32, angles: DVec2, spacing: DVec2) -> DVec2 { + let (p1, p2) = get_line_points_for_iso(g, column_index, row_index, angles, spacing, None); + (p1 - p2).perp().try_normalize().unwrap_or(DVec2::X) +} + +pub fn gizmo_mouse_icon_rect(g: RectangularGizmoType) -> MouseCursorIcon { + match g { + RectangularGizmoType::Top | RectangularGizmoType::Down => MouseCursorIcon::NSResize, + RectangularGizmoType::Right | RectangularGizmoType::Left => MouseCursorIcon::EWResize, + } +} + +pub fn gizmo_mouse_icon_iso(g: IsometricGizmoType) -> MouseCursorIcon { + match g { + IsometricGizmoType::Top | IsometricGizmoType::Down | IsometricGizmoType::IsometricMiddleUp | IsometricGizmoType::IsometricMiddleDown => MouseCursorIcon::NSResize, + IsometricGizmoType::Right | IsometricGizmoType::Left => MouseCursorIcon::EWResize, + } +} + +impl RectangularGizmoType { + pub fn all() -> [Self; 4] { + [Self::Top, Self::Right, Self::Down, Self::Left] + } +} + +impl IsometricGizmoType { + pub fn all() -> [Self; 6] { + [Self::Top, Self::Right, Self::Down, Self::Left, Self::IsometricMiddleUp, Self::IsometricMiddleDown] + } + + pub fn old_row_col_index(&self, row_index: u32, column_index: u32) -> (u32, u32) { + match self { + IsometricGizmoType::Right => (row_index, column_index), + IsometricGizmoType::Left => (row_index, column_index + 1), + IsometricGizmoType::Down => { + if column_index % 2 == 0 { + (row_index, column_index) + } else { + (row_index, column_index + 1) + } + } + IsometricGizmoType::Top => (row_index + 1, column_index), + IsometricGizmoType::IsometricMiddleUp | IsometricGizmoType::IsometricMiddleDown => { + if column_index % 2 == 0 { + (row_index, column_index + 1) + } else { + (row_index, column_index) + } + } + } + } + + pub fn delta_sign(&self) -> f64 { + match self { + IsometricGizmoType::Right => 1., + IsometricGizmoType::Left => -1., + IsometricGizmoType::Top => -1., + _ => 1., + } + } +} + +impl GridSpacingGizmoType { + pub fn line(&self, column_index: u32, row_index: u32, angles: DVec2, spacing: DVec2, viewport: DAffine2, stroke_width: Option) -> Line { + match self { + GridSpacingGizmoType::Rect(g) => { + let (p0, p1) = get_line_points_for_rect(*g, column_index, row_index, spacing, stroke_width); + gizmo_line_from_points(p0, p1, viewport) + } + GridSpacingGizmoType::Iso(g) => { + let (p0, p1) = get_line_points_for_iso(*g, column_index, row_index, angles, spacing, stroke_width); + gizmo_line_from_points(p0, p1, viewport) + } + } + } + + pub fn get_closest_line(grid_type: GridType, mouse_position: DVec2, column_index: u32, row_index: u32, spacing: DVec2, angles: DVec2, viewport: DAffine2, stroke_width: Option) -> Self { + match grid_type { + GridType::Rectangular => Self::Rect(closest_line_rect(mouse_position, column_index, row_index, spacing, viewport, stroke_width)), + GridType::Isometric => Self::Iso(closest_line_iso(mouse_position, column_index, row_index, angles, spacing, viewport, stroke_width)), + } + } + + pub fn direction(&self, column_index: u32, row_index: u32, angles: DVec2, spacing: DVec2, viewport: DAffine2) -> DVec2 { + match &self { + GridSpacingGizmoType::Rect(g) => gizmo_direction_rect(*g, spacing, viewport), + GridSpacingGizmoType::Iso(g) => gizmo_direction_iso(*g, column_index, row_index, angles, spacing), + } + } + + fn new_spacing(&self, delta: f64, spacing: DVec2) -> DVec2 { + match &self { + GridSpacingGizmoType::Rect(g) => gizmo_new_spacing_rect(*g, delta, spacing), + GridSpacingGizmoType::Iso(g) => gizmo_new_spacing_iso(*g, delta, spacing), + } + } + + fn mouse_icon(&self) -> MouseCursorIcon { + match self { + GridSpacingGizmoType::Rect(g) => gizmo_mouse_icon_rect(*g), + GridSpacingGizmoType::Iso(g) => gizmo_mouse_icon_iso(*g), + } + } + + pub fn transform_grid(&self, spacing_delta: DVec2, direction: DVec2, column_index: u32, row_index: u32) -> DAffine2 { + match self { + GridSpacingGizmoType::Rect(gizmo_type) => match gizmo_type { + RectangularGizmoType::Right => { + if column_index == 0 { + DAffine2::IDENTITY + } else { + DAffine2::from_translation(-spacing_delta * direction * column_index as f64) + } + } + RectangularGizmoType::Down => { + if row_index == 0 { + DAffine2::IDENTITY + } else { + DAffine2::from_translation(-spacing_delta * direction * row_index as f64) + } + } + RectangularGizmoType::Left => { + if column_index == 0 { + DAffine2::from_translation(spacing_delta * direction) + } else { + DAffine2::from_translation(spacing_delta * direction * (column_index + 1) as f64) + } + } + RectangularGizmoType::Top => { + if row_index == 0 { + DAffine2::from_translation(spacing_delta * direction) + } else { + DAffine2::from_translation(spacing_delta * direction * (row_index + 1) as f64) + } + } + }, + + GridSpacingGizmoType::Iso(gizmo_type) => match gizmo_type { + IsometricGizmoType::Right | IsometricGizmoType::Left => DAffine2::from_translation(-spacing_delta), + _ => DAffine2::IDENTITY, + }, // Placeholder: no transformation for now + } + } +} + +fn closest_line_generic(mouse_position: DVec2, viewport: DAffine2, all_variants: &[T], get_line_points: impl Fn(T) -> (DVec2, DVec2)) -> T +where + T: Copy + PartialEq, +{ + let mut gizmo_type = all_variants[0]; + let mut closest_distance = { + let (p0, p1) = get_line_points(gizmo_type); + gizmo_line_from_points(p0, p1, viewport).nearest(dvec2_to_point(mouse_position), 1e-6).distance_sq + }; + + for &t in all_variants.iter().skip(1) { + let (p0, p1) = get_line_points(t); + let nearest = gizmo_line_from_points(p0, p1, viewport).nearest(dvec2_to_point(mouse_position), 1e-6); + if nearest.distance_sq < closest_distance { + gizmo_type = t; + closest_distance = nearest.distance_sq; + } + } + gizmo_type +} + +pub fn closest_line_rect(mouse_position: DVec2, column_index: u32, row_index: u32, spacing: DVec2, viewport: DAffine2, stroke_width: Option) -> RectangularGizmoType { + closest_line_generic(mouse_position, viewport, &RectangularGizmoType::all(), |t| { + get_line_points_for_rect(t, column_index, row_index, spacing, stroke_width) + }) +} + +pub fn closest_line_iso(mouse_position: DVec2, column_index: u32, row_index: u32, angles: DVec2, spacing: DVec2, viewport: DAffine2, stroke_width: Option) -> IsometricGizmoType { + closest_line_generic(mouse_position, viewport, &IsometricGizmoType::all(), |t| { + get_line_points_for_iso(t, column_index, row_index, angles, spacing, stroke_width) + }) +} diff --git a/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/mod.rs b/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/mod.rs index 2b88dddd5e..b2af3b4b46 100644 --- a/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/mod.rs +++ b/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/mod.rs @@ -1,2 +1,4 @@ +pub mod grid_row_columns_gizmo; +pub mod grid_spacing_gizmos; pub mod number_of_points_dial; pub mod point_radius_handle; diff --git a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs index 126e5b160c..e2d83a9550 100644 --- a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs +++ b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs @@ -356,6 +356,10 @@ pub fn get_text_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkIn NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name("Text") } +pub fn get_grid_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option { + NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name("Grid") +} + /// Gets properties from the Text node pub fn get_text(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<(&String, &Font, TypesettingConfig, bool)> { let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs("Text")?; diff --git a/editor/src/messages/tool/common_functionality/shapes/grid_shape.rs b/editor/src/messages/tool/common_functionality/shapes/grid_shape.rs new file mode 100644 index 0000000000..99eda8b279 --- /dev/null +++ b/editor/src/messages/tool/common_functionality/shapes/grid_shape.rs @@ -0,0 +1,174 @@ +use super::shape_utility::ShapeToolModifierKey; +use super::*; +use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn; +use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type; +use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; +use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; +use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeTemplate}; +use crate::messages::tool::common_functionality::gizmos::shape_gizmos::grid_row_columns_gizmo::RowColumnGizmoState; +use crate::messages::tool::common_functionality::gizmos::shape_gizmos::grid_row_columns_gizmo::{RowColumnGizmo, calculate_rectangle_side_direction, calculate_rectangle_top_direction}; +use crate::messages::tool::common_functionality::gizmos::shape_gizmos::grid_spacing_gizmos::{GridSpacingGizmo, GridSpacingGizmoState}; +use crate::messages::tool::common_functionality::graph_modification_utils; +use crate::messages::tool::common_functionality::shape_editor::ShapeState; +use crate::messages::tool::common_functionality::shapes::shape_utility::ShapeGizmoHandler; +use crate::messages::tool::tool_messages::tool_prelude::*; +use glam::DAffine2; +use graph_craft::document::NodeInput; +use graph_craft::document::value::TaggedValue; +use graphene_std::vector::misc::{GridType, dvec2_to_point}; +use kurbo::{Line, ParamCurveNearest}; +use std::collections::VecDeque; + +#[derive(Clone, Debug, Default)] +pub struct GridGizmoHandler { + row_column_gizmo: RowColumnGizmo, + grid_spacing_gizmo: GridSpacingGizmo, +} + +impl ShapeGizmoHandler for GridGizmoHandler { + fn is_any_gizmo_hovered(&self) -> bool { + self.row_column_gizmo.is_hovered() || self.grid_spacing_gizmo.is_hovered() + } + + fn handle_state(&mut self, selected_grid_layer: LayerNodeIdentifier, mouse_position: DVec2, document: &DocumentMessageHandler, responses: &mut VecDeque) { + self.row_column_gizmo.handle_actions(selected_grid_layer, mouse_position, document, responses); + self.grid_spacing_gizmo.handle_actions(selected_grid_layer, mouse_position, document, responses); + } + + fn handle_click(&mut self) { + if self.row_column_gizmo.is_hovered() { + self.row_column_gizmo.update_state(RowColumnGizmoState::Dragging); + } + + if self.grid_spacing_gizmo.is_hovered() { + self.grid_spacing_gizmo.update_state(GridSpacingGizmoState::Dragging); + } + } + + fn handle_update(&mut self, drag_start: DVec2, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque) { + if self.row_column_gizmo.is_dragging() { + self.row_column_gizmo.update(document, input, responses, drag_start); + } + + if self.grid_spacing_gizmo.is_dragging() { + self.grid_spacing_gizmo.update(document, input, responses, drag_start); + } + } + + fn overlays( + &self, + document: &DocumentMessageHandler, + selected_grid_layer: Option, + _input: &InputPreprocessorMessageHandler, + shape_editor: &mut &mut ShapeState, + mouse_position: DVec2, + overlay_context: &mut OverlayContext, + ) { + self.row_column_gizmo.overlays(document, selected_grid_layer, shape_editor, mouse_position, overlay_context); + self.grid_spacing_gizmo.overlays(document, selected_grid_layer, shape_editor, mouse_position, overlay_context); + } + + fn dragging_overlays( + &self, + document: &DocumentMessageHandler, + _input: &InputPreprocessorMessageHandler, + shape_editor: &mut &mut ShapeState, + mouse_position: DVec2, + overlay_context: &mut OverlayContext, + ) { + if self.row_column_gizmo.is_dragging() { + self.row_column_gizmo.overlays(document, None, shape_editor, mouse_position, overlay_context); + } + + if self.grid_spacing_gizmo.is_dragging() { + self.grid_spacing_gizmo.overlays(document, None, shape_editor, mouse_position, overlay_context); + } + } + + fn cleanup(&mut self) { + self.grid_spacing_gizmo.cleanup(); + self.row_column_gizmo.cleanup(); + } +} + +#[derive(Default)] +pub struct Grid; + +impl Grid { + pub fn create_node(grid_type: GridType) -> NodeTemplate { + let node_type = resolve_document_node_type("Grid").expect("Grid can't be found"); + node_type.node_template_input_override([ + None, + Some(NodeInput::value(TaggedValue::GridType(grid_type), false)), + Some(NodeInput::value(TaggedValue::DVec2(DVec2::ZERO), false)), + ]) + } + + pub fn update_shape( + document: &DocumentMessageHandler, + ipp: &InputPreprocessorMessageHandler, + layer: LayerNodeIdentifier, + grid_type: GridType, + shape_tool_data: &mut ShapeToolData, + modifier: ShapeToolModifierKey, + responses: &mut VecDeque, + ) { + let [center, lock_ratio, _, _] = modifier; + + let start = shape_tool_data.data.viewport_drag_start(document); + let end = ipp.mouse.position; + + let mut dimensions = (start - end).abs(); + + let mut translation = shape_tool_data.data.viewport_drag_start(document); + let mut scale = (end - start).signum(); + + match grid_type { + GridType::Rectangular => { + if ipp.keyboard.key(center) && ipp.keyboard.key(lock_ratio) { + let max = dimensions.x.max(dimensions.y); + let distance_to_make_center = max; + translation = shape_tool_data.data.viewport_drag_start(document) - distance_to_make_center; + dimensions = 2. * DVec2::splat(max) / 9.; + scale = DVec2::ONE; + } else if ipp.keyboard.key(lock_ratio) { + let max = dimensions.x.max(dimensions.y); + dimensions = DVec2::splat(max) / 9. + } else if ipp.keyboard.key(center) { + let distance_to_make_center = dimensions; + translation = shape_tool_data.data.viewport_drag_start(document) - distance_to_make_center; + dimensions = 2. * dimensions / 9.; + scale = DVec2::ONE; + } else { + dimensions = dimensions / 9.; + }; + } + GridType::Isometric => { + if ipp.keyboard.key(center) { + let distance_to_make_center = DVec2::splat(dimensions.y); + translation = shape_tool_data.data.viewport_drag_start(document) - distance_to_make_center; + dimensions = 2. * DVec2::splat(dimensions.y) / 9.; + scale = DVec2::ONE; + } else { + dimensions = DVec2::splat(dimensions.y) / 9.; + }; + } + } + + let Some(node_id) = graph_modification_utils::get_grid_id(layer, &document.network_interface) else { + return; + }; + + responses.add(NodeGraphMessage::SetInput { + input_connector: InputConnector::node(node_id, 2), + input: NodeInput::value(TaggedValue::DVec2(dimensions), false), + }); + + responses.add(GraphOperationMessage::TransformSet { + layer, + transform: DAffine2::from_scale_angle_translation(scale, 0., translation), + transform_in: TransformIn::Viewport, + skip_rerender: false, + }); + } +} diff --git a/editor/src/messages/tool/common_functionality/shapes/mod.rs b/editor/src/messages/tool/common_functionality/shapes/mod.rs index 44f40b5982..e22fce11ed 100644 --- a/editor/src/messages/tool/common_functionality/shapes/mod.rs +++ b/editor/src/messages/tool/common_functionality/shapes/mod.rs @@ -1,4 +1,5 @@ pub mod ellipse_shape; +pub mod grid_shape; pub mod line_shape; pub mod polygon_shape; pub mod rectangle_shape; diff --git a/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs b/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs index 955984150b..123040d4ab 100644 --- a/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs +++ b/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs @@ -1,4 +1,5 @@ use super::ShapeToolData; +use crate::consts::{GRID_ANGLE_INDEX, GRID_COLUMNS_INDEX, GRID_ROW_INDEX, GRID_SPACING_INDEX, GRID_TYPE_INDEX}; use crate::messages::message::Message; use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; @@ -14,7 +15,7 @@ use glam::{DAffine2, DMat2, DVec2}; use graph_craft::document::NodeInput; use graph_craft::document::value::TaggedValue; use graphene_std::vector::click_target::ClickTargetType; -use graphene_std::vector::misc::dvec2_to_point; +use graphene_std::vector::misc::{GridType, dvec2_to_point}; use kurbo::{BezPath, PathEl, Shape}; use std::collections::VecDeque; use std::f64::consts::{PI, TAU}; @@ -24,9 +25,10 @@ pub enum ShapeType { #[default] Polygon = 0, Star = 1, - Rectangle = 2, - Ellipse = 3, - Line = 4, + Grid = 2, + Rectangle = 3, + Ellipse = 4, + Line = 5, } impl ShapeType { @@ -37,6 +39,7 @@ impl ShapeType { Self::Rectangle => "Rectangle", Self::Ellipse => "Ellipse", Self::Line => "Line", + Self::Grid => "Grid", }) .into() } @@ -363,3 +366,21 @@ pub fn draw_snapping_ticks(snap_radii: &[f64], direction: DVec2, viewport: DAffi overlay_context.line(tick_position, tick_position - tick_direction * 5., None, Some(2.)); } } + +/// Extract the node input values of Grid. +/// Returns an option of (Grid-type, spacing,columns,rows,angles). +pub fn extract_grid_parameters(layer: LayerNodeIdentifier, document: &DocumentMessageHandler) -> Option<(GridType, DVec2, u32, u32, DVec2)> { + let node_inputs = NodeGraphLayer::new(layer, &document.network_interface).find_node_inputs("Grid")?; + + let (Some(&TaggedValue::GridType(grid_type)), Some(&TaggedValue::DVec2(spacing)), Some(&TaggedValue::U32(columns)), Some(&TaggedValue::U32(rows)), Some(&TaggedValue::DVec2(angles))) = ( + node_inputs.get(GRID_TYPE_INDEX)?.as_value(), + node_inputs.get(GRID_SPACING_INDEX)?.as_value(), + node_inputs.get(GRID_COLUMNS_INDEX)?.as_value(), + node_inputs.get(GRID_ROW_INDEX)?.as_value(), + node_inputs.get(GRID_ANGLE_INDEX)?.as_value(), + ) else { + return None; + }; + + Some((grid_type, spacing, columns, rows, angles)) +} diff --git a/editor/src/messages/tool/tool_messages/shape_tool.rs b/editor/src/messages/tool/tool_messages/shape_tool.rs index 0f43adf286..605d5a5ae5 100644 --- a/editor/src/messages/tool/tool_messages/shape_tool.rs +++ b/editor/src/messages/tool/tool_messages/shape_tool.rs @@ -10,6 +10,7 @@ use crate::messages::tool::common_functionality::gizmos::gizmo_manager::GizmoMan use crate::messages::tool::common_functionality::graph_modification_utils; use crate::messages::tool::common_functionality::graph_modification_utils::NodeGraphLayer; use crate::messages::tool::common_functionality::resize::Resize; +use crate::messages::tool::common_functionality::shapes::grid_shape::Grid; use crate::messages::tool::common_functionality::shapes::line_shape::{LineToolData, clicked_on_line_endpoints}; use crate::messages::tool::common_functionality::shapes::polygon_shape::Polygon; use crate::messages::tool::common_functionality::shapes::shape_utility::{ShapeToolModifierKey, ShapeType, anchor_overlays, transform_cage_overlays}; @@ -22,7 +23,7 @@ use graph_craft::document::value::TaggedValue; use graph_craft::document::{NodeId, NodeInput}; use graphene_std::Color; use graphene_std::renderer::Quad; -use graphene_std::vector::misc::ArcType; +use graphene_std::vector::misc::{ArcType, GridType}; use std::vec; #[derive(Default)] @@ -39,6 +40,7 @@ pub struct ShapeToolOptions { vertices: u32, shape_type: ShapeType, arc_type: ArcType, + grid_type: GridType, } impl Default for ShapeToolOptions { @@ -50,6 +52,7 @@ impl Default for ShapeToolOptions { vertices: 5, shape_type: ShapeType::Polygon, arc_type: ArcType::Open, + grid_type: GridType::Rectangular, } } } @@ -65,6 +68,7 @@ pub enum ShapeOptionsUpdate { Vertices(u32), ShapeType(ShapeType), ArcType(ArcType), + GridType(GridType), } #[impl_message(Message, ToolMessage, Shape)] @@ -109,6 +113,9 @@ fn create_shape_option_widget(shape_type: ShapeType) -> WidgetHolder { MenuListEntry::new("Star") .label("Star") .on_commit(move |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::ShapeType(ShapeType::Star)).into()), + MenuListEntry::new("Grid") + .label("Grid") + .on_commit(move |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::ShapeType(ShapeType::Grid)).into()), ]]; DropdownInput::new(entries).selected_index(Some(shape_type as u32)).widget_holder() } @@ -123,6 +130,18 @@ fn create_weight_widget(line_weight: f64) -> WidgetHolder { .widget_holder() } +fn create_grid_type_widget(grid_type: GridType) -> WidgetHolder { + let entries = vec![ + RadioEntryData::new("Rectangular") + .label("Rectangular") + .on_update(move |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::GridType(GridType::Rectangular)).into()), + RadioEntryData::new("Isometric") + .label("Isometric") + .on_update(move |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::GridType(GridType::Isometric)).into()), + ]; + RadioInput::new(entries).selected_index(Some(grid_type as u32)).widget_holder() +} + impl LayoutHolder for ShapeTool { fn layout(&self) -> Layout { let mut widgets = vec![]; @@ -137,6 +156,11 @@ impl LayoutHolder for ShapeTool { } } + if self.options.shape_type == ShapeType::Grid { + widgets.push(create_grid_type_widget(self.options.grid_type)); + widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); + } + if self.options.shape_type != ShapeType::Line { widgets.append(&mut self.options.fill.create_widgets( "Fill", @@ -203,6 +227,9 @@ impl<'a> MessageHandler> for Shap ShapeOptionsUpdate::ArcType(arc_type) => { self.options.arc_type = arc_type; } + ShapeOptionsUpdate::GridType(grid_type) => { + self.options.grid_type = grid_type; + } } self.fsm_state.update_hints(responses); @@ -540,6 +567,7 @@ impl Fsm for ShapeToolFsmState { if tool_data.gizmo_manger.handle_click() { tool_data.data.drag_start = document.metadata().document_to_viewport.inverse().transform_point2(mouse_pos); + responses.add(DocumentMessage::StartTransaction); return ShapeToolFsmState::ModifyingGizmo; } @@ -578,7 +606,7 @@ impl Fsm for ShapeToolFsmState { }; match tool_data.current_shape { - ShapeType::Polygon | ShapeType::Star | ShapeType::Ellipse | ShapeType::Rectangle => tool_data.data.start(document, input), + ShapeType::Polygon | ShapeType::Star | ShapeType::Ellipse | ShapeType::Rectangle | ShapeType::Grid => tool_data.data.start(document, input), ShapeType::Line => { let point = SnapCandidatePoint::handle(document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position)); let snapped = tool_data.data.snap_manager.free_snap(&SnapData::new(document, input), &point, SnapTypeConfiguration::default()); @@ -594,6 +622,7 @@ impl Fsm for ShapeToolFsmState { ShapeType::Rectangle => Rectangle::create_node(), ShapeType::Ellipse => Ellipse::create_node(), ShapeType::Line => Line::create_node(document, tool_data.data.drag_start), + ShapeType::Grid => Grid::create_node(tool_options.grid_type), }; let nodes = vec![(NodeId(0), node)]; @@ -602,7 +631,7 @@ impl Fsm for ShapeToolFsmState { responses.add(Message::StartBuffer); match tool_data.current_shape { - ShapeType::Ellipse | ShapeType::Rectangle | ShapeType::Polygon | ShapeType::Star => { + ShapeType::Ellipse | ShapeType::Rectangle | ShapeType::Polygon | ShapeType::Star | ShapeType::Grid => { responses.add(GraphOperationMessage::TransformSet { layer, transform: DAffine2::from_scale_angle_translation(DVec2::ONE, 0., input.mouse.position), @@ -635,6 +664,7 @@ impl Fsm for ShapeToolFsmState { ShapeType::Line => Line::update_shape(document, input, layer, tool_data, modifier, responses), ShapeType::Polygon => Polygon::update_shape(document, input, layer, tool_data, modifier, responses), ShapeType::Star => Star::update_shape(document, input, layer, tool_data, modifier, responses), + ShapeType::Grid => Grid::update_shape(document, input, layer, tool_options.grid_type, tool_data, modifier, responses), } // Auto-panning @@ -656,8 +686,7 @@ impl Fsm for ShapeToolFsmState { self } (ShapeToolFsmState::ModifyingGizmo, ShapeToolMessage::PointerMove(..)) => { - responses.add(DocumentMessage::StartTransaction); - tool_data.gizmo_manger.handle_update(tool_data.data.drag_start, document, input, responses); + tool_data.gizmo_manger.handle_update(tool_data.data.viewport_drag_start(document), document, input, responses); responses.add(OverlaysMessage::Draw); @@ -853,6 +882,11 @@ impl Fsm for ShapeToolFsmState { HintInfo::keys([Key::Shift], "Constrain Square").prepend_plus(), HintInfo::keys([Key::Alt], "From Center").prepend_plus(), ])], + ShapeType::Grid => vec![HintGroup(vec![ + HintInfo::mouse(MouseMotion::LmbDrag, "Draw Grid"), + HintInfo::keys([Key::Shift], "Constrain Grid").prepend_plus(), + HintInfo::keys([Key::Alt], "From Center").prepend_plus(), + ])], }; HintData(hint_groups) } @@ -862,6 +896,7 @@ impl Fsm for ShapeToolFsmState { ShapeType::Polygon | ShapeType::Star => HintGroup(vec![HintInfo::keys([Key::Shift], "Constrain Regular"), HintInfo::keys([Key::Alt], "From Center")]), ShapeType::Rectangle => HintGroup(vec![HintInfo::keys([Key::Shift], "Constrain Square"), HintInfo::keys([Key::Alt], "From Center")]), ShapeType::Ellipse => HintGroup(vec![HintInfo::keys([Key::Shift], "Constrain Circular"), HintInfo::keys([Key::Alt], "From Center")]), + ShapeType::Grid => HintGroup(vec![HintInfo::keys([Key::Shift], "Constrain Grid"), HintInfo::keys([Key::Alt], "From Center")]), ShapeType::Line => HintGroup(vec![ HintInfo::keys([Key::Shift], "15° Increments"), HintInfo::keys([Key::Alt], "From Center"), diff --git a/node-graph/gcore/src/vector/misc.rs b/node-graph/gcore/src/vector/misc.rs index a273cd313c..bdf8a18da6 100644 --- a/node-graph/gcore/src/vector/misc.rs +++ b/node-graph/gcore/src/vector/misc.rs @@ -58,8 +58,8 @@ impl AsI64 for f64 { #[widget(Radio)] pub enum GridType { #[default] - Rectangular, - Isometric, + Rectangular = 0, + Isometric = 1, } #[repr(C)] @@ -100,6 +100,13 @@ pub fn dvec2_to_point(value: DVec2) -> Point { Point { x: value.x, y: value.y } } +pub fn get_line_endpoints(line: Line) -> (DVec2, DVec2) { + let po = line.p0; + let p1 = line.p1; + + (point_to_dvec2(po), point_to_dvec2(p1)) +} + pub fn segment_to_handles(segment: &PathSeg) -> BezierHandles { match *segment { PathSeg::Line(_) => BezierHandles::Linear, diff --git a/node-graph/gcore/src/vector/vector_data/attributes.rs b/node-graph/gcore/src/vector/vector_data/attributes.rs index ea4460c78c..c21e978e84 100644 --- a/node-graph/gcore/src/vector/vector_data/attributes.rs +++ b/node-graph/gcore/src/vector/vector_data/attributes.rs @@ -127,7 +127,7 @@ impl PointDomain { } pub fn push(&mut self, id: PointId, position: DVec2) { - debug_assert!(!self.id.contains(&id)); + // debug_assert!(!self.id.contains(&id)); self.id.push(id); self.position.push(position); }