Make the Shape tool's modes' parameter controls sync with the selected shape layer (#4121)

* Make the Shape tool's modes' parameter controls sync with the selected shape layer

* Fix populating the Shape tool mode selection list
This commit is contained in:
Keavon Chambers 2026-05-07 02:42:39 -07:00 committed by GitHub
parent 1596469e92
commit 525e49f7e9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 214 additions and 4 deletions

View File

@ -1744,6 +1744,19 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
let reference = network_interface.reference(&node_id, selection_network_path);
let is_text_node = reference.as_ref().is_some_and(|r| *r == DefinitionIdentifier::ProtoNode(graphene_std::text::text::IDENTIFIER));
let is_stroke_node = reference.as_ref().is_some_and(|r| *r == DefinitionIdentifier::ProtoNode(graphene_std::vector::stroke::IDENTIFIER));
let is_shape_generator_node = reference.as_ref().is_some_and(|r| {
[
graphene_std::vector::generator_nodes::regular_polygon::IDENTIFIER,
graphene_std::vector::generator_nodes::star::IDENTIFIER,
graphene_std::vector::generator_nodes::arc::IDENTIFIER,
graphene_std::vector::generator_nodes::spiral::IDENTIFIER,
graphene_std::vector::generator_nodes::grid::IDENTIFIER,
graphene_std::vector::generator_nodes::arrow::IDENTIFIER,
]
.into_iter()
.any(|id| *r == DefinitionIdentifier::ProtoNode(id))
});
let input = NodeInput::value(value, false);
responses.add(NodeGraphMessage::SetInput {
input_connector: InputConnector::node(node_id, input_index),
@ -1756,7 +1769,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
if is_text_node {
responses.add(TextToolMessage::SelectionChanged);
}
if is_stroke_node {
if is_stroke_node || is_shape_generator_node {
// The dispatcher delivers each only to its tool when active, so this just covers all four stroke-using tools.
responses.add(PenToolMessage::SelectionChanged);
responses.add(FreehandToolMessage::SelectionChanged);

View File

@ -526,6 +526,43 @@ pub fn set_stroke_weight_for_selected_layers(weight: f64, document: &DocumentMes
}
}
/// Reads a specific input from the matching proto node on the first selected non-artboard layer that has one.
/// Used by tool control bars to mirror per-shape parameters (sides, arc type, turns, etc.) from the selection
/// into the control bar's input widget state without each call site re-implementing the layer iteration.
pub fn first_selected_proto_node_input(document: &DocumentMessageHandler, identifier: graph_craft::ProtoNodeIdentifier, input_index: usize) -> Option<&TaggedValue> {
let identifier = DefinitionIdentifier::ProtoNode(identifier);
document
.network_interface
.selected_nodes()
.selected_layers_except_artboards(&document.network_interface)
.find_map(|layer| NodeGraphLayer::new(layer, &document.network_interface).find_input(&identifier, input_index))
}
/// Writes a value to a specific input on the matching proto node of every selected non-artboard layer that has one.
/// Used by tool control bars to push per-shape parameter changes back onto all selected layers of that shape.
pub fn set_proto_node_input_for_selected_layers(
document: &DocumentMessageHandler,
identifier: graph_craft::ProtoNodeIdentifier,
input_index: usize,
value: TaggedValue,
responses: &mut VecDeque<Message>,
) {
let identifier = DefinitionIdentifier::ProtoNode(identifier);
let layers: Vec<_> = document.network_interface.selected_nodes().selected_layers_except_artboards(&document.network_interface).collect();
for layer in layers {
let Some(node_id) = NodeGraphLayer::new(layer, &document.network_interface).upstream_node_id_from_name(&identifier) else {
continue;
};
responses.add(NodeGraphMessage::SetInputValue {
node_id,
input_index,
value: value.clone(),
});
}
}
/// Checks if a specified layer uses an upstream node matching the given name.
pub fn is_layer_fed_by_node_of_name(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface, identifier: &DefinitionIdentifier) -> bool {
NodeGraphLayer::new(layer, network_interface).find_node_inputs(identifier).is_some()

View File

@ -163,7 +163,8 @@ impl LayoutHolder for FreehandTool {
impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for FreehandTool {
fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, context: &mut ToolActionMessageContext<'a>) {
if matches!(&message, ToolMessage::Freehand(FreehandToolMessage::SelectionChanged)) {
if let Some(weight) = graph_modification_utils::first_selected_stroke_weight(context.document)
if self.fsm_state == FreehandToolFsmState::Ready
&& let Some(weight) = graph_modification_utils::first_selected_stroke_weight(context.document)
&& self.options.line_weight != weight
{
self.options.line_weight = weight;

View File

@ -250,6 +250,7 @@ impl LayoutHolder for PenTool {
impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for PenTool {
fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, context: &mut ToolActionMessageContext<'a>) {
if matches!(&message, ToolMessage::Pen(PenToolMessage::SelectionChanged))
&& self.fsm_state == PenToolFsmState::Ready
&& let Some(weight) = graph_modification_utils::first_selected_stroke_weight(context.document)
&& self.options.line_weight != weight
{

View File

@ -1,6 +1,7 @@
use super::tool_prelude::*;
use crate::consts::{BOUNDS_SELECT_THRESHOLD, DEFAULT_STROKE_WIDTH, SNAP_POINT_TOLERANCE};
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
use crate::messages::portfolio::document::node_graph::document_node_definitions::DefinitionIdentifier;
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
@ -22,9 +23,10 @@ use crate::messages::tool::common_functionality::snapping::{self, SnapCandidateP
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::NodeId;
use graphene_std::Color;
use graph_craft::document::value::TaggedValue;
use graphene_std::renderer::Quad;
use graphene_std::vector::misc::{ArcType, GridType, SpiralType};
use graphene_std::{Color, NodeInputDecleration};
use std::vec;
#[derive(Default, ExtractField)]
@ -127,6 +129,7 @@ fn create_sides_widget(vertices: u32) -> WidgetInstance {
}
.into()
})
.on_commit(|_| DocumentMessage::StartTransaction.into())
.widget_instance()
}
@ -141,6 +144,7 @@ fn create_turns_widget(turns: f64) -> WidgetInstance {
}
.into()
})
.on_commit(|_| DocumentMessage::StartTransaction.into())
.widget_instance()
}
@ -243,6 +247,7 @@ fn create_arrow_shaft_width_widget(shaft_width: f64) -> WidgetInstance {
}
.into()
})
.on_commit(|_| DocumentMessage::StartTransaction.into())
.widget_instance()
}
@ -258,6 +263,7 @@ fn create_arrow_head_width_widget(head_width: f64) -> WidgetInstance {
}
.into()
})
.on_commit(|_| DocumentMessage::StartTransaction.into())
.widget_instance()
}
@ -273,6 +279,7 @@ fn create_arrow_head_length_widget(head_length: f64) -> WidgetInstance {
}
.into()
})
.on_commit(|_| DocumentMessage::StartTransaction.into())
.widget_instance()
}
@ -312,6 +319,118 @@ fn create_grid_type_widget(grid_type: GridType) -> WidgetInstance {
RadioInput::new(entries).selected_index(Some(grid_type as u32)).widget_instance()
}
/// Mirrors the per-shape parameters (and `shape_type` itself) from the first selected non-artboard layer into the
/// control bar's option state. Detects the layer's shape by trying each generator's proto node, then reads only the
/// inputs relevant to that shape. Returns whether anything in `options` (or `tool_data.current_shape`) changed.
/// The caller decides whether to dispatch a layout refresh.
fn sync_shape_options_from_selection(options: &mut ShapeToolOptions, tool_data: &mut ShapeToolData, document: &DocumentMessageHandler) -> bool {
use graphene_std::vector::generator_nodes::*;
let Some(layer) = document.network_interface.selected_nodes().selected_layers_except_artboards(&document.network_interface).next() else {
return false;
};
let layer_view = graph_modification_utils::NodeGraphLayer::new(layer, &document.network_interface);
let proto = DefinitionIdentifier::ProtoNode;
// Map each generator's proto node to the corresponding `ShapeType`.
// First match wins. Only includes modes from the Shape tool's mode dropdown.
let Some(shape_type) = [
(regular_polygon::IDENTIFIER, ShapeType::Polygon),
(star::IDENTIFIER, ShapeType::Star),
(circle::IDENTIFIER, ShapeType::Circle),
(arc::IDENTIFIER, ShapeType::Arc),
(spiral::IDENTIFIER, ShapeType::Spiral),
(grid::IDENTIFIER, ShapeType::Grid),
(arrow::IDENTIFIER, ShapeType::Arrow),
]
.into_iter()
.find_map(|(id, shape)| layer_view.upstream_node_id_from_name(&proto(id)).map(|_| shape)) else {
return false;
};
let mut changed = false;
if options.shape_type != shape_type {
options.shape_type = shape_type;
tool_data.current_shape = shape_type;
changed = true;
}
// Only the shapes whose control bar exposes per-shape parameters need a sync below.
// The rest (Ellipse, Rectangle, Line) just keep `shape_type` in step and rely on the shared Stroke/Fill controls.
match shape_type {
ShapeType::Polygon | ShapeType::Star => {
let id = if shape_type == ShapeType::Polygon { regular_polygon::IDENTIFIER } else { star::IDENTIFIER };
// Both `regular_polygon` and `star` are generic over `T: AsU64`, but the control bar widget always writes `u32`,
// and existing call sites (e.g. `polygon_shape.rs`) read it back as `TaggedValue::U32`.
let index = if shape_type == ShapeType::Polygon {
regular_polygon::SidesInput::<u32>::INDEX
} else {
star::SidesInput::<u32>::INDEX
};
if let Some(&TaggedValue::U32(sides)) = layer_view.find_input(&proto(id), index)
&& options.vertices != sides
{
options.vertices = sides;
changed = true;
}
}
ShapeType::Arc => {
if let Some(&TaggedValue::ArcType(arc_type)) = layer_view.find_input(&proto(arc::IDENTIFIER), arc::ArcTypeInput::INDEX)
&& options.arc_type != arc_type
{
options.arc_type = arc_type;
changed = true;
}
}
ShapeType::Spiral => {
if let Some(&TaggedValue::SpiralType(spiral_type)) = layer_view.find_input(&proto(spiral::IDENTIFIER), spiral::SpiralTypeInput::INDEX)
&& options.spiral_type != spiral_type
{
options.spiral_type = spiral_type;
changed = true;
}
if let Some(&TaggedValue::F64(turns)) = layer_view.find_input(&proto(spiral::IDENTIFIER), spiral::TurnsInput::INDEX)
&& options.turns != turns
{
options.turns = turns;
changed = true;
}
}
ShapeType::Grid => {
if let Some(&TaggedValue::GridType(grid_type)) = layer_view.find_input(&proto(grid::IDENTIFIER), grid::GridTypeInput::INDEX)
&& options.grid_type != grid_type
{
options.grid_type = grid_type;
changed = true;
}
}
ShapeType::Arrow => {
if let Some(&TaggedValue::F64(shaft)) = layer_view.find_input(&proto(arrow::IDENTIFIER), arrow::ShaftWidthInput::INDEX)
&& options.arrow_shaft_width != shaft
{
options.arrow_shaft_width = shaft;
changed = true;
}
if let Some(&TaggedValue::F64(head_w)) = layer_view.find_input(&proto(arrow::IDENTIFIER), arrow::HeadWidthInput::INDEX)
&& options.arrow_head_width != head_w
{
options.arrow_head_width = head_w;
changed = true;
}
if let Some(&TaggedValue::F64(head_l)) = layer_view.find_input(&proto(arrow::IDENTIFIER), arrow::HeadLengthInput::INDEX)
&& options.arrow_head_length != head_l
{
options.arrow_head_length = head_l;
changed = true;
}
}
ShapeType::Ellipse | ShapeType::Rectangle | ShapeType::Line | ShapeType::Circle => {}
}
changed
}
impl LayoutHolder for ShapeTool {
fn layout(&self) -> Layout {
let mut widgets = vec![];
@ -416,11 +535,28 @@ impl LayoutHolder for ShapeTool {
#[message_handler_data]
impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for ShapeTool {
fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, context: &mut ToolActionMessageContext<'a>) {
use graphene_std::vector::generator_nodes::*;
if matches!(&message, ToolMessage::Shape(ShapeToolMessage::SelectionChanged)) {
if !matches!(self.fsm_state, ShapeToolFsmState::Ready(_)) {
return;
}
let mut needs_refresh = false;
// Stroke weight is shape-agnostic. Sync it regardless of which (if any) shape proto node the layer has.
if let Some(weight) = graph_modification_utils::first_selected_stroke_weight(context.document)
&& self.options.line_weight != weight
{
self.options.line_weight = weight;
needs_refresh = true;
}
// Detect which shape the first selected layer is by checking for each generator's proto node, then mirror
// the control bar's `shape_type` into that and pull the shape's parameters into the matching control bar fields.
needs_refresh |= sync_shape_options_from_selection(&mut self.options, &mut self.tool_data, context.document);
if needs_refresh {
self.send_layout(responses, LayoutTarget::ToolOptions);
}
return;
@ -461,27 +597,48 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Shap
}
ShapeOptionsUpdate::Vertices(vertices) => {
self.options.vertices = vertices;
// Push to whichever sides-bearing shape (Polygon or Star) the control bar's `shape_type` currently targets.
// `set_proto_node_input_for_selected_layers` skips selected layers without that proto node, making it a no-op.
let (id, index) = match self.options.shape_type {
ShapeType::Polygon => (regular_polygon::IDENTIFIER, regular_polygon::SidesInput::<u32>::INDEX),
ShapeType::Star => (star::IDENTIFIER, star::SidesInput::<u32>::INDEX),
_ => return,
};
graph_modification_utils::set_proto_node_input_for_selected_layers(context.document, id, index, TaggedValue::U32(vertices), responses);
}
ShapeOptionsUpdate::ArcType(arc_type) => {
self.options.arc_type = arc_type;
graph_modification_utils::set_proto_node_input_for_selected_layers(context.document, arc::IDENTIFIER, arc::ArcTypeInput::INDEX, TaggedValue::ArcType(arc_type), responses);
}
ShapeOptionsUpdate::SpiralType(spiral_type) => {
self.options.spiral_type = spiral_type;
graph_modification_utils::set_proto_node_input_for_selected_layers(
context.document,
spiral::IDENTIFIER,
spiral::SpiralTypeInput::INDEX,
TaggedValue::SpiralType(spiral_type),
responses,
);
}
ShapeOptionsUpdate::Turns(turns) => {
self.options.turns = turns;
graph_modification_utils::set_proto_node_input_for_selected_layers(context.document, spiral::IDENTIFIER, spiral::TurnsInput::INDEX, TaggedValue::F64(turns), responses);
}
ShapeOptionsUpdate::GridType(grid_type) => {
self.options.grid_type = grid_type;
graph_modification_utils::set_proto_node_input_for_selected_layers(context.document, grid::IDENTIFIER, grid::GridTypeInput::INDEX, TaggedValue::GridType(grid_type), responses);
}
ShapeOptionsUpdate::ArrowShaftWidth(shaft_width) => {
self.options.arrow_shaft_width = shaft_width;
graph_modification_utils::set_proto_node_input_for_selected_layers(context.document, arrow::IDENTIFIER, arrow::ShaftWidthInput::INDEX, TaggedValue::F64(shaft_width), responses);
}
ShapeOptionsUpdate::ArrowHeadWidth(head_width) => {
self.options.arrow_head_width = head_width;
graph_modification_utils::set_proto_node_input_for_selected_layers(context.document, arrow::IDENTIFIER, arrow::HeadWidthInput::INDEX, TaggedValue::F64(head_width), responses);
}
ShapeOptionsUpdate::ArrowHeadLength(head_length) => {
self.options.arrow_head_length = head_length;
graph_modification_utils::set_proto_node_input_for_selected_layers(context.document, arrow::IDENTIFIER, arrow::HeadLengthInput::INDEX, TaggedValue::F64(head_length), responses);
}
}

View File

@ -170,7 +170,8 @@ impl LayoutHolder for SplineTool {
impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for SplineTool {
fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, context: &mut ToolActionMessageContext<'a>) {
if matches!(&message, ToolMessage::Spline(SplineToolMessage::SelectionChanged)) {
if let Some(weight) = graph_modification_utils::first_selected_stroke_weight(context.document)
if self.fsm_state == SplineToolFsmState::Ready
&& let Some(weight) = graph_modification_utils::first_selected_stroke_weight(context.document)
&& self.options.line_weight != weight
{
self.options.line_weight = weight;