Skip to content
Open
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
37 changes: 33 additions & 4 deletions editor/src/messages/tool/tool_messages/brush_tool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use crate::messages::portfolio::document::node_graph::document_node_definitions:
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::network_interface::{FlowType, InputConnector, OutputConnector};
use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, selection_changed_since_last_sync, solid};
use crate::messages::tool::common_functionality::resize::viewport_zoom;
use graph_craft::document::value::TaggedValue;
use graph_craft::document::{NodeId, NodeInput};
use graphene_std::Color;
Expand All @@ -29,6 +30,7 @@ pub struct BrushOptions {
hardness: f64,
flow: f64,
color: ToolColorOptions,
autoscale: bool,
last_synced_selection: Vec<LayerNodeIdentifier>,
}

Expand All @@ -39,6 +41,7 @@ impl Default for BrushOptions {
hardness: BRUSH_HARDNESS_DEFAULT,
flow: BRUSH_FLOW_DEFAULT,
color: ToolColorOptions::default(),
autoscale: false,
last_synced_selection: Vec::new(),
}
}
Expand All @@ -48,6 +51,10 @@ impl BrushOptions {
fn active_color(&self) -> Color {
self.color.active_color().unwrap_or_default()
}

fn stroke_diameter(&self, document: &DocumentMessageHandler) -> f64 {
if self.autoscale { self.diameter / viewport_zoom(document) } else { self.diameter }
}
}

#[impl_message(Message, ToolMessage, Brush)]
Expand All @@ -74,6 +81,7 @@ pub enum BrushToolMessageOptionsUpdate {
Diameter(f64),
Hardness(f64),
Flow(f64),
Autoscale(bool),
WorkingColorsChanged,
}

Expand All @@ -98,6 +106,8 @@ impl ToolMetadata for BrushTool {

impl LayoutHolder for BrushTool {
fn layout(&self) -> Layout {
let autoscale_id = CheckboxId::new();
let autoscale_description = "Automatically scale the brush with viewport zoom.";
let widgets = vec![
ColorInput::new(FillChoice::<SRGBA8>::from(self.options.color.fill_choice.as_ref().unwrap_or(&FillChoice::None)))
.mixed(self.options.color.fill_choice.is_none())
Expand Down Expand Up @@ -149,6 +159,24 @@ impl LayoutHolder for BrushTool {
.into()
})
.widget_instance(),
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
CheckboxInput::new(self.options.autoscale)
.tooltip_label("Autoscale")
.tooltip_description(autoscale_description)
.for_label(autoscale_id)
.on_update(|checkbox_input: &CheckboxInput| {
BrushToolMessage::UpdateOptions {
options: BrushToolMessageOptionsUpdate::Autoscale(checkbox_input.checked),
}
.into()
})
.widget_instance(),
Separator::new(SeparatorStyle::Related).widget_instance(),
TextLabel::new("Autoscale")
.tooltip_label("Autoscale")
.tooltip_description(autoscale_description)
.for_checkbox(autoscale_id)
.widget_instance(),
];

Layout(vec![LayoutGroup::row(widgets)])
Expand Down Expand Up @@ -190,6 +218,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Brus
responses.add(ToolMessage::SelectWorkingColor { color, primary: true });
}
}
BrushToolMessageOptionsUpdate::Autoscale(autoscale) => self.options.autoscale = autoscale,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When the user toggles Autoscale with a brush layer selected, this branch reinterprets the unchanged diameter in the wrong unit. Convert options.diameter by the current viewport zoom when changing modes so the existing document-space diameter remains unchanged.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At editor/src/messages/tool/tool_messages/brush_tool.rs, line 221:

<comment>When the user toggles Autoscale with a brush layer selected, this branch reinterprets the unchanged diameter in the wrong unit. Convert `options.diameter` by the current viewport zoom when changing modes so the existing document-space diameter remains unchanged.</comment>

<file context>
@@ -190,6 +218,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Brus
 					responses.add(ToolMessage::SelectWorkingColor { color, primary: true });
 				}
 			}
