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
2 changes: 1 addition & 1 deletion demo-artwork/changing-seasons.graphite

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion demo-artwork/isometric-fountain.graphite

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion demo-artwork/painted-dreams.graphite

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion demo-artwork/parametric-dunescape.graphite

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion demo-artwork/procedural-string-lights.graphite

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion demo-artwork/red-dress.graphite

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion demo-artwork/valley-of-spires.graphite

Large diffs are not rendered by default.

17 changes: 7 additions & 10 deletions editor/src/messages/portfolio/document/document_message_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,15 @@ use graph_craft::application_io::wgpu_available;
use graph_craft::descriptor;
use graph_craft::document::value::TaggedValue;
use graph_craft::document::{NodeId, NodeInput, NodeNetwork, OldNodeNetwork};
use graphene_std::graphic::is_paint_present;
use graphene_std::math::quad::Quad;
use graphene_std::path_bool_nodes::boolean_intersect;
use graphene_std::raster::BlendMode;
use graphene_std::subpath::Subpath;
use graphene_std::vector::PointId;
use graphene_std::vector::click_target::{ClickTarget, ClickTargetType};
use graphene_std::vector::misc::dvec2_to_point;
use graphene_std::vector::style::{Fill, RenderMode};
use graphene_std::vector::style::{Gradient, RenderMode};
use kurbo::{Affine, BezPath, Line, PathSeg};
use std::collections::HashSet;
use std::path::PathBuf;
Expand Down Expand Up @@ -125,10 +126,10 @@ pub struct DocumentMessageHandler {
#[serde(skip)]
pub(crate) path: Option<PathBuf>,
// TODO: Eventually remove this document upgrade code
/// Set when a freshly-opened document still has legacy bounding-box-relative gradients; the deferred gradient
/// migration converts them to absolute after the first graph run (when geometry bounds are available) and clears this.
/// Fill nodes whose legacy bounding-box-relative gradient was decomposed into the value model, but whose transform still
/// needs the bounding box baked in. The deferred migration bakes them after the first graph run (when bounds are available) and clears this.
#[serde(skip)]
pub(crate) pending_gradient_migration: bool,
pub(crate) pending_gradient_bbox_bake: Vec<(NodeId, Gradient)>,
/// Path to network currently viewed in the node graph overlay. This will eventually be stored in each panel, so that multiple panels can refer to different networks
#[serde(skip)]
breadcrumb_network_path: Vec<NodeId>,
Expand Down Expand Up @@ -187,7 +188,7 @@ impl Default for DocumentMessageHandler {
name: DEFAULT_DOCUMENT_NAME.to_string(),
path: None,
// TODO: Eventually remove this document upgrade code
pending_gradient_migration: false,
pending_gradient_bbox_bake: Vec::new(),
breadcrumb_network_path: Vec::new(),
selection_network_path: Vec::new(),
document_undo_history: VecDeque::new(),
Expand Down Expand Up @@ -2525,11 +2526,7 @@ impl DocumentMessageHandler {
let fill_graphic_list = self.network_interface.document_metadata().layer_fill_attributes.get(&layer);
let stroke_graphic_list = self.network_interface.document_metadata().layer_stroke_attributes.get(&layer);

let has_fill = if let Some(list) = fill_graphic_list {
list.element(0).is_some()
} else {
!matches!(style.fill, Fill::None)
};
let has_fill = fill_graphic_list.is_some_and(|g| is_paint_present(g));
// `style.stroke` is `Some` whenever a `Stroke` node is in the chain, even with weight 0 or a transparent color.
// So `is_some()` would treat invisibly-stroked fill-only layers as having a stroke.
// `ATTR_STROKE` is the source of truth when set; fall back to `style.stroke.color` only when no attribute is present.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ use graphene_std::raster::BlendMode;
use graphene_std::raster_types::Image;
use graphene_std::subpath::Subpath;
use graphene_std::text::{Font, TypesettingConfig};
use graphene_std::vector::style::{Fill, GradientSpreadMethod, GradientType, Stroke};
use graphene_std::vector::{GradientStops, PointId, Vector, VectorModification, VectorModificationType};
use graphene_std::vector::style::{Fill, GradientSpreadMethod, GradientType, Stroke, build_transform_with_y_preservation};
use graphene_std::vector::{GradientAppearance, GradientStops, PointId, Vector, VectorModification, VectorModificationType};
use graphene_std::{Artboard, Color, Graphic, NodeInputDecleration};

#[derive(PartialEq, Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
Expand Down Expand Up @@ -454,29 +454,59 @@ impl<'a> ModifyInputsContext<'a> {
}

pub fn fill_set(&mut self, fill: Fill) {
let fill_index = 1;
let backup_color_index = 2;
let backup_gradient_index = 3;

let Some(fill_node_id) = self.existing_proto_node_id(graphene_std::vector_nodes::fill::IDENTIFIER, true) else {
return;
};
let input_connector = InputConnector::node(fill_node_id, graphene_std::vector::fill::FillInput::INDEX);

match &fill {
Fill::None => {
let input_connector = InputConnector::node(fill_node_id, backup_color_index);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Color(None), false), true);
let backup_input_connector = InputConnector::node(fill_node_id, graphene_std::vector::fill::BackupColorInput::INDEX);
self.set_input_with_refresh(backup_input_connector, NodeInput::value(TaggedValue::Color(None), false), true);

self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Color(None), false), false);
}
Fill::Solid(color) => {
let input_connector = InputConnector::node(fill_node_id, backup_color_index);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Color(Some(*color)), false), true);
let backup_input_connector = InputConnector::node(fill_node_id, graphene_std::vector::fill::BackupColorInput::INDEX);
self.set_input_with_refresh(backup_input_connector, NodeInput::value(TaggedValue::Color(Some(*color)), false), true);

