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-07 17:53:37 -07:00
committed by GitHub
co-authored by Keavon Chambers
parent 65171a5b8e
commit 3e50d177b7
13 changed files with 850 additions and 87 deletions
@@ -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"),