+			BrushToolMessageOptionsUpdate::Autoscale(autoscale) => self.options.autoscale = autoscale,
 			BrushToolMessageOptionsUpdate::WorkingColorsChanged => {
 				self.options.color.fill_choice = Some(solid(context.global_tool_data.primary_color));
</file context>
Suggested change
BrushToolMessageOptionsUpdate::Autoscale(autoscale) => self.options.autoscale = autoscale,
BrushToolMessageOptionsUpdate::Autoscale(autoscale) => {
if self.options.autoscale != autoscale {
let zoom = viewport_zoom(context.document);
self.options.diameter = if autoscale { self.options.diameter * zoom } else { self.options.diameter / zoom };
}
self.options.autoscale = autoscale;
}

BrushToolMessageOptionsUpdate::WorkingColorsChanged => {
self.options.color.fill_choice = Some(solid(context.global_tool_data.primary_color));
}
Expand Down Expand Up @@ -234,7 +263,7 @@ impl BrushTool {
};
let value = |index: usize| node.inputs.get(index).and_then(|input| input.as_value());
if let Some(TaggedValue::F64(diameter)) = value(STROKES_DIAMETER_INPUT) {
self.options.diameter = *diameter;
self.options.diameter = if self.options.autoscale { *diameter * viewport_zoom(document) } else { *diameter };
}
if let Some(TaggedValue::F64(hardness)) = value(STROKES_HARDNESS_INPUT) {
self.options.hardness = *hardness;
Expand Down Expand Up @@ -355,7 +384,7 @@ impl BrushToolData {
};
let value = |index: usize| node.inputs.get(index).and_then(|input| input.as_value());
matches!(value(STROKES_COLOR_INPUT), Some(TaggedValue::Color(color)) if *color == options.active_color())
&& matches!(value(STROKES_DIAMETER_INPUT), Some(TaggedValue::F64(diameter)) if *diameter == options.diameter)
&& matches!(value(STROKES_DIAMETER_INPUT), Some(TaggedValue::F64(diameter)) if *diameter == options.stroke_diameter(document))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When autoscale selection sync round-trips a diameter through a non-representable zoom, exact equality rejects the matching stroke and creates a new brush group. Compare the diameters with a scale-aware floating-point tolerance.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At editor/src/messages/tool/tool_messages/brush_tool.rs, line 387:

<comment>When autoscale selection sync round-trips a diameter through a non-representable zoom, exact equality rejects the matching stroke and creates a new brush group. Compare the diameters with a scale-aware floating-point tolerance.</comment>

<file context>
@@ -355,7 +384,7 @@ impl BrushToolData {
 		let value = |index: usize| node.inputs.get(index).and_then(|input| input.as_value());
 		matches!(value(STROKES_COLOR_INPUT), Some(TaggedValue::Color(color)) if *color == options.active_color())
-			&& matches!(value(STROKES_DIAMETER_INPUT), Some(TaggedValue::F64(diameter)) if *diameter == options.diameter)
+			&& matches!(value(STROKES_DIAMETER_INPUT), Some(TaggedValue::F64(diameter)) if *diameter == options.stroke_diameter(document))
 			&& matches!(value(STROKES_HARDNESS_INPUT), Some(TaggedValue::F64(hardness)) if *hardness == options.hardness)
 			&& matches!(value(STROKES_FLOW_INPUT), Some(TaggedValue::F64(flow)) if *flow == options.flow)
</file context>
Suggested change
&& matches!(value(STROKES_DIAMETER_INPUT), Some(TaggedValue::F64(diameter)) if *diameter == options.stroke_diameter(document))
&& matches!(value(STROKES_DIAMETER_INPUT), Some(TaggedValue::F64(diameter)) if (*diameter - options.stroke_diameter(document)).abs() <= f64::EPSILON * diameter.abs().max(options.stroke_diameter(document).abs()).max(1.))

&& matches!(value(STROKES_HARDNESS_INPUT), Some(TaggedValue::F64(hardness)) if *hardness == options.hardness)
&& matches!(value(STROKES_FLOW_INPUT), Some(TaggedValue::F64(flow)) if *flow == options.flow)
}
Expand Down Expand Up @@ -447,7 +476,7 @@ impl Fsm for BrushToolFsmState {
parent,
insert_index,
color: tool_options.active_color(),
diameter: tool_options.diameter,
diameter: tool_options.stroke_diameter(document),
hardness: tool_options.hardness,
flow: tool_options.flow,
});
Expand All @@ -461,7 +490,7 @@ impl Fsm for BrushToolFsmState {
layer,
strokes_node_id,
color: tool_options.active_color(),
diameter: tool_options.diameter,
diameter: tool_options.stroke_diameter(document),
hardness: tool_options.hardness,
flow: tool_options.flow,
});
Expand Down
Loading