self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Color(Some(*color)), false), false);
}
Fill::Gradient(gradient) => {
let input_connector = InputConnector::node(fill_node_id, backup_gradient_index);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::FillGradient(gradient.clone()), false), true);
let backup_input_connector = InputConnector::node(fill_node_id, graphene_std::vector::fill::BackupGradientInput::INDEX);
self.set_input_with_refresh(backup_input_connector, NodeInput::value(TaggedValue::Gradient(gradient.stops.clone()), false), true);

self.set_input_with_refresh(
InputConnector::node(fill_node_id, graphene_std::vector::fill::FillInput::INDEX),
NodeInput::value(TaggedValue::Gradient(gradient.stops.clone()), false),
false,
);

let old_transform: DAffine2 = self
.network_interface
.document_network()
.nodes
.get(&fill_node_id)
.and_then(|node| node.inputs.get(graphene_std::vector::fill::GradientAppearanceInput::INDEX))
.and_then(|input| input.as_value())
.and_then(|value| if let TaggedValue::GradientAppearance(appearance) = value { appearance.transform } else { None })
.unwrap_or(DAffine2::IDENTITY);

let new_transform = build_transform_with_y_preservation(old_transform, gradient.start, gradient.end);
self.set_input_with_refresh(
InputConnector::node(fill_node_id, graphene_std::vector::fill::GradientAppearanceInput::INDEX),
NodeInput::value(
TaggedValue::GradientAppearance(GradientAppearance {
transform: Some(new_transform),
gradient_type: gradient.gradient_type,
spread_method: gradient.spread_method,
}),
false,
),
false,
);
}
}
let input_connector = InputConnector::node(fill_node_id, fill_index);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Fill(fill), false), false);
}

pub fn blend_mode_set(&mut self, blend_mode: BlendMode) {
Expand Down Expand Up @@ -536,8 +566,20 @@ impl<'a> ModifyInputsContext<'a> {
/// Write the gradient stops to the 'Gradient Value' node feeding the layer.
pub fn gradient_stops_set(&mut self, stops: GradientStops) {
let Some(output_layer) = self.get_output_layer() else { return };
let Some(gradient_value_id) = get_upstream_gradient_value_node_id(output_layer, self.network_interface) else {
return;

let gradient_value_id = match get_upstream_gradient_value_node_id(output_layer, self.network_interface) {
Some(id) => id,
None => {
let target = gradient_chain_target_input(output_layer, self.network_interface);
let Some(node_definition) = resolve_proto_node_type(graphene_std::math_nodes::gradient_value::IDENTIFIER) else {
return;
};
let node_id = NodeId::new();
self.network_interface.insert_node(node_id, node_definition.default_node_template(), &[]);
self.network_interface.set_input(&target, NodeInput::node(node_id, 0), &[]);

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.

P1: Creating a missing gradient value node uses set_input which replaces the existing upstream connection instead of splicing into it, potentially disconnecting existing gradient transform/type/spread nodes.

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/graph_operation/utility_types.rs, line 584:

<comment>Creating a missing gradient value node uses `set_input` which replaces the existing upstream connection instead of splicing into it, potentially disconnecting existing gradient transform/type/spread nodes.</comment>

<file context>
@@ -536,8 +571,20 @@ impl<'a> ModifyInputsContext<'a> {
+				};
+				let node_id = NodeId::new();
+				self.network_interface.insert_node(node_id, node_definition.default_node_template(), &[]);
+				self.network_interface.set_input(&target, NodeInput::node(node_id, 0), &[]);
+
+				node_id
</file context>
Suggested change
self.network_interface.set_input(&target, NodeInput::node(node_id, 0), &[]);
self.network_interface.insert_node_before_input(&node_id, &target, &[]);


node_id
}
};

let input_connector = InputConnector::node(gradient_value_id, graphene_std::math_nodes::gradient_value::GradientInput::INDEX);
Expand Down Expand Up @@ -615,6 +657,7 @@ impl<'a> ModifyInputsContext<'a> {
/// from the default (`Linear`).
pub fn gradient_type_set(&mut self, gradient_type: GradientType) {
let Some(output_layer) = self.get_output_layer() else { return };

let target_input = gradient_chain_target_input(output_layer, self.network_interface);
let identifier = graphene_std::math_nodes::gradient_type::IDENTIFIER;
let create_if_nonexistent = gradient_type != GradientType::default();
Expand All @@ -630,6 +673,7 @@ impl<'a> ModifyInputsContext<'a> {
/// from the default (`Pad`).
pub fn gradient_spread_method_set(&mut self, spread_method: GradientSpreadMethod) {
let Some(output_layer) = self.get_output_layer() else { return };

let target_input = gradient_chain_target_input(output_layer, self.network_interface);
let identifier = graphene_std::math_nodes::spread_method::IDENTIFIER;
let create_if_nonexistent = spread_method != GradientSpreadMethod::default();
Expand All @@ -655,7 +699,7 @@ impl<'a> ModifyInputsContext<'a> {
return;
};

let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::ColorInput::INDEX);
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::PaintInput::INDEX);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Color(stroke.color), false), true);
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::WeightInput::INDEX);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(stroke.weight), false), true);
Expand Down Expand Up @@ -804,40 +848,3 @@ impl<'a> ModifyInputsContext<'a> {
}
}
}

/// Rebuild the y-axis so its (parallel, perpendicular) components in the x-axis-aligned frame stay constant, both
/// rescaled by `|new_x| / |old_x|`. This holds the (x, y) parallelogram's aspect ratio and skew fixed across an endpoint
/// drag, so a radial ellipse stays the same shape (just rotated and resized) instead of distorting as x grows or shrinks.
/// Falls back to a +90° rotation of `new_x` when `old_x` is degenerate.
fn scale_y_axis_to_match_new_x(old_x: DVec2, old_y: DVec2, new_x: DVec2) -> DVec2 {
let old_x_length = old_x.length();
if old_x_length < 1e-9 {
return DVec2::new(-new_x.y, new_x.x);
}
let ex_old = old_x / old_x_length;
let ey_old = DVec2::new(-ex_old.y, ex_old.x);

let new_x_length = new_x.length();
if new_x_length < 1e-9 {
return DVec2::ZERO;
}
let ex_new = new_x / new_x_length;
let ey_new = DVec2::new(-ex_new.y, ex_new.x);

let parallel = old_y.dot(ex_old);
let perpendicular = old_y.dot(ey_old);
let scale = new_x_length / old_x_length;

scale * (parallel * ex_new + perpendicular * ey_new)
}

/// Build a new affine that maps canonical (0,0) -> (1,0) to (new_start, new_end), preserving the y-axis
/// shape of `old` proportionally to the x-axis length change.
fn build_transform_with_y_preservation(old: DAffine2, new_start: DVec2, new_end: DVec2) -> DAffine2 {
let new_x_axis = new_end - new_start;
let preserved_y_axis = scale_y_axis_to_match_new_x(old.matrix2.x_axis, old.matrix2.y_axis, new_x_axis);
DAffine2 {
matrix2: glam::DMat2::from_cols(new_x_axis, preserved_y_axis),
translation: new_start,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1764,7 +1764,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
NodeGraphMessage::SetInputValue { node_id, input_index, value } => {
use graphene_std::vector::generator_nodes::*;

let is_fill = matches!(value, TaggedValue::Fill(_));
let is_fill = matches!(value, TaggedValue::Gradient(_) | TaggedValue::Color(_));
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));
Expand Down
Loading
Loading