Skip to content

Commit 525e49f

Browse files
authored
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
1 parent 1596469 commit 525e49f

6 files changed

Lines changed: 214 additions & 4 deletions

File tree

editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1744,6 +1744,19 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
17441744
let reference = network_interface.reference(&node_id, selection_network_path);
17451745
let is_text_node = reference.as_ref().is_some_and(|r| *r == DefinitionIdentifier::ProtoNode(graphene_std::text::text::IDENTIFIER));
17461746
let is_stroke_node = reference.as_ref().is_some_and(|r| *r == DefinitionIdentifier::ProtoNode(graphene_std::vector::stroke::IDENTIFIER));
1747+
let is_shape_generator_node = reference.as_ref().is_some_and(|r| {
1748+
[
1749+
graphene_std::vector::generator_nodes::regular_polygon::IDENTIFIER,
1750+
graphene_std::vector::generator_nodes::star::IDENTIFIER,
1751+
graphene_std::vector::generator_nodes::arc::IDENTIFIER,
1752+
graphene_std::vector::generator_nodes::spiral::IDENTIFIER,
1753+
graphene_std::vector::generator_nodes::grid::IDENTIFIER,
1754+
graphene_std::vector::generator_nodes::arrow::IDENTIFIER,
1755+
]
1756+
.into_iter()
1757+
.any(|id| *r == DefinitionIdentifier::ProtoNode(id))
1758+
});
1759+
17471760
let input = NodeInput::value(value, false);
17481761
responses.add(NodeGraphMessage::SetInput {
17491762
input_connector: InputConnector::node(node_id, input_index),
@@ -1756,7 +1769,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
17561769
if is_text_node {
17571770
responses.add(TextToolMessage::SelectionChanged);
17581771
}
1759-
if is_stroke_node {
1772+
if is_stroke_node || is_shape_generator_node {
17601773
// The dispatcher delivers each only to its tool when active, so this just covers all four stroke-using tools.
17611774
responses.add(PenToolMessage::SelectionChanged);
17621775
responses.add(FreehandToolMessage::SelectionChanged);

editor/src/messages/tool/common_functionality/graph_modification_utils.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -526,6 +526,43 @@ pub fn set_stroke_weight_for_selected_layers(weight: f64, document: &DocumentMes
526526
}
527527
}
528528

529+
/// Reads a specific input from the matching proto node on the first selected non-artboard layer that has one.
530+
/// Used by tool control bars to mirror per-shape parameters (sides, arc type, turns, etc.) from the selection
531+
/// into the control bar's input widget state without each call site re-implementing the layer iteration.
532+
pub fn first_selected_proto_node_input(document: &DocumentMessageHandler, identifier: graph_craft::ProtoNodeIdentifier, input_index: usize) -> Option<&TaggedValue> {
533+
let identifier = DefinitionIdentifier::ProtoNode(identifier);
534+
document
535+
.network_interface
536+
.selected_nodes()
537+
.selected_layers_except_artboards(&document.network_interface)
538+
.find_map(|layer| NodeGraphLayer::new(layer, &document.network_interface).find_input(&identifier, input_index))
539+
}
540+
541+
/// Writes a value to a specific input on the matching proto node of every selected non-artboard layer that has one.
542+
/// Used by tool control bars to push per-shape parameter changes back onto all selected layers of that shape.
543+
pub fn set_proto_node_input_for_selected_layers(
544+
document: &DocumentMessageHandler,
545+
identifier: graph_craft::ProtoNodeIdentifier,
546+
input_index: usize,
547+
value: TaggedValue,
548+
responses: &mut VecDeque<Message>,
549+
) {
550+
let identifier = DefinitionIdentifier::ProtoNode(identifier);
551+
552+
let layers: Vec<_> = document.network_interface.selected_nodes().selected_layers_except_artboards(&document.network_interface).collect();
553+
554+
for layer in layers {
555+
let Some(node_id) = NodeGraphLayer::new(layer, &document.network_interface).upstream_node_id_from_name(&identifier) else {
556+
continue;
557+
};
558+
responses.add(NodeGraphMessage::SetInputValue {
559+
node_id,
560+
input_index,
561+
value: value.clone(),
562+
});
563+
}
564+
}
565+
529566
/// Checks if a specified layer uses an upstream node matching the given name.
530567
pub fn is_layer_fed_by_node_of_name(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface, identifier: &DefinitionIdentifier) -> bool {
531568
NodeGraphLayer::new(layer, network_interface).find_node_inputs(identifier).is_some()

editor/src/messages/tool/tool_messages/freehand_tool.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,8 @@ impl LayoutHolder for FreehandTool {
163163
impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for FreehandTool {
164164
fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, context: &mut ToolActionMessageContext<'a>) {
165165
if matches!(&message, ToolMessage::Freehand(FreehandToolMessage::SelectionChanged)) {
166-
if let Some(weight) = graph_modification_utils::first_selected_stroke_weight(context.document)
166+
if self.fsm_state == FreehandToolFsmState::Ready
167+
&& let Some(weight) = graph_modification_utils::first_selected_stroke_weight(context.document)
167168
&& self.options.line_weight != weight
168169
{
169170
self.options.line_weight = weight;

editor/src/messages/tool/tool_messages/pen_tool.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,7 @@ impl LayoutHolder for PenTool {
250250
impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for PenTool {
251251
fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, context: &mut ToolActionMessageContext<'a>) {
252252
if matches!(&message, ToolMessage::Pen(PenToolMessage::SelectionChanged))
253+
&& self.fsm_state == PenToolFsmState::Ready
253254
&& let Some(weight) = graph_modification_utils::first_selected_stroke_weight(context.document)
254255
&& self.options.line_weight != weight
255256
{

editor/src/messages/tool/tool_messages/shape_tool.rs

Lines changed: 158 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use super::tool_prelude::*;
22
use crate::consts::{BOUNDS_SELECT_THRESHOLD, DEFAULT_STROKE_WIDTH, SNAP_POINT_TOLERANCE};
33
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
4+
use crate::messages::portfolio::document::node_graph::document_node_definitions::DefinitionIdentifier;
45
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
56
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
67
use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
@@ -22,9 +23,10 @@ use crate::messages::tool::common_functionality::snapping::{self, SnapCandidateP
2223
use crate::messages::tool::common_functionality::transformation_cage::{BoundingBoxManager, EdgeBool};
2324
use crate::messages::tool::common_functionality::utility_functions::{closest_point, resize_bounds, rotate_bounds, skew_bounds, transforming_transform_cage};
2425
use graph_craft::document::NodeId;
25-
use graphene_std::Color;
26+
use graph_craft::document::value::TaggedValue;
2627
use graphene_std::renderer::Quad;
2728
use graphene_std::vector::misc::{ArcType, GridType, SpiralType};
29+
use graphene_std::{Color, NodeInputDecleration};
2830
use std::vec;
2931

3032
#[derive(Default, ExtractField)]
@@ -127,6 +129,7 @@ fn create_sides_widget(vertices: u32) -> WidgetInstance {
127129
}
128130
.into()
129131
})
132+
.on_commit(|_| DocumentMessage::StartTransaction.into())
130133
.widget_instance()
131134
}
132135

@@ -141,6 +144,7 @@ fn create_turns_widget(turns: f64) -> WidgetInstance {
141144
}
142145
.into()
143146
})
147+
.on_commit(|_| DocumentMessage::StartTransaction.into())
144148
.widget_instance()
145149
}
146150

@@ -243,6 +247,7 @@ fn create_arrow_shaft_width_widget(shaft_width: f64) -> WidgetInstance {
243247
}
244248
.into()
245249
})
250+
.on_commit(|_| DocumentMessage::StartTransaction.into())
246251
.widget_instance()
247252
}
248253

@@ -258,6 +263,7 @@ fn create_arrow_head_width_widget(head_width: f64) -> WidgetInstance {
258263
}
259264
.into()
260265
})
266+
.on_commit(|_| DocumentMessage::StartTransaction.into())
261267
.widget_instance()
262268
}
263269

