Skip to content
Open
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
5 changes: 4 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion editor/src/consts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,9 @@ pub const LINE_ROTATE_SNAP_ANGLE: f64 = 15.;

// BRUSH TOOL
pub const BRUSH_SIZE_CHANGE_KEYBOARD: f64 = 5.;
pub const DEFAULT_BRUSH_SIZE: f64 = 20.;
pub const BRUSH_SIZE_DEFAULT: f64 = 40.;
pub const BRUSH_HARDNESS_DEFAULT: f64 = 0.;
pub const BRUSH_FLOW_DEFAULT: f64 = 100.;

// EYEDROPPER TOOL
pub const EYEDROPPER_PREVIEW_AREA_RESOLUTION: u32 = 11;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1066,7 +1066,10 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
(true, Some(storage)) => storage
.export_to_bytes(
document_format::ExportFormat::Xz,
document_format::ExportOptions::default(),
document_format::ExportOptions {
include_history: false,

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 .gdd saving is enabled, this drops the document's undo/redo history on every save. Reopening then bootstraps a flat registry instead of restoring the saved history; keep history enabled for normal document saves.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At editor/src/messages/portfolio/document/document_message_handler.rs, line 1070:

<comment>When `.gdd` saving is enabled, this drops the document's undo/redo history on every save. Reopening then bootstraps a flat registry instead of restoring the saved history; keep history enabled for normal document saves.</comment>

<file context>
@@ -1066,7 +1066,10 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
 								document_format::ExportFormat::Xz,
-								document_format::ExportOptions::default(),
+								document_format::ExportOptions {
+									include_history: false,
+									..Default::default()
+								},
</file context>
Suggested change
include_history: false,
include_history: true,

..Default::default()
},
export_load_handle.as_ref(),
Some(&legacy_document),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ use crate::messages::prelude::*;
use glam::{DAffine2, DVec2};
use graph_craft::document::NodeId;
use graphene_std::Color;
use graphene_std::brush::brush_stroke::BrushStroke;
use graphene_std::raster::BlendMode;
use graphene_std::raster_types::Image;
use graphene_std::text::{Font, TypesettingConfig};
Expand Down Expand Up @@ -112,9 +111,23 @@ pub enum GraphOperationMessage {
layer: LayerNodeIdentifier,
modification_type: VectorModificationType,
},
Brush {
NewBrushGroupLayer {
id: NodeId,
strokes_node_id: NodeId,
parent: LayerNodeIdentifier,
insert_index: usize,
color: Color,
diameter: f64,
hardness: f64,
flow: f64,
},
NewBrushStrokesNode {
layer: LayerNodeIdentifier,
strokes: Vec<BrushStroke>,
strokes_node_id: NodeId,
color: Color,
diameter: f64,
hardness: f64,
flow: f64,
},
SetUpstreamToChain {
layer: LayerNodeIdentifier,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ use super::transform_utils;
use super::utility_types::{ModifyInputsContext, set_stroke_paint_order};
use crate::consts::{LAYER_INDENT_OFFSET, STACK_VERTICAL_GAP};
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
use crate::messages::portfolio::document::node_graph::document_node_definitions::{BLEND_PATH_INPUT_INDEX, DefinitionIdentifier};
use crate::messages::portfolio::document::node_graph::document_node_definitions::{BLEND_PATH_INPUT_INDEX, DefinitionIdentifier, resolve_proto_node_type};
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeNetworkInterface, OutputConnector};
use crate::messages::portfolio::document::utility_types::nodes::CollapsedLayers;
use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::graph_modification_utils::get_clip_mode;
use glam::{DAffine2, DVec2, IVec2};
use graph_craft::document::value::TaggedValue;
use graph_craft::document::{NodeId, NodeInput};
use graph_craft::list;
use graphene_std::renderer::convert_usvg_path::convert_usvg_path;
Expand Down Expand Up @@ -172,10 +173,35 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
modify_inputs.vector_modify(modification_type);
}
}
GraphOperationMessage::Brush { layer, strokes } => {
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
modify_inputs.brush_modify(strokes);
}
GraphOperationMessage::NewBrushGroupLayer {
id,
strokes_node_id,
parent,
insert_index,
color,
diameter,
hardness,
flow,
} => {
let layer = ModifyInputsContext::new(network_interface, responses).create_layer(id);
insert_brush_strokes_chain(network_interface, layer, strokes_node_id, color, diameter, hardness, flow);

responses.add(NodeGraphMessage::MoveLayerToStack { layer, parent, insert_index });
responses.add(GraphOperationMessage::SetUpstreamToChain { layer });
responses.add(NodeGraphMessage::RunDocumentGraph);
}
GraphOperationMessage::NewBrushStrokesNode {
layer,
strokes_node_id,
color,
diameter,
hardness,
flow,
} => {
insert_brush_strokes_chain(network_interface, layer, strokes_node_id, color, diameter, hardness, flow);

responses.add(GraphOperationMessage::SetUpstreamToChain { layer });
responses.add(NodeGraphMessage::RunDocumentGraph);
}
GraphOperationMessage::SetUpstreamToChain { layer } => {
let Some(OutputConnector::Node { node_id: first_chain_node, .. }) = network_interface.upstream_output_connector(&InputConnector::layer_secondary_input(layer.to_node()), &[]) else {
Expand Down Expand Up @@ -847,6 +873,22 @@ fn import_usvg_node_inner(
}
}

fn insert_brush_strokes_chain(network_interface: &mut NodeNetworkInterface, layer: LayerNodeIdentifier, strokes_node_id: NodeId, color: Color, diameter: f64, hardness: f64, flow: f64) {
let Some(strokes_node_type) = resolve_proto_node_type(graphene_std::brush::brush_strokes::IDENTIFIER) else {
log::error!("Brush strokes node does not exist");
return;
};
let strokes_node = strokes_node_type.node_template_input_override([
Some(NodeInput::value(TaggedValue::Strokes(Vec::new()), false)),
Some(NodeInput::value(TaggedValue::Color(color), false)),
Some(NodeInput::value(TaggedValue::F64(diameter), false)),
Some(NodeInput::value(TaggedValue::F64(hardness), false)),
Some(NodeInput::value(TaggedValue::F64(flow), false)),
]);
network_interface.insert_node(strokes_node_id, strokes_node, &[]);
network_interface.set_input(&InputConnector::node_at_index(layer.to_node(), 1), NodeInput::node(strokes_node_id, 0), &[]);
}

/// Helper to apply path data (vector geometry, fill, stroke, transform) to a layer.
fn import_usvg_path(modify_inputs: &mut ModifyInputsContext, node: &usvg::Node, path: &usvg::Path, layer: LayerNodeIdentifier, gradient_info: &SvgGradientInfo) {
let bezpath = convert_usvg_path(path);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ use graph_craft::application_io::resource::ResourceId;
use graph_craft::document::value::TaggedValue;
use graph_craft::document::{NodeId, NodeInput};
use graph_craft::{ProtoNodeIdentifier, list};
use graphene_std::brush::brush_stroke::BrushStroke;
use graphene_std::raster::BlendMode;
use graphene_std::raster_types::Image;
use graphene_std::text::{Font, TypesettingConfig};
Expand Down Expand Up @@ -977,17 +976,6 @@ impl<'a> ModifyInputsContext<'a> {
self.responses.add(NodeGraphMessage::RunDocumentGraph);
}

pub fn brush_modify(&mut self, strokes: Vec<BrushStroke>) {
let Some(brush_node_id) = self.existing_proto_node_id(graphene_std::brush::brush::brush::IDENTIFIER, true) else {
return;
};
self.set_input_with_refresh(
InputConnector::node(brush_node_id, graphene_std::brush::brush::brush::TraceInput),
NodeInput::value(TaggedValue::BrushStrokes(strokes), false),
false,
);
}

pub fn resize_artboard(&mut self, location: DVec2, dimensions: DVec2) {
let Some(artboard_node_id) = self.existing_network_node_id("Artboard", true) else {
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ use graph_craft::document::value::TaggedValue;
use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput};
use graph_craft::{Type, concrete};
use graphene_std::animation::RealTimeMode;
use graphene_std::brush::brush_stroke::BrushTrace;
use graphene_std::color::SRGBA8;
use graphene_std::extract_xy::XY;
use graphene_std::raster::{
Expand Down Expand Up @@ -286,7 +285,6 @@ pub(crate) fn property_from_type(
Some(x) if id_is::<DAffine2>(x) => transform_widget(default_info, &mut extra_widgets),
Some(x) if id_is::<Color>(x) => color_widget(default_info, ColorInput::default().allow_none(false)),
Some(x) if id_is::<Gradient>(x) => color_widget(default_info, ColorInput::default().allow_none(false)),
Some(x) if id_is::<BrushTrace>(x) => brush_strokes_widget(default_info).into(),

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 brush-strokes node is selected, its List<Stroke> input now falls through to the unsupported-widget placeholder because this change removes the only brush-stroke summary widget. Restore an equivalent Stroke-list widget, or add the new list shape to the supported property dispatch.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At editor/src/messages/portfolio/document/node_graph/node_properties.rs, line 289:

<comment>When the brush-strokes node is selected, its `List<Stroke>` input now falls through to the unsupported-widget placeholder because this change removes the only brush-stroke summary widget. Restore an equivalent `Stroke`-list widget, or add the new list shape to the supported property dispatch.</comment>

<file context>
@@ -286,7 +285,6 @@ pub(crate) fn property_from_type(
 						Some(x) if id_is::<Gradient>(x) => color_widget(default_info, ColorInput::default().allow_none(false)),
-						Some(x) if id_is::<BrushTrace>(x) => brush_strokes_widget(default_info).into(),
 						// ============
 						// STRUCT TYPES
 						// ============
@@ -460,33 +458,6 @@ pub fn vector_modification_widget(parameter_widgets_info: ParameterWidgetsInfo)
</file context>

// ============
// STRUCT TYPES
// ============
Expand Down Expand Up @@ -460,33 +458,6 @@ pub fn vector_modification_widget(parameter_widgets_info: ParameterWidgetsInfo)
widgets
}

pub fn brush_strokes_widget(parameter_widgets_info: ParameterWidgetsInfo) -> Vec<WidgetInstance> {
let ParameterWidgetsInfo { document_node, node_id: _, index, .. } = parameter_widgets_info;

let mut widgets = start_widgets(&parameter_widgets_info);

let Some(document_node) = document_node else { return widgets };
let Some(input) = document_node.inputs.get(index) else { return widgets };

if let Some(TaggedValue::BrushStrokes(strokes)) = input.as_non_exposed_value() {
let stroke_count = strokes.len();
let sample_count: usize = strokes.iter().map(|s| s.trace.len()).sum();
let label = if stroke_count == 0 {
"Empty".to_string()
} else {
format!(
"{stroke_count} {} / {sample_count} {}",
if stroke_count == 1 { "Stroke" } else { "Strokes" },
if sample_count == 1 { "Sample" } else { "Samples" }
)
};

widgets.extend_from_slice(&[Separator::new(SeparatorStyle::Unrelated).widget_instance(), TextLabel::new(label).widget_instance()]);
}

widgets
}

pub fn image_data_widget(parameter_widgets_info: ParameterWidgetsInfo) -> Vec<WidgetInstance> {
let ParameterWidgetsInfo { document_node, node_id: _, index, .. } = parameter_widgets_info;

Expand Down
73 changes: 0 additions & 73 deletions editor/src/messages/portfolio/document_migration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,25 +76,6 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[
aliases: &["graphene_core::raster::OpacityNode", "graphene_core::blending_nodes::OpacityNode"],
},
// ================================
// brush
// ================================
NodeReplacement {
node: graphene_std::brush::brush::blit::IDENTIFIER,
aliases: &["graphene_brush::BlitNode", "graphene_std::brush::BlitNode", "graphene_brush::brush::BlitNode"],
},
NodeReplacement {
node: graphene_std::brush::brush::brush::IDENTIFIER,
aliases: &["graphene_brush::BrushNode", "graphene_std::brush::BrushNode", "graphene_brush::brush::BrushNode"],
},
NodeReplacement {
node: graphene_std::brush::brush::brush_stamp_generator::IDENTIFIER,
aliases: &[
"graphene_brush::BrushStampGeneratorNode",
"graphene_std::brush::BrushStampGeneratorNode",
"graphene_brush::brush::BrushStampGeneratorNode",
],
},
// ================================
// gcore
// ================================
NodeReplacement {
Expand Down Expand Up @@ -1189,30 +1170,6 @@ pub fn document_migration_upgrades(document: &mut DocumentMessageHandler, reset_
}
}

// The "Brush" wrapper network was replaced with the `brush` proto node directly. Convert old `Network("Brush")` instances to the proto node, forwarding all 3 inputs (Background, Trace, Cache) one-to-one.
// This must run as a pre-pass before the recursive iteration below: replacing the outer Brush's network impl orphans its child paths, and the recursive iteration would log errors for those stale paths.
let brush_layers: Vec<(NodeId, Vec<NodeId>)> = document
.network_interface
.document_network()
.recursive_nodes()
.filter_map(|(node_id, _, path)| (document.network_interface.reference(node_id, &path) == Some(DefinitionIdentifier::Network("Brush".into()))).then_some((*node_id, path)))
.collect();
for (node_id, network_path) in &brush_layers {
// Pre-load `outward_wires` so the chain-break check inside `set_input` resolves the original upstream→node wire from cache
// rather than triggering a fresh rebuild from the (already-mutated) post-`replace_inputs` state, which would orphan wires.
let _ = document.network_interface.outward_wires(network_path);
let new_reference = DefinitionIdentifier::ProtoNode(graphene_std::brush::brush::brush::IDENTIFIER);
let Some(definition) = resolve_document_node_type(&new_reference) else { continue };
let mut node_template = definition.default_node_template();
document.network_interface.replace_implementation(node_id, network_path, &mut node_template);
let Some(old_inputs) = document.network_interface.replace_inputs(node_id, network_path, &mut node_template) else {
continue;
};
for (index, input) in old_inputs.iter().take(3).enumerate() {
document.network_interface.set_input(&InputConnector::node_at_index(*node_id, index), input.clone(), network_path);
}
}

// The "Transform" wrapper network was replaced with the `transform` proto node directly. Convert old `Network("Transform")` instances to the proto node, forwarding the 5 user-facing inputs (Value, Translation, Rotation, Scale, Skew) and dropping the legacy migration sentinels (Origin Offset, Scale Appearance) at indices 5 and 6 if present.
// Pre-pass for the same reason as the Brush migration above: replacing the outer Transform's network impl orphans its child paths.
let transform_layers: Vec<(NodeId, Vec<NodeId>, usize)> = document
Expand Down Expand Up @@ -2275,36 +2232,6 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
document.network_interface.set_input(&InputConnector::node_at_index(*node_id, 1), old_inputs[1].clone(), network_path);
}

// Old shape: [background, bounds, trace, cache]. Both "bounds" (input 1) and "cache" (input 3) are dropped, and "cache" is now stored as
// internal node state via `#[data]` on the brush node, so it is not a node input at all in the new shape.
if reference == DefinitionIdentifier::ProtoNode(graphene_std::brush::brush::brush::IDENTIFIER) && inputs_count == 4 {
let mut node_template = resolve_document_node_type(&reference)?.default_node_template();
document.network_interface.replace_implementation(node_id, network_path, &mut node_template);

let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template)?;

document.network_interface.set_input(&InputConnector::node_at_index(*node_id, 0), old_inputs[0].clone(), network_path);
document.network_interface.set_input(&InputConnector::node_at_index(*node_id, 1), old_inputs[2].clone(), network_path);
}

// Old shape: [background, trace, cache]. The "cache" input is dropped because the brush node now stores its cache as internal `#[data]` state.
if reference == DefinitionIdentifier::ProtoNode(graphene_std::brush::brush::brush::IDENTIFIER) && inputs_count == 3 {
let mut node_template = resolve_document_node_type(&reference)?.default_node_template();
document.network_interface.replace_implementation(node_id, network_path, &mut node_template);

let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template)?;

document.network_interface.set_input(&InputConnector::node_at_index(*node_id, 0), old_inputs[0].clone(), network_path);
document.network_interface.set_input(&InputConnector::node_at_index(*node_id, 1), old_inputs[1].clone(), network_path);
}

// A brush node saved before `Item<Raster<CPU>>` had a default stored its unconnected background as the invalid `()`,
// which fails type resolution against the raster primary; adopt the definition's empty-raster default instead.
if reference == DefinitionIdentifier::ProtoNode(graphene_std::brush::brush::brush::IDENTIFIER) && matches!(node.inputs.first().and_then(|input| input.as_value()), Some(TaggedValue::None)) {
let default_background = resolve_document_node_type(&reference)?.node_template.inputs.first()?.clone();
document.network_interface.set_input(&InputConnector::node_at_index(*node_id, 0), default_background, network_path);
}

if reference == DefinitionIdentifier::ProtoNode(ProtoNodeIdentifier::new("graphene_core::vector::RemoveHandlesNode")) {
let mut node_template = resolve_document_node_type(&DefinitionIdentifier::ProtoNode(graphene_std::vector::auto_tangents::IDENTIFIER))?.default_node_template();
document.network_interface.replace_implementation(node_id, network_path, &mut node_template);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ impl MessageHandler<PreferencesMessage, PreferencesMessageContext<'_>> for Prefe
zoom_with_scroll: self.zoom_with_scroll,
});
responses.add(FrontendMessage::UpdateUIScale { scale: self.ui_scale });
responses.add(ToolMessage::RefreshToolShelf);

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 loading preferences disables the brush tool while Brush is active, this refresh hides Brush in the shelf but keeps Brush active. Mirror the PreferencesMessage::BrushTool deactivation check in the load path before refreshing the shelf.

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

<comment>When loading preferences disables the brush tool while Brush is active, this refresh hides Brush in the shelf but keeps Brush active. Mirror the `PreferencesMessage::BrushTool` deactivation check in the load path before refreshing the shelf.</comment>

<file context>
@@ -93,6 +93,7 @@ impl MessageHandler<PreferencesMessage, PreferencesMessageContext<'_>> for Prefe
 					zoom_with_scroll: self.zoom_with_scroll,
 				});
 				responses.add(FrontendMessage::UpdateUIScale { scale: self.ui_scale });
+				responses.add(ToolMessage::RefreshToolShelf);
 			}
 			PreferencesMessage::ResetToDefaults => {
</file context>
Suggested change
responses.add(ToolMessage::RefreshToolShelf);
if !self.brush_tool && tool_message_handler.tool_state.tool_data.active_tool_type == ToolType::Brush {
responses.add(ToolMessage::ActivateToolSelect);
}
responses.add(ToolMessage::RefreshToolShelf);

}
PreferencesMessage::ResetToDefaults => {
responses.add(PreferencesMessage::Load { preferences: Self::default() });
Expand Down
Loading
Loading