Add "Grid" to the Shape tool along with row/column gizmos (#2921)

* integrated grid shape in shape-tool

* add overlays,detection,transform for gizmo

* fix compile issues

* handle negative correctly,fix undo redo and abort

* fix missed merge conflicts

* fixed mouse cursor,correctly translatiing

* cleanup

* fix click-target area inside rect and spacing

* add 10px closer to gizmo line

* resolved conflicts

* Code review

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
0SlowPoke0 2025-09-08 06:23:37 +05:30 committed by GitHub
parent 65171a5b8e
commit 3e50d177b7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 850 additions and 87 deletions

View File

@ -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;

View File

@ -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,
}
}
@ -184,6 +195,10 @@ impl GizmoManager {
if graph_modification_utils::get_circle_id(layer, &document.network_interface).is_some() {
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
}

View File

@ -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.);

View File

@ -0,0 +1,435 @@
use crate::consts::GRID_ROW_COLUMN_GIZMO_OFFSET;
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::NodeInputDecleration;
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<LayerNodeIdentifier>,
pub gizmo_type: RowColumnGizmoType,
initial_rows: u32,
initial_columns: u32,
spacing: DVec2,
initial_mouse_start: Option<DVec2>,
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::Bottom => self.initial_rows,
RowColumnGizmoType::Left | RowColumnGizmoType::Right => 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<LayerNodeIdentifier>, _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<Message>, 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_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::Bottom | 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<RowColumnGizmoType> {
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,
Bottom,
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::Bottom => 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::Bottom => 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::Bottom | Self::Right => (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 {
match self {
Self::Top => Self::Bottom,
Self::Right => Self::Left,
Self::Bottom => 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::Bottom => 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::Bottom => rows,
RowColumnGizmoType::Left | RowColumnGizmoType::Right => 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::Bottom => spacing.y,
RowColumnGizmoType::Left | RowColumnGizmoType::Right => {
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 {
use graphene_std::vector::generator_nodes::grid::*;
match self {
RowColumnGizmoType::Top | RowColumnGizmoType::Bottom => RowsInput::INDEX,
RowColumnGizmoType::Left | RowColumnGizmoType::Right => ColumnsInput::INDEX,
RowColumnGizmoType::None => panic!("RowColumnGizmoType::None does not have a mouse_icon"),
}
}
pub fn mouse_icon(&self) -> MouseCursorIcon {
match self {
RowColumnGizmoType::Top | RowColumnGizmoType::Bottom => MouseCursorIcon::NSResize,
RowColumnGizmoType::Left | RowColumnGizmoType::Right => MouseCursorIcon::EWResize,
RowColumnGizmoType::None => panic!("RowColumnGizmoType::None does not have a mouse_icon"),
}
}
pub fn all() -> [Self; 4] {
[Self::Top, Self::Right, Self::Bottom, Self::Left]
}
}

View File

@ -1,4 +1,5 @@
pub mod circle_arc_radius_handle;
pub mod grid_rows_columns_gizmo;
pub mod number_of_points_dial;
pub mod point_radius_handle;
pub mod sweep_angle_gizmo;

View File

@ -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<NodeId> {
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")?;

View File

@ -0,0 +1,256 @@
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_rows_columns_gizmo::{RowColumnGizmo, 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::NodeInputDecleration;
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<Message>) {
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<Message>) {
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<LayerNodeIdentifier>,
_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<MouseCursorIcon> {
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<Message>,
) {
use graphene_std::vector::generator_nodes::grid::*;
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
responses.add(NodeGraphMessage::SetInput {
input_connector: InputConnector::node(node_id, SpacingInput::<f64>::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, AnglesInput::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<f64>) {
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° - standardized isometric view
angle = Some(30.);
// 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 the 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.);
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.
}

View File

@ -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;

View File

@ -1,19 +1,16 @@
use super::shape_utility::ShapeToolModifierKey;
use super::shape_utility::update_radius_sign;
use super::shape_utility::{ShapeToolModifierKey, update_radius_sign};
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::number_of_points_dial::NumberOfPointsDial;
use crate::messages::tool::common_functionality::gizmos::shape_gizmos::number_of_points_dial::NumberOfPointsDialState;
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::gizmos::shape_gizmos::number_of_points_dial::{NumberOfPointsDial, NumberOfPointsDialState};
use crate::messages::tool::common_functionality::gizmos::shape_gizmos::point_radius_handle::{PointRadiusHandle, PointRadiusHandleState};
use crate::messages::tool::common_functionality::graph_modification_utils::{self, 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::common_functionality::shapes::shape_utility::{ShapeGizmoHandler, 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 +157,35 @@ impl Polygon {
});
}
}
pub fn increase_decrease_sides(increase: bool, document: &DocumentMessageHandler, shape_tool_data: &mut ShapeToolData, responses: &mut VecDeque<Message>) {
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 {
options: 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);
}
}
}

View File

@ -14,9 +14,10 @@ use crate::messages::tool::utility_types::*;
use glam::{DAffine2, DMat2, DVec2};
use graph_craft::document::NodeInput;
use graph_craft::document::value::TaggedValue;
use graphene_std::NodeInputDecleration;
use graphene_std::subpath::{self, Subpath};
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 +29,7 @@ pub enum ShapeType {
Star,
Circle,
Arc,
Grid,
Rectangle,
Ellipse,
Line,
@ -40,6 +42,7 @@ impl ShapeType {
Self::Star => "Star",
Self::Circle => "Circle",
Self::Arc => "Arc",
Self::Grid => "Grid",
Self::Rectangle => "Rectangle",
Self::Ellipse => "Ellipse",
Self::Line => "Line",
@ -475,3 +478,23 @@ 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)> {
use graphene_std::vector::generator_nodes::grid::*;
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(GridTypeInput::INDEX)?.as_value(),
node_inputs.get(SpacingInput::<f64>::INDEX)?.as_value(),
node_inputs.get(ColumnsInput::INDEX)?.as_value(),
node_inputs.get(RowsInput::INDEX)?.as_value(),
node_inputs.get(AnglesInput::INDEX)?.as_value(),
) else {
return None;
};
Some((grid_type, spacing, columns, rows, angles))
}

View File

@ -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, ExtractField)]
@ -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)]
@ -134,6 +135,12 @@ fn create_shape_option_widget(shape_type: ShapeType) -> WidgetHolder {
}
.into()
}),
MenuListEntry::new("Grid").label("Grid").on_commit(move |_| {
ShapeToolMessage::UpdateOptions {
options: ShapeOptionsUpdate::ShapeType(ShapeType::Grid),
}
.into()
}),
]];
DropdownInput::new(entries).selected_index(Some(shape_type as u32)).widget_holder()
}
@ -177,6 +184,24 @@ 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 {
options: ShapeOptionsUpdate::GridType(GridType::Rectangular),
}
.into()
}),
RadioEntryData::new("Isometric").label("Isometric").on_update(move |_| {
ShapeToolMessage::UpdateOptions {
options: 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![];
@ -196,6 +221,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",
@ -297,6 +327,9 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> 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);
@ -527,12 +560,13 @@ impl Fsm for ShapeToolFsmState {
if is_skewing || (dragging_bounds && is_near_square && !hovering_over_gizmo) {
bounds.render_skew_gizmos(&mut overlay_context, tool_data.skew_edge);
}
if !is_skewing && dragging_bounds && !hovering_over_gizmo {
if let Some(edges) = edges {
if dragging_bounds
&& !is_skewing && !hovering_over_gizmo
&& let Some(edges) = edges
{
tool_data.skew_edge = bounds.get_closest_edge(edges, input.mouse.position);
}
}
}
let cursor = tool_data.gizmo_manager.mouse_cursor_icon().unwrap_or_else(|| tool_data.transform_cage_mouse_icon(input));
@ -595,64 +629,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 {
options: 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 {
options: 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
}
@ -681,6 +663,8 @@ impl Fsm for ShapeToolFsmState {
modifier: ShapeToolData::shape_tool_modifier_keys(),
});
responses.add(DocumentMessage::StartTransaction);
return ShapeToolFsmState::ModifyingGizmo;
}
@ -692,11 +676,11 @@ impl Fsm for ShapeToolFsmState {
document.network_interface.selected_nodes().selected_visible_and_unlocked_layers(&document.network_interface),
|_| false,
preferences,
) {
if clicked_on_line_endpoints(layer, document, input, tool_data) && !input.keyboard.key(Key::Control) {
) && clicked_on_line_endpoints(layer, document, input, tool_data)
&& !input.keyboard.key(Key::Control)
{
return ShapeToolFsmState::DraggingLineEndpoints;
}
}
let (resize, rotate, skew) = transforming_transform_cage(document, &mut tool_data.bounding_box_manager, input, responses, &mut tool_data.layers_dragging, None);
@ -735,7 +719,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::Grid | ShapeType::Rectangle | ShapeType::Ellipse => 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());
@ -750,6 +734,7 @@ impl Fsm for ShapeToolFsmState {
ShapeType::Star => Star::create_node(tool_options.vertices),
ShapeType::Circle => Circle::create_node(),
ShapeType::Arc => Arc::create_node(tool_options.arc_type),
ShapeType::Grid => Grid::create_node(tool_options.grid_type),
ShapeType::Rectangle => Rectangle::create_node(),
ShapeType::Ellipse => Ellipse::create_node(),
ShapeType::Line => Line::create_node(document, tool_data.data.drag_start),
@ -761,7 +746,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::Grid | ShapeType::Rectangle | ShapeType::Ellipse => {
defered_responses.add(GraphOperationMessage::TransformSet {
layer,
transform: DAffine2::from_scale_angle_translation(DVec2::ONE, 0., input.mouse.position),
@ -798,6 +783,7 @@ impl Fsm for ShapeToolFsmState {
ShapeType::Star => Star::update_shape(document, input, layer, tool_data, modifier, responses),
ShapeType::Circle => Circle::update_shape(document, input, layer, tool_data, modifier, responses),
ShapeType::Arc => Arc::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),
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),
@ -905,12 +891,12 @@ impl Fsm for ShapeToolFsmState {
}
(ShapeToolFsmState::ResizingBounds | ShapeToolFsmState::SkewingBounds { .. }, ShapeToolMessage::PointerOutsideViewport { .. }) => {
// Auto-panning
if let Some(shift) = tool_data.auto_panning.shift_viewport(input, responses) {
if let Some(bounds) = &mut tool_data.bounding_box_manager {
if let Some(shift) = tool_data.auto_panning.shift_viewport(input, responses)
&& let Some(bounds) = &mut tool_data.bounding_box_manager
{
bounds.center_of_transformation += shift;
bounds.original_bound_transform.translation += shift;
}
}
self
}
@ -1039,6 +1025,11 @@ fn update_dynamic_hints(state: &ShapeToolFsmState, responses: &mut VecDeque<Mess
HintInfo::keys([Key::Shift], "Constrain Arc").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 Regular").prepend_plus(),
HintInfo::keys([Key::Alt], "From Center").prepend_plus(),
])],
};
HintData(hint_groups)
}
@ -1048,6 +1039,7 @@ fn update_dynamic_hints(state: &ShapeToolFsmState, responses: &mut VecDeque<Mess
ShapeType::Polygon | ShapeType::Star | ShapeType::Arc => 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 Regular"), HintInfo::keys([Key::Alt], "From Center")]),
ShapeType::Line => HintGroup(vec![
HintInfo::keys([Key::Shift], "15° Increments"),
HintInfo::keys([Key::Alt], "From Center"),

View File

@ -60,7 +60,7 @@ impl AsI64 for f64 {
#[widget(Radio)]
pub enum GridType {
#[default]
Rectangular,
Rectangular = 0,
Isometric,
}
@ -102,6 +102,10 @@ pub fn dvec2_to_point(value: DVec2) -> Point {
Point { x: value.x, y: value.y }
}
pub fn get_line_endpoints(line: Line) -> (DVec2, DVec2) {
(point_to_dvec2(line.p0), point_to_dvec2(line.p1))
}
pub fn segment_to_handles(segment: &PathSeg) -> BezierHandles {
match *segment {
PathSeg::Line(_) => BezierHandles::Linear,

View File

@ -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);
}