@@ -273,6 +279,7 @@ fn create_arrow_head_length_widget(head_length: f64) -> WidgetInstance {
273279
}
274280
.into()
275281
})
282+
.on_commit(|_| DocumentMessage::StartTransaction.into())
276283
.widget_instance()
277284
}
278285

@@ -312,6 +319,118 @@ fn create_grid_type_widget(grid_type: GridType) -> WidgetInstance {
312319
RadioInput::new(entries).selected_index(Some(grid_type as u32)).widget_instance()
313320
}
314321

322+
/// Mirrors the per-shape parameters (and `shape_type` itself) from the first selected non-artboard layer into the
323+
/// control bar's option state. Detects the layer's shape by trying each generator's proto node, then reads only the
324+
/// inputs relevant to that shape. Returns whether anything in `options` (or `tool_data.current_shape`) changed.
325+
/// The caller decides whether to dispatch a layout refresh.
326+
fn sync_shape_options_from_selection(options: &mut ShapeToolOptions, tool_data: &mut ShapeToolData, document: &DocumentMessageHandler) -> bool {
327+
use graphene_std::vector::generator_nodes::*;
328+
329+
let Some(layer) = document.network_interface.selected_nodes().selected_layers_except_artboards(&document.network_interface).next() else {
330+
return false;
331+
};
332+
let layer_view = graph_modification_utils::NodeGraphLayer::new(layer, &document.network_interface);
333+
let proto = DefinitionIdentifier::ProtoNode;
334+
335+
// Map each generator's proto node to the corresponding `ShapeType`.
336+
// First match wins. Only includes modes from the Shape tool's mode dropdown.
337+
let Some(shape_type) = [
338+
(regular_polygon::IDENTIFIER, ShapeType::Polygon),
339+
(star::IDENTIFIER, ShapeType::Star),
340+
(circle::IDENTIFIER, ShapeType::Circle),
341+
(arc::IDENTIFIER, ShapeType::Arc),
342+
(spiral::IDENTIFIER, ShapeType::Spiral),
343+
(grid::IDENTIFIER, ShapeType::Grid),
344+
(arrow::IDENTIFIER, ShapeType::Arrow),
345+
]
346+
.into_iter()
347+
.find_map(|(id, shape)| layer_view.upstream_node_id_from_name(&proto(id)).map(|_| shape)) else {
348+
return false;
349+
};
350+
351+
let mut changed = false;
352+
353+
if options.shape_type != shape_type {
354+
options.shape_type = shape_type;
355+
tool_data.current_shape = shape_type;
356+
changed = true;
357+
}
358+
359+
// Only the shapes whose control bar exposes per-shape parameters need a sync below.
360+
// The rest (Ellipse, Rectangle, Line) just keep `shape_type` in step and rely on the shared Stroke/Fill controls.
361+
match shape_type {
362+
ShapeType::Polygon | ShapeType::Star => {
363+
let id = if shape_type == ShapeType::Polygon { regular_polygon::IDENTIFIER } else { star::IDENTIFIER };
364+
// Both `regular_polygon` and `star` are generic over `T: AsU64`, but the control bar widget always writes `u32`,
365+
// and existing call sites (e.g. `polygon_shape.rs`) read it back as `TaggedValue::U32`.
366+
let index = if shape_type == ShapeType::Polygon {
367+
regular_polygon::SidesInput::<u32>::INDEX
368+
} else {
369+
star::SidesInput::<u32>::INDEX
370+
};
371+
if let Some(&TaggedValue::U32(sides)) = layer_view.find_input(&proto(id), index)
372+
&& options.vertices != sides
373+
{
374+
options.vertices = sides;
375+
changed = true;
376+
}
377+
}
378+
ShapeType::Arc => {
379+
if let Some(&TaggedValue::ArcType(arc_type)) = layer_view.find_input(&proto(arc::IDENTIFIER), arc::ArcTypeInput::INDEX)
380+
&& options.arc_type != arc_type
381+
{
382+
options.arc_type = arc_type;
383+
changed = true;
384+
}
385+
}
386+
ShapeType::Spiral => {
387+
if let Some(&TaggedValue::SpiralType(spiral_type)) = layer_view.find_input(&proto(spiral::IDENTIFIER), spiral::SpiralTypeInput::INDEX)
388+
&& options.spiral_type != spiral_type
389+
{
390+
options.spiral_type = spiral_type;
391+
changed = true;
392+
}
393+
if let Some(&TaggedValue::F64(turns)) = layer_view.find_input(&proto(spiral::IDENTIFIER), spiral::TurnsInput::INDEX)
394+
&& options.turns != turns
395+
{
396+
options.turns = turns;
397+
changed = true;
398+
}
399+
}
400+
ShapeType::Grid => {
401+
if let Some(&TaggedValue::GridType(grid_type)) = layer_view.find_input(&proto(grid::IDENTIFIER), grid::GridTypeInput::INDEX)
402+
&& options.grid_type != grid_type
403+
{
404+
options.grid_type = grid_type;
405+
changed = true;
406+
}
407+
}
408+
ShapeType::Arrow => {
409+
if let Some(&TaggedValue::F64(shaft)) = layer_view.find_input(&proto(arrow::IDENTIFIER), arrow::ShaftWidthInput::INDEX)
410+
&& options.arrow_shaft_width != shaft
411+
{
412+
options.arrow_shaft_width = shaft;
413+
changed = true;
414+
}
415+
if let Some(&TaggedValue::F64(head_w)) = layer_view.find_input(&proto(arrow::IDENTIFIER), arrow::HeadWidthInput::INDEX)
416+
&& options.arrow_head_width != head_w
417+
{
418+
options.arrow_head_width = head_w;
419+
changed = true;
420+
}
421+
if let Some(&TaggedValue::F64(head_l)) = layer_view.find_input(&proto(arrow::IDENTIFIER), arrow::HeadLengthInput::INDEX)
422+
&& options.arrow_head_length != head_l
423+
{
424+
options.arrow_head_length = head_l;
425+
changed = true;
426+
}
427+
}
428+
ShapeType::Ellipse | ShapeType::Rectangle | ShapeType::Line | ShapeType::Circle => {}
429+
}
430+
431+
changed
432+
}
433+
315434
impl LayoutHolder for ShapeTool {
316435
fn layout(&self) -> Layout {
317436
let mut widgets = vec![];
@@ -416,11 +535,28 @@ impl LayoutHolder for ShapeTool {
416535
#[message_handler_data]
417536
impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for ShapeTool {
418537
fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, context: &mut ToolActionMessageContext<'a>) {
538+
use graphene_std::vector::generator_nodes::*;
539+
419540
if matches!(&message, ToolMessage::Shape(ShapeToolMessage::SelectionChanged)) {
541+
if !matches!(self.fsm_state, ShapeToolFsmState::Ready(_)) {
542+
return;
543+
}
544+
545+
let mut needs_refresh = false;
546+
547+
// Stroke weight is shape-agnostic. Sync it regardless of which (if any) shape proto node the layer has.
420548
if let Some(weight) = graph_modification_utils::first_selected_stroke_weight(context.document)
421549
&& self.options.line_weight != weight
422550
{
423551
self.options.line_weight = weight;
552+
needs_refresh = true;
553+
}
554+
555+
// Detect which shape the first selected layer is by checking for each generator's proto node, then mirror
556+
// the control bar's `shape_type` into that and pull the shape's parameters into the matching control bar fields.
557+
needs_refresh |= sync_shape_options_from_selection(&mut self.options, &mut self.tool_data, context.document);
558+
559+
if needs_refresh {
424560
self.send_layout(responses, LayoutTarget::ToolOptions);
425561
}
426562
return;
@@ -461,27 +597,48 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Shap
461597
}
462598
ShapeOptionsUpdate::Vertices(vertices) => {
463599
self.options.vertices = vertices;
600+
// Push to whichever sides-bearing shape (Polygon or Star) the control bar's `shape_type` currently targets.
601+
// `set_proto_node_input_for_selected_layers` skips selected layers without that proto node, making it a no-op.
602+
let (id, index) = match self.options.shape_type {
603+
ShapeType::Polygon => (regular_polygon::IDENTIFIER, regular_polygon::SidesInput::<u32>::INDEX),
604+
ShapeType::Star => (star::IDENTIFIER, star::SidesInput::<u32>::INDEX),
605+
_ => return,
606+
};
607+
graph_modification_utils::set_proto_node_input_for_selected_layers(context.document, id, index, TaggedValue::U32(vertices), responses);
464608
}
465609
ShapeOptionsUpdate::ArcType(arc_type) => {
466610
self.options.arc_type = arc_type;
611+
graph_modification_utils::set_proto_node_input_for_selected_layers(context.document, arc::IDENTIFIER, arc::ArcTypeInput::INDEX, TaggedValue::ArcType(arc_type), responses);
467612
}
468613
ShapeOptionsUpdate::SpiralType(spiral_type) => {
469614
self.options.spiral_type = spiral_type;
615+
graph_modification_utils::set_proto_node_input_for_selected_layers(
616+
context.document,
617+
spiral::IDENTIFIER,
618+
spiral::SpiralTypeInput::INDEX,
619+
TaggedValue::SpiralType(spiral_type),
620+
responses,
621+
);
470622
}
471623
ShapeOptionsUpdate::Turns(turns) => {
472624
self.options.turns = turns;
625+
graph_modification_utils::set_proto_node_input_for_selected_layers(context.document, spiral::IDENTIFIER, spiral::TurnsInput::INDEX, TaggedValue::F64(turns), responses);
473626
}
474627
ShapeOptionsUpdate::GridType(grid_type) => {
475628
self.options.grid_type = grid_type;
629+
graph_modification_utils::set_proto_node_input_for_selected_layers(context.document, grid::IDENTIFIER, grid::GridTypeInput::INDEX, TaggedValue::GridType(grid_type), responses);
476630
}
477631
ShapeOptionsUpdate::ArrowShaftWidth(shaft_width) => {
478632
self.options.arrow_shaft_width = shaft_width;
633+
graph_modification_utils::set_proto_node_input_for_selected_layers(context.document, arrow::IDENTIFIER, arrow::ShaftWidthInput::INDEX, TaggedValue::F64(shaft_width), responses);
479634
}
480635
ShapeOptionsUpdate::ArrowHeadWidth(head_width) => {
481636
self.options.arrow_head_width = head_width;
637+
graph_modification_utils::set_proto_node_input_for_selected_layers(context.document, arrow::IDENTIFIER, arrow::HeadWidthInput::INDEX, TaggedValue::F64(head_width), responses);
482638
}
483639
ShapeOptionsUpdate::ArrowHeadLength(head_length) => {
484640
self.options.arrow_head_length = head_length;
641+
graph_modification_utils::set_proto_node_input_for_selected_layers(context.document, arrow::IDENTIFIER, arrow::HeadLengthInput::INDEX, TaggedValue::F64(head_length), responses);
485642
}
486643
}
487644

editor/src/messages/tool/tool_messages/spline_tool.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,8 @@ impl LayoutHolder for SplineTool {
170170
impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for SplineTool {
171171
fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, context: &mut ToolActionMessageContext<'a>) {
172172
if matches!(&message, ToolMessage::Spline(SplineToolMessage::SelectionChanged)) {
173-
if let Some(weight) = graph_modification_utils::first_selected_stroke_weight(context.document)
173+
if self.fsm_state == SplineToolFsmState::Ready
174+
&& let Some(weight) = graph_modification_utils::first_selected_stroke_weight(context.document)
174175
&& self.options.line_weight != weight
175176
{
176177
self.options.line_weight = weight;

0 commit comments

Comments
 (0)