Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
3 changes: 2 additions & 1 deletion editor/src/messages/tool/tool_messages/freehand_tool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions editor/src/messages/tool/tool_messages/pen_tool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
159 changes: 158 additions & 1 deletion editor/src/messages/tool/tool_messages/shape_tool.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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)]
Expand Down Expand Up @@ -127,6 +129,7 @@ fn create_sides_widget(vertices: u32) -> WidgetInstance {
}
.into()
})
.on_commit(|_| DocumentMessage::StartTransaction.into())
.widget_instance()
}

Expand All @@ -141,6 +144,7 @@ fn create_turns_widget(turns: f64) -> WidgetInstance {
}
.into()
})
.on_commit(|_| DocumentMessage::StartTransaction.into())
.widget_instance()
}

Expand Down Expand Up @@ -243,6 +247,7 @@ fn create_arrow_shaft_width_widget(shaft_width: f64) -> WidgetInstance {
}
.into()
})
.on_commit(|_| DocumentMessage::StartTransaction.into())
.widget_instance()
}

Expand All @@ -258,6 +263,7 @@ fn create_arrow_head_width_widget(head_width: f64) -> WidgetInstance {
}
.into()
})
.on_commit(|_| DocumentMessage::StartTransaction.into())
.widget_instance()
}

Expand All @@ -273,6 +279,7 @@ fn create_arrow_head_length_widget(head_length: f64) -> WidgetInstance {
}
.into()
})
.on_commit(|_| DocumentMessage::StartTransaction.into())
.widget_instance()
}

Expand Down Expand Up @@ -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),
]
Comment thread
Keavon marked this conversation as resolved.
.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![];
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
}

Expand Down
3 changes: 2 additions & 1 deletion editor/src/messages/tool/tool_messages/spline_tool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading