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:
parent
65171a5b8e
commit
3e50d177b7
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.);
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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")?;
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,10 +560,11 @@ 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 {
|
||||
tool_data.skew_edge = bounds.get_closest_edge(edges, input.mouse.position);
|
||||
}
|
||||
if dragging_bounds
|
||||
&& !is_skewing && !hovering_over_gizmo
|
||||
&& let Some(edges) = edges
|
||||
{
|
||||
tool_data.skew_edge = bounds.get_closest_edge(edges, input.mouse.position);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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,10 +676,10 @@ 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) {
|
||||
return ShapeToolFsmState::DraggingLineEndpoints;
|
||||
}
|
||||
) && 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,11 +891,11 @@ 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 {
|
||||
bounds.center_of_transformation += shift;
|
||||
bounds.original_bound_transform.translation += shift;
|
||||
}
|
||||
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"),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue