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 44a5b1d210..96beeeea5f 100644 --- a/editor/src/consts.rs +++ b/editor/src/consts.rs @@ -131,6 +131,7 @@ pub const ARC_SNAP_THRESHOLD: f64 = 5.; pub const ARC_SWEEP_GIZMO_RADIUS: f64 = 14.; pub const ARC_SWEEP_GIZMO_TEXT_HEIGHT: f64 = 12.; pub const GIZMO_HIDE_THRESHOLD: f64 = 20.; +pub const GRID_ROW_COLUMN_GIZMO_OFFSET: f64 = 15.; // SCROLLBARS pub const SCROLLBAR_SPACING: f64 = 0.1; @@ -157,3 +158,10 @@ pub const AUTO_SAVE_TIMEOUT_SECONDS: u64 = 1; // INPUT pub const DOUBLE_CLICK_MILLISECONDS: u64 = 500; + +// GRID INPUT INDICES +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/tool/common_functionality/gizmos/gizmo_manager.rs b/editor/src/messages/tool/common_functionality/gizmos/gizmo_manager.rs index 57cbb4ed33..025bb4cb89 100644 --- a/editor/src/messages/tool/common_functionality/gizmos/gizmo_manager.rs +++ b/editor/src/messages/tool/common_functionality/gizmos/gizmo_manager.rs @@ -7,6 +7,7 @@ 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::arc_shape::ArcGizmoHandler; use crate::messages::tool::common_functionality::shapes::circle_shape::CircleGizmoHandler; +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; @@ -28,6 +29,7 @@ pub enum ShapeGizmoHandlers { Polygon(PolygonGizmoHandler), Arc(ArcGizmoHandler), Circle(CircleGizmoHandler), + Grid(GridGizmoHandler), } impl ShapeGizmoHandlers { @@ -39,6 +41,7 @@ impl ShapeGizmoHandlers { Self::Polygon(_) => "polygon", Self::Arc(_) => "arc", Self::Circle(_) => "circle", + Self::Grid(_) => "grid", Self::None => "none", } } @@ -50,6 +53,7 @@ impl ShapeGizmoHandlers { Self::Polygon(h) => h.handle_state(layer, mouse_position, document, responses), Self::Arc(h) => h.handle_state(layer, mouse_position, document, responses), Self::Circle(h) => h.handle_state(layer, mouse_position, document, responses), + Self::Grid(h) => h.handle_state(layer, mouse_position, document, responses), Self::None => {} } } @@ -61,6 +65,7 @@ impl ShapeGizmoHandlers { Self::Polygon(h) => h.is_any_gizmo_hovered(), Self::Arc(h) => h.is_any_gizmo_hovered(), Self::Circle(h) => h.is_any_gizmo_hovered(), + Self::Grid(h) => h.is_any_gizmo_hovered(), Self::None => false, } } @@ -72,6 +77,7 @@ impl ShapeGizmoHandlers { Self::Polygon(h) => h.handle_click(), Self::Arc(h) => h.handle_click(), Self::Circle(h) => h.handle_click(), + Self::Grid(h) => h.handle_click(), Self::None => {} } } @@ -83,6 +89,7 @@ impl ShapeGizmoHandlers { Self::Polygon(h) => h.handle_update(drag_start, document, input, responses), Self::Arc(h) => h.handle_update(drag_start, document, input, responses), Self::Circle(h) => h.handle_update(drag_start, document, input, responses), + Self::Grid(h) => h.handle_update(drag_start, document, input, responses), Self::None => {} } } @@ -94,6 +101,7 @@ impl ShapeGizmoHandlers { Self::Polygon(h) => h.cleanup(), Self::Arc(h) => h.cleanup(), Self::Circle(h) => h.cleanup(), + Self::Grid(h) => h.cleanup(), Self::None => {} } } @@ -113,6 +121,7 @@ impl ShapeGizmoHandlers { Self::Polygon(h) => h.overlays(document, layer, input, shape_editor, mouse_position, overlay_context), Self::Arc(h) => h.overlays(document, layer, input, shape_editor, mouse_position, overlay_context), Self::Circle(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 => {} } } @@ -131,6 +140,7 @@ impl ShapeGizmoHandlers { Self::Polygon(h) => h.dragging_overlays(document, input, shape_editor, mouse_position, overlay_context), Self::Arc(h) => h.dragging_overlays(document, input, shape_editor, mouse_position, overlay_context), Self::Circle(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 => {} } } @@ -141,6 +151,7 @@ impl ShapeGizmoHandlers { Self::Polygon(h) => h.mouse_cursor_icon(), Self::Arc(h) => h.mouse_cursor_icon(), Self::Circle(h) => h.mouse_cursor_icon(), + Self::Grid(h) => h.mouse_cursor_icon(), Self::None => None, } } @@ -185,6 +196,11 @@ impl GizmoManager { return Some(ShapeGizmoHandlers::Circle(CircleGizmoHandler::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/circle_arc_radius_handle.rs b/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/circle_arc_radius_handle.rs index 2cbf1c4509..6152a6a5dd 100644 --- a/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/circle_arc_radius_handle.rs +++ b/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/circle_arc_radius_handle.rs @@ -53,7 +53,7 @@ impl RadiusHandle { let center = viewport.transform_point2(DVec2::ZERO); if let Some(stroke_width) = get_stroke_width(layer, &document.network_interface) { let circle_point = calculate_circle_point_position(angle, radius.abs()); - let direction = circle_point.normalize(); + let Some(direction) = circle_point.try_normalize() else { return false }; let mouse_distance = mouse_position.distance(center); let spacing = Self::calculate_extra_spacing(viewport, radius, center, stroke_width, 15.); 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..7e8cf12031 --- /dev/null +++ b/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/grid_row_columns_gizmo.rs @@ -0,0 +1,432 @@ +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, 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, Rect}; +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) { + 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); + } + } + + 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, _, columns, rows, angles)) = extract_grid_parameters(layer, document) else { + return; + }; + let direction = self.gizmo_type.direction(viewport); + let delta_vector = input.mouse.position - self.initial_mouse_start.unwrap_or(drag_start); + + let projection = delta_vector.project_onto(self.gizmo_type.direction(viewport)); + let delta = viewport.inverse().transform_vector2(projection).length() * delta_vector.dot(direction).signum(); + + if delta.abs() < 1e-6 { + return; + } + + let dimensions_to_add = (delta / (self.gizmo_type.spacing(self.spacing, grid_type, angles))).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, self.spacing, grid_type, 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, spacing: DVec2, grid_type: GridType, angles: DVec2, viewport: DAffine2) -> DAffine2 { + match &self.gizmo_type { + RowColumnGizmoType::Top => { + let move_up_by = self.gizmo_type.direction(viewport) * dimensions_delta as f64 * spacing.y; + DAffine2::from_translation(move_up_by) + } + RowColumnGizmoType::Left => { + let move_left_by = self.gizmo_type.direction(viewport) * dimensions_delta as f64 * &self.gizmo_type.spacing(spacing, grid_type, angles); + 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 = 32.; + + for gizmo_type in RowColumnGizmoType::all() { + let line = gizmo_type.line(grid_type, columns, rows, spacing, angles, viewport); + let rect = gizmo_type.rect(grid_type, columns, rows, spacing, angles, viewport); + + if rect.contains(mouse_point) || line.nearest(mouse_point, accuracy).distance_sq < threshold { + return Some(gizmo_type); + } + } + + None +} + +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) +} + +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 if columns == 2 { + DVec2::new(spacing.x * 0.25, 0.) + } else { + DVec2::new(spacing.x * 0.5, 0.) + }; + + (top_left + offset, top_right - offset) +} + +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 if columns == 2 { + DVec2::new(spacing.x * 0.25, 0.) + } else { + DVec2::new(spacing.x * 0.5, 0.) + }; + + (bottom_left + offset, bottom_right - offset) +} + +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 if rows == 2 { + DVec2::new(0., -spacing.y * 0.25) + } else { + DVec2::new(0., -spacing.y * 0.5) + }; + + (top_right - offset, bottom_right + offset) +} + +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 if rows == 2 { + DVec2::new(0., -spacing.y * 0.25) + } 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) +} + +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) +} + +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) +} + +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) +} + +#[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(viewport); + let gap = GRID_ROW_COLUMN_GIZMO_OFFSET * viewport.inverse().transform_vector2(direction).normalize(); + + convert_to_gizmo_line(viewport.transform_point2(p0 + gap), viewport.transform_point2(p1 + gap)) + } + + fn rect(&self, grid_type: GridType, columns: u32, rows: u32, spacing: DVec2, angles: DVec2, viewport: DAffine2) -> Rect { + let (p0, p1) = self.get_line_points(grid_type, columns, rows, spacing, angles); + let direction = self.direction(viewport); + let gap = GRID_ROW_COLUMN_GIZMO_OFFSET * direction.normalize(); + + let (x0, x1) = match self { + Self::Top | Self::Left => (viewport.transform_point2(p0 + gap), viewport.transform_point2(p1)), + Self::Right | Self::Down => (viewport.transform_point2(p0), viewport.transform_point2(p1 + gap)), + Self::None => panic!("RowColumnGizmoType::None does not have opposite"), + }; + + Rect::new(x0.x, x0.y, x1.x, x1.y) + } + + 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"), + }; + } + + pub fn direction(&self, viewport: DAffine2) -> DVec2 { + match self { + RowColumnGizmoType::Top => viewport.transform_vector2(-DVec2::Y), + RowColumnGizmoType::Down => viewport.transform_vector2(DVec2::Y), + RowColumnGizmoType::Right => viewport.transform_vector2(DVec2::X), + RowColumnGizmoType::Left => viewport.transform_vector2(-DVec2::X), + 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, grid_type: GridType, angles: DVec2) -> f64 { + match self { + RowColumnGizmoType::Top | RowColumnGizmoType::Down => spacing.y, + RowColumnGizmoType::Right | RowColumnGizmoType::Left => { + if grid_type == GridType::Rectangular { + spacing.x + } else { + spacing.y / (angles.x.to_radians().tan() + angles.y.to_radians().tan()) + } + } + 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"), + } + } + + pub 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/mod.rs b/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/mod.rs index 710584f471..98f4808691 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,4 +1,5 @@ pub mod circle_arc_radius_handle; +pub mod grid_row_columns_gizmo; pub mod number_of_points_dial; pub mod point_radius_handle; pub mod sweep_angle_gizmo; 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 cecc7563dc..9c13c3f305 100644 --- a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs +++ b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs @@ -367,6 +367,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..d1b6621b51 --- /dev/null +++ b/editor/src/messages/tool/common_functionality/shapes/grid_shape.rs @@ -0,0 +1,257 @@ +use super::shape_utility::ShapeToolModifierKey; +use super::*; +use crate::consts::{GRID_ANGLE_INDEX, GRID_SPACING_INDEX}; +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::RowColumnGizmo; +use crate::messages::tool::common_functionality::gizmos::shape_gizmos::grid_row_columns_gizmo::RowColumnGizmoState; + +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; +use std::collections::VecDeque; + +#[derive(Clone, Debug, Default)] +pub struct GridGizmoHandler { + row_column_gizmo: RowColumnGizmo, +} + +impl ShapeGizmoHandler for GridGizmoHandler { + fn is_any_gizmo_hovered(&self) -> bool { + self.row_column_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); + } + + fn handle_click(&mut self) { + if self.row_column_gizmo.is_hovered() { + self.row_column_gizmo.update_state(RowColumnGizmoState::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); + } + } + + 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); + } + + 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); + } + } + + fn cleanup(&mut self) { + self.row_column_gizmo.cleanup(); + } + + fn mouse_cursor_icon(&self) -> Option { + if self.row_column_gizmo.is_hovered() || self.row_column_gizmo.is_dragging() { + return Some(self.row_column_gizmo.gizmo_type.mouse_icon()); + } + + None + } +} + +#[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 is_isometric = grid_type == GridType::Isometric; + + let Some(node_id) = graph_modification_utils::get_grid_id(layer, &document.network_interface) else { + return; + }; + + let start = shape_tool_data.data.viewport_drag_start(document); + let end = ipp.mouse.position; + + let (translation, dimensions, angle) = calculate_grid_params(start, end, is_isometric, ipp.keyboard.key(center), ipp.keyboard.key(lock_ratio)); + + // Set dimensions/spacing + let input_index = if is_isometric { GRID_SPACING_INDEX } else { 2 }; + responses.add(NodeGraphMessage::SetInput { + input_connector: InputConnector::node(node_id, input_index), + input: NodeInput::value(TaggedValue::DVec2(dimensions), false), + }); + + // Set angle for isometric grids + if let Some(angle_deg) = angle { + responses.add(NodeGraphMessage::SetInput { + input_connector: InputConnector::node(node_id, GRID_ANGLE_INDEX), + input: NodeInput::value(TaggedValue::DVec2(DVec2::splat(angle_deg)), false), + }); + } + + // Set transform + responses.add(GraphOperationMessage::TransformSet { + layer, + transform: DAffine2::from_scale_angle_translation(DVec2::ONE, 0., translation), + transform_in: TransformIn::Viewport, + skip_rerender: false, + }); + } +} + +fn calculate_grid_params(start: DVec2, end: DVec2, is_isometric: bool, center: bool, lock_ratio: bool) -> (DVec2, DVec2, Option) { + let raw_dimensions = (start - end).abs(); + let mouse_delta = end - start; + let dimensions; + let mut translation = start; + let mut angle = None; + + match (center, lock_ratio) { + // Both center and lock_ratio: Centered + square/fixed-angle grid + (true, true) => { + if is_isometric { + // Fix angle at 30 degree - standardized isometric view + angle = Some(30_f64); + + // Calculate the width based on given height and angle 30° + let width = calculate_isometric_x_position(raw_dimensions.y / 9., (30_f64).to_radians(), (30_f64).to_radians()).abs(); + + // To make draw from center: shift x by half of width and y by half of height (mouse_delta.y) + translation -= DVec2::new(width / 2., mouse_delta.y / 2.); + dimensions = DVec2::splat(raw_dimensions.y) / 9.; + + // Adjust for negative upward drag - compensate for coordinate system + if end.y < start.y { + translation -= DVec2::new(0., start.y - end.y); + } + } else { + // We want to make both dimensions same so we choose whichever is bigger and shift to make center + let max = raw_dimensions.x.max(raw_dimensions.y); + let distance_to_center = max; + translation = start - distance_to_center; + dimensions = 2. * DVec2::splat(max) / 9.; // 2x because centering halves the effective area + } + } + + // Only center: Centered grid with free aspect ratio + (true, false) => { + if is_isometric { + // Calculate angle from mouse movement - dynamic angle based on drag direction + angle = Some((raw_dimensions.y / (mouse_delta.x * 2.)).atan().to_degrees()); + + // To make draw from center: shift by half of mouse movement + translation -= mouse_delta / 2.; + dimensions = DVec2::splat(raw_dimensions.y) / 9.; + + // Adjust for upward drag - maintain proper grid positioning + if end.y < start.y { + translation -= DVec2::new(0., start.y - end.y); + } + } else { + // Logic: Rectangular centered grid using exact drag proportions + let distance_to_center = raw_dimensions; + translation = start - distance_to_center; + dimensions = 2. * raw_dimensions / 9.; // 2x for centering + } + } + + // Only lock_ratio: Square/fixed-angle grid from drag start point + (false, true) => { + let max: f64; + if is_isometric { + dimensions = DVec2::splat(raw_dimensions.y) / 9.; + + // Use 30° for angle - consistent isometric standard + angle = Some(30. as f64); + max = raw_dimensions.y; + } else { + // Logic: Force square grid by using larger dimension + max = raw_dimensions.x.max(raw_dimensions.y); + dimensions = DVec2::splat(max) / 9.; + } + + // Adjust for negative drag directions - maintain grid at intended position + if end.y < start.y { + translation -= DVec2::new(0., max); + } + if end.x < start.x { + translation -= DVec2::new(max, 0.); + } + } + + // Neither center nor lock_ratio: Free-form grid following exact user input + (false, false) => { + if is_isometric { + // Calculate angle from mouse movement - fully dynamic + // Logic: Angle represents user's exact intended perspective + angle = Some((raw_dimensions.y / (mouse_delta.x * 2.)).atan().to_degrees()); + dimensions = DVec2::splat(raw_dimensions.y) / 9.; + } else { + // Use exact drag dimensions for grid spacing - what you drag is what you get + // Logic: Direct mapping of user gesture to grid parameters + dimensions = raw_dimensions / 9.; + + // Adjust for leftward drag - keep grid positioned correctly + if end.x < start.x { + translation -= DVec2::new(start.x - end.x, 0.); + } + } + + // Adjust for upward drag (common to both grid types) + // Logic: Compensate for coordinate system where Y increases downward + if end.y < start.y { + translation -= DVec2::new(0., start.y - end.y); + } + } + } + + (translation, dimensions, angle) +} + +fn calculate_isometric_x_position(y_spacing: f64, rad_a: f64, rad_b: f64) -> f64 { + let spacing_x = y_spacing / (rad_a.tan() + rad_b.tan()); + spacing_x * 9. as f64 +} diff --git a/editor/src/messages/tool/common_functionality/shapes/mod.rs b/editor/src/messages/tool/common_functionality/shapes/mod.rs index a994ac52d1..683d2d627b 100644 --- a/editor/src/messages/tool/common_functionality/shapes/mod.rs +++ b/editor/src/messages/tool/common_functionality/shapes/mod.rs @@ -1,6 +1,7 @@ pub mod arc_shape; pub mod circle_shape; 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/polygon_shape.rs b/editor/src/messages/tool/common_functionality/shapes/polygon_shape.rs index ef90e7a4fa..30cca33b31 100644 --- a/editor/src/messages/tool/common_functionality/shapes/polygon_shape.rs +++ b/editor/src/messages/tool/common_functionality/shapes/polygon_shape.rs @@ -11,9 +11,11 @@ use crate::messages::tool::common_functionality::gizmos::shape_gizmos::number_of use crate::messages::tool::common_functionality::gizmos::shape_gizmos::point_radius_handle::PointRadiusHandle; use crate::messages::tool::common_functionality::gizmos::shape_gizmos::point_radius_handle::PointRadiusHandleState; 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::shape_editor::ShapeState; use crate::messages::tool::common_functionality::shapes::shape_utility::ShapeGizmoHandler; use crate::messages::tool::common_functionality::shapes::shape_utility::polygon_outline; +use crate::messages::tool::tool_messages::shape_tool::ShapeOptionsUpdate; use crate::messages::tool::tool_messages::tool_prelude::*; use glam::DAffine2; use graph_craft::document::NodeInput; @@ -160,4 +162,33 @@ impl Polygon { }); } } + + pub fn increase_decrease_sides(increase: bool, document: &DocumentMessageHandler, shape_tool_data: &mut ShapeToolData, responses: &mut VecDeque) { + if let Some(layer) = shape_tool_data.data.layer { + let Some(node_id) = graph_modification_utils::get_polygon_id(layer, &document.network_interface).or(graph_modification_utils::get_star_id(layer, &document.network_interface)) else { + return; + }; + + let Some(node_inputs) = NodeGraphLayer::new(layer, &document.network_interface) + .find_node_inputs("Regular Polygon") + .or(NodeGraphLayer::new(layer, &document.network_interface).find_node_inputs("Star")) + else { + return; + }; + + let Some(&TaggedValue::U32(n)) = node_inputs.get(1).unwrap().as_value() else { + return; + }; + + let new_dimension = if increase { n + 1 } else { (n - 1).max(3) }; + + responses.add(ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::Vertices(new_dimension))); + + responses.add(NodeGraphMessage::SetInput { + input_connector: InputConnector::node(node_id, 1), + input: NodeInput::value(TaggedValue::U32(new_dimension), false), + }); + responses.add(NodeGraphMessage::RunDocumentGraph); + } + } } 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 aa43e0cb29..a07e40ff86 100644 --- a/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs +++ b/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs @@ -1,5 +1,6 @@ use super::ShapeToolData; use crate::consts::{ARC_SWEEP_GIZMO_RADIUS, ARC_SWEEP_GIZMO_TEXT_HEIGHT}; +use crate::consts::{GRID_ANGLE_INDEX, GRID_COLUMNS_INDEX, GRID_ROW_INDEX, GRID_SPACING_INDEX, GRID_TYPE_INDEX}; use crate::messages::frontend::utility_types::MouseCursorIcon; use crate::messages::message::Message; use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; @@ -16,7 +17,9 @@ 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::{ArcType, dvec2_to_point}; +use graphene_std::vector::misc::{ + ArcType, {GridType, dvec2_to_point}, +}; use kurbo::{BezPath, PathEl, Shape}; use std::collections::VecDeque; use std::f64::consts::{PI, TAU}; @@ -28,6 +31,7 @@ pub enum ShapeType { Star, Circle, Arc, + Grid, Rectangle, Ellipse, Line, @@ -43,6 +47,7 @@ impl ShapeType { Self::Rectangle => "Rectangle", Self::Ellipse => "Ellipse", Self::Line => "Line", + Self::Grid => "Grid", }) .into() } @@ -475,3 +480,21 @@ pub fn calculate_arc_text_transform(angle: f64, offset_angle: f64, center: DVec2 ); DAffine2::from_translation(text_texture_position + center) } + +/// 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 c6e6911a60..eee9a72f30 100644 --- a/editor/src/messages/tool/tool_messages/shape_tool.rs +++ b/editor/src/messages/tool/tool_messages/shape_tool.rs @@ -3,15 +3,14 @@ use crate::consts::{DEFAULT_STROKE_WIDTH, SNAP_POINT_TOLERANCE}; 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::tool::common_functionality::auto_panning::AutoPanning; use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType}; use crate::messages::tool::common_functionality::gizmos::gizmo_manager::GizmoManager; 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::arc_shape::Arc; use crate::messages::tool::common_functionality::shapes::circle_shape::Circle; +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}; @@ -20,11 +19,10 @@ use crate::messages::tool::common_functionality::shapes::{Ellipse, Line, Rectang use crate::messages::tool::common_functionality::snapping::{self, SnapCandidatePoint, SnapData, SnapTypeConfiguration}; use crate::messages::tool::common_functionality::transformation_cage::{BoundingBoxManager, EdgeBool}; use crate::messages::tool::common_functionality::utility_functions::{closest_point, resize_bounds, rotate_bounds, skew_bounds, transforming_transform_cage}; -use graph_craft::document::value::TaggedValue; -use graph_craft::document::{NodeId, NodeInput}; +use graph_craft::document::NodeId; 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)] @@ -41,6 +39,7 @@ pub struct ShapeToolOptions { vertices: u32, shape_type: ShapeType, arc_type: ArcType, + grid_type: GridType, } impl Default for ShapeToolOptions { @@ -52,6 +51,7 @@ impl Default for ShapeToolOptions { vertices: 5, shape_type: ShapeType::Polygon, arc_type: ArcType::Open, + grid_type: GridType::Rectangular, } } } @@ -67,6 +67,7 @@ pub enum ShapeOptionsUpdate { Vertices(u32), ShapeType(ShapeType), ArcType(ArcType), + GridType(GridType), } #[impl_message(Message, ToolMessage, Shape)] @@ -117,6 +118,9 @@ fn create_shape_option_widget(shape_type: ShapeType) -> WidgetHolder { MenuListEntry::new("Arc") .label("Arc") .on_commit(move |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::ShapeType(ShapeType::Arc)).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() } @@ -146,6 +150,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![]; @@ -165,6 +181,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", @@ -231,6 +252,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; + } } update_dynamic_hints(&self.fsm_state, responses, &self.tool_data); @@ -525,60 +549,12 @@ impl Fsm for ShapeToolFsmState { self } (ShapeToolFsmState::Drawing(_), ShapeToolMessage::IncreaseSides) => { - if let Some(layer) = tool_data.data.layer { - let Some(node_id) = graph_modification_utils::get_polygon_id(layer, &document.network_interface).or(graph_modification_utils::get_star_id(layer, &document.network_interface)) - else { - return self; - }; - - let Some(node_inputs) = NodeGraphLayer::new(layer, &document.network_interface) - .find_node_inputs("Regular Polygon") - .or(NodeGraphLayer::new(layer, &document.network_interface).find_node_inputs("Star")) - else { - return self; - }; - - let Some(&TaggedValue::U32(n)) = node_inputs.get(1).unwrap().as_value() else { - return self; - }; - - responses.add(ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::Vertices(n + 1))); - - responses.add(NodeGraphMessage::SetInput { - input_connector: InputConnector::node(node_id, 1), - input: NodeInput::value(TaggedValue::U32(n + 1), false), - }); - responses.add(NodeGraphMessage::RunDocumentGraph); - } + Polygon::increase_decrease_sides(true, document, tool_data, responses); self } (ShapeToolFsmState::Drawing(_), ShapeToolMessage::DecreaseSides) => { - if let Some(layer) = tool_data.data.layer { - let Some(node_id) = graph_modification_utils::get_polygon_id(layer, &document.network_interface).or(graph_modification_utils::get_star_id(layer, &document.network_interface)) - else { - return self; - }; - - let Some(node_inputs) = NodeGraphLayer::new(layer, &document.network_interface) - .find_node_inputs("Regular Polygon") - .or(NodeGraphLayer::new(layer, &document.network_interface).find_node_inputs("Star")) - else { - return self; - }; - - let Some(&TaggedValue::U32(n)) = node_inputs.get(1).unwrap().as_value() else { - return self; - }; - - responses.add(ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::Vertices((n - 1).max(3)))); - - responses.add(NodeGraphMessage::SetInput { - input_connector: InputConnector::node(node_id, 1), - input: NodeInput::value(TaggedValue::U32((n - 1).max(3)), false), - }); - responses.add(NodeGraphMessage::RunDocumentGraph); - } + Polygon::increase_decrease_sides(false, document, tool_data, responses); self } @@ -605,6 +581,7 @@ impl Fsm for ShapeToolFsmState { // Send a PointerMove message to refresh the cursor icon responses.add(ShapeToolMessage::PointerMove(ShapeToolData::shape_tool_modifier_keys())); + responses.add(DocumentMessage::StartTransaction); return ShapeToolFsmState::ModifyingGizmo; } @@ -657,7 +634,7 @@ impl Fsm for ShapeToolFsmState { }; match tool_data.current_shape { - ShapeType::Polygon | ShapeType::Star | ShapeType::Circle | ShapeType::Arc | ShapeType::Rectangle | ShapeType::Ellipse => tool_data.data.start(document, input), + ShapeType::Polygon | ShapeType::Star | ShapeType::Circle | ShapeType::Arc | ShapeType::Rectangle | ShapeType::Ellipse | 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()); @@ -675,6 +652,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)]; @@ -683,7 +661,7 @@ impl Fsm for ShapeToolFsmState { let defered_responses = &mut VecDeque::new(); match tool_data.current_shape { - ShapeType::Polygon | ShapeType::Star | ShapeType::Circle | ShapeType::Arc | ShapeType::Rectangle | ShapeType::Ellipse => { + ShapeType::Polygon | ShapeType::Star | ShapeType::Circle | ShapeType::Arc | ShapeType::Rectangle | ShapeType::Ellipse | ShapeType::Grid => { defered_responses.add(GraphOperationMessage::TransformSet { layer, transform: DAffine2::from_scale_angle_translation(DVec2::ONE, 0., input.mouse.position), @@ -723,6 +701,7 @@ impl Fsm for ShapeToolFsmState { ShapeType::Rectangle => Rectangle::update_shape(document, input, layer, tool_data, modifier, responses), ShapeType::Ellipse => Ellipse::update_shape(document, input, layer, tool_data, modifier, responses), ShapeType::Line => Line::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 @@ -960,6 +939,11 @@ fn update_dynamic_hints(state: &ShapeToolFsmState, responses: &mut VecDeque 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) } @@ -969,6 +953,7 @@ fn update_dynamic_hints(state: &ShapeToolFsmState, responses: &mut VecDeque 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 8c60ae9354..c94485e3b8 100644 --- a/node-graph/gcore/src/vector/misc.rs +++ b/node-graph/gcore/src/vector/misc.rs @@ -60,8 +60,8 @@ impl AsI64 for f64 { #[widget(Radio)] pub enum GridType { #[default] - Rectangular, - Isometric, + Rectangular = 0, + Isometric = 1, } #[repr(C)] @@ -102,6 +102,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_attributes.rs b/node-graph/gcore/src/vector/vector_attributes.rs index 8edc97d766..38449ba9d2 100644 --- a/node-graph/gcore/src/vector/vector_attributes.rs +++ b/node-graph/gcore/src/vector/vector_attributes.rs @@ -128,7 +128,10 @@ impl PointDomain { } pub fn push(&mut self, id: PointId, position: DVec2) { - debug_assert!(!self.id.contains(&id)); + if self.id.contains(&id) { + return; + } + self.id.push(id); self.position.push(position); }