mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-24 06:18:13 +08:00
Let the Fill and Gradient tools paint blank layers with whole-expanse colors and gradients (#4407)
* Let the Gradient and Fill tools paint blank layers with whole-expanse gradients and colors * Make it work on layers with un-fed Transform nodes * Show the Fill tool's hover pattern over the whole expanse of layers painted by a color chain * Let the Fill and Gradient tools replace one another's whole-expanse paint on a selected layer * Keep a shared paint node alive when a tool takes over a layer, and share the paint value lookup * Stop a chain node from being shifted twice when a node is inserted in front of it
This commit is contained in:
@@ -20,6 +20,10 @@ pub enum GraphOperationMessage {
|
|||||||
layer: LayerNodeIdentifier,
|
layer: LayerNodeIdentifier,
|
||||||
color: Option<Color>,
|
color: Option<Color>,
|
||||||
},
|
},
|
||||||
|
ColorValueSet {
|
||||||
|
layer: LayerNodeIdentifier,
|
||||||
|
color: Color,
|
||||||
|
},
|
||||||
FillGradientSet {
|
FillGradientSet {
|
||||||
layer: LayerNodeIdentifier,
|
layer: LayerNodeIdentifier,
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
|
|||||||
+6
-1
@@ -40,6 +40,11 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
|
|||||||
modify_inputs.fill_color_set(color);
|
modify_inputs.fill_color_set(color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
GraphOperationMessage::ColorValueSet { layer, color } => {
|
||||||
|
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
|
||||||
|
modify_inputs.color_value_set(color);
|
||||||
|
}
|
||||||
|
}
|
||||||
GraphOperationMessage::FillGradientSet {
|
GraphOperationMessage::FillGradientSet {
|
||||||
layer,
|
layer,
|
||||||
gradient,
|
gradient,
|
||||||
@@ -324,7 +329,7 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
|
|||||||
GraphOperationMessage::NewColorFillLayer { node_id, color, parent, insert_index } => {
|
GraphOperationMessage::NewColorFillLayer { node_id, color, parent, insert_index } => {
|
||||||
let mut modify_inputs = ModifyInputsContext::new(network_interface, responses);
|
let mut modify_inputs = ModifyInputsContext::new(network_interface, responses);
|
||||||
let layer = modify_inputs.create_layer(node_id);
|
let layer = modify_inputs.create_layer(node_id);
|
||||||
modify_inputs.insert_color_value(color, layer);
|
modify_inputs.insert_color_value(color, layer, InputConnector::layer_secondary_input(layer.to_node()));
|
||||||
network_interface.move_layer_to_stack(layer, parent, insert_index, &[]);
|
network_interface.move_layer_to_stack(layer, parent, insert_index, &[]);
|
||||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ 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::document_metadata::LayerNodeIdentifier;
|
||||||
use crate::messages::portfolio::document::utility_types::network_interface::{self, FlowType, InputConnector, NodeNetworkInterface};
|
use crate::messages::portfolio::document::utility_types::network_interface::{self, FlowType, InputConnector, NodeNetworkInterface};
|
||||||
use crate::messages::prelude::*;
|
use crate::messages::prelude::*;
|
||||||
use crate::messages::tool::common_functionality::graph_modification_utils::{get_fill_input_node_id, get_upstream_gradient_value_node_id, gradient_chain_target_input};
|
use crate::messages::tool::common_functionality::graph_modification_utils::{
|
||||||
|
ReplaceablePaintChain, get_fill_input_node_id, get_upstream_gradient_value_node_id, gradient_chain_target_input, replaceable_paint_chain,
|
||||||
|
};
|
||||||
use glam::{DAffine2, DVec2, IVec2};
|
use glam::{DAffine2, DVec2, IVec2};
|
||||||
use graph_craft::application_io::resource::ResourceId;
|
use graph_craft::application_io::resource::ResourceId;
|
||||||
use graph_craft::document::value::TaggedValue;
|
use graph_craft::document::value::TaggedValue;
|
||||||
@@ -233,14 +235,41 @@ impl<'a> ModifyInputsContext<'a> {
|
|||||||
self.network_interface.move_node_to_chain_start(&fill_id, layer, &[], self.import);
|
self.network_interface.move_node_to_chain_start(&fill_id, layer, &[], self.import);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn insert_color_value(&mut self, color: Color, layer: LayerNodeIdentifier) {
|
pub fn insert_color_value(&mut self, color: Color, layer: LayerNodeIdentifier, attachment_input: InputConnector) -> NodeId {
|
||||||
let color_value = resolve_proto_node_type(graphene_std::math_nodes::color_value::IDENTIFIER)
|
let color_value = resolve_proto_node_type(graphene_std::math_nodes::color_value::IDENTIFIER)
|
||||||
.expect("Color Value node does not exist")
|
.expect("Color Value node does not exist")
|
||||||
.node_template_input_override([Some(NodeInput::value(TaggedValue::None, false)), Some(NodeInput::value(TaggedValue::Color(color), false))]);
|
.node_template_input_override([Some(NodeInput::value(TaggedValue::None, false)), Some(NodeInput::value(TaggedValue::Color(color), false))]);
|
||||||
|
|
||||||
let color_value_id = NodeId::new();
|
let color_value_id = NodeId::new();
|
||||||
self.network_interface.insert_node(color_value_id, color_value, &[]);
|
self.network_interface.insert_node(color_value_id, color_value, &[]);
|
||||||
self.network_interface.move_node_to_chain_start(&color_value_id, layer, &[], self.import);
|
self.start_paint_chain(&color_value_id, layer, attachment_input);
|
||||||
|
|
||||||
|
color_value_id
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear the whole-expanse paint one tool left on a layer so the other can start its own chain there.
|
||||||
|
/// Severing at the attachment detaches the layer from whatever the walk stopped at, which is the only part a node
|
||||||
|
/// the rest of the graph also draws from is subjected to, since such a node is never among those deleted.
|
||||||
|
fn clear_paint_chain(&mut self, paint_chain: &ReplaceablePaintChain) {
|
||||||
|
self.network_interface.disconnect_input(&paint_chain.attachment_input, &[]);
|
||||||
|
|
||||||
|
if !paint_chain.nodes.is_empty() {
|
||||||
|
self.network_interface.delete_nodes(paint_chain.nodes.clone(), false, &[]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wire a node that paints the layer's whole expanse into the start of its chain,
|
||||||
|
/// or past the 'Transform' nodes a blank layer already carries so those go on applying to the paint.
|
||||||
|
fn start_paint_chain(&mut self, node_id: &NodeId, layer: LayerNodeIdentifier, attachment_input: InputConnector) {
|
||||||
|
let layer_content_input = InputConnector::layer_secondary_input(layer.to_node());
|
||||||
|
|
||||||
|
if attachment_input == layer_content_input {
|
||||||
|
self.network_interface.move_node_to_chain_start(node_id, layer, &[], self.import);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.network_interface.set_input(&attachment_input, NodeInput::node(*node_id, 0), &[]);
|
||||||
|
self.network_interface.set_chain_position(node_id, &[]);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn insert_image_data(&mut self, image: Image<Color>, layer: LayerNodeIdentifier) {
|
pub fn insert_image_data(&mut self, image: Image<Color>, layer: LayerNodeIdentifier) {
|
||||||
@@ -503,6 +532,29 @@ impl<'a> ModifyInputsContext<'a> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Update the chain's 'Color Value' node, or start a chain with one on an empty layer, painting the layer's whole expanse.
|
||||||
|
pub fn color_value_set(&mut self, color: Color) {
|
||||||
|
let Some(output_layer) = self.get_output_layer() else { return };
|
||||||
|
|
||||||
|
let target_input = gradient_chain_target_input(output_layer, self.network_interface);
|
||||||
|
if let Some(node_id) = self.existing_proto_node_id_at(&target_input, graphene_std::math_nodes::color_value::IDENTIFIER, false) {
|
||||||
|
let input_connector = InputConnector::node(node_id, graphene_std::math_nodes::color_value::ColorInput);
|
||||||
|
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Color(color), false), false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The 'Color Value' node discards its primary input, so only a blank 'Merge' layer may start a chain with one,
|
||||||
|
// which any whole-expanse paint the other tool left behind is cleared off to become
|
||||||
|
let Some(paint_chain) = replaceable_paint_chain(output_layer, self.network_interface) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
self.clear_paint_chain(&paint_chain);
|
||||||
|
|
||||||
|
let color_value_id = self.insert_color_value(color, output_layer, paint_chain.attachment_input);
|
||||||
|
let input_connector = InputConnector::node(color_value_id, graphene_std::math_nodes::color_value::ColorInput);
|
||||||
|
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Color(color), false), false);
|
||||||
|
}
|
||||||
|
|
||||||
/// Write the gradient stops to the 'Gradient Value' node feeding the layer.
|
/// Write the gradient stops to the 'Gradient Value' node feeding the layer.
|
||||||
pub fn gradient_stops_set(&mut self, stops: Gradient) {
|
pub fn gradient_stops_set(&mut self, stops: Gradient) {
|
||||||
let Some(output_layer) = self.get_output_layer() else { return };
|
let Some(output_layer) = self.get_output_layer() else { return };
|
||||||
@@ -513,11 +565,19 @@ impl<'a> ModifyInputsContext<'a> {
|
|||||||
let target = gradient_chain_target_input(output_layer, self.network_interface);
|
let target = gradient_chain_target_input(output_layer, self.network_interface);
|
||||||
let starts_layer_chain = target == InputConnector::layer_secondary_input(output_layer.to_node());
|
let starts_layer_chain = target == InputConnector::layer_secondary_input(output_layer.to_node());
|
||||||
|
|
||||||
// The Gradient Value node discards its primary input, so starting a chain ahead of existing layer content would drop that content; refuse instead
|
// The 'Gradient Value' node discards its primary input, so only a blank 'Merge' layer may start a chain
|
||||||
if starts_layer_chain && self.network_interface.upstream_output_connector(&target, &[]).is_some() {
|
// with one, which any whole-expanse paint the other tool left behind is cleared off to become
|
||||||
log::error!("Refusing to start a gradient chain ahead of existing layer content");
|
let paint_chain = if starts_layer_chain {
|
||||||
return;
|
let Some(paint_chain) = replaceable_paint_chain(output_layer, self.network_interface) else {
|
||||||
}
|
log::error!("Refusing to start a gradient chain on anything but a blank 'Merge' layer");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
self.clear_paint_chain(&paint_chain);
|
||||||
|
|
||||||
|
Some(paint_chain)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
let Some(node_definition) = resolve_proto_node_type(graphene_std::math_nodes::gradient_value::IDENTIFIER) else {
|
let Some(node_definition) = resolve_proto_node_type(graphene_std::math_nodes::gradient_value::IDENTIFIER) else {
|
||||||
return;
|
return;
|
||||||
@@ -525,9 +585,9 @@ impl<'a> ModifyInputsContext<'a> {
|
|||||||
let node_id = NodeId::new();
|
let node_id = NodeId::new();
|
||||||
self.network_interface.insert_node(node_id, node_definition.default_node_template(), &[]);
|
self.network_interface.insert_node(node_id, node_definition.default_node_template(), &[]);
|
||||||
|
|
||||||
if starts_layer_chain {
|
if let Some(paint_chain) = paint_chain {
|
||||||
// No Fill node: the new node starts the layer's chain
|
// No Fill node: the new node starts the layer's chain
|
||||||
self.network_interface.move_node_to_chain_start(&node_id, output_layer, &[], self.import);
|
self.start_paint_chain(&node_id, output_layer, paint_chain.attachment_input);
|
||||||
} else {
|
} else {
|
||||||
// Feeding a Fill node's paint input: wire it up and place it one chain-width left and a step below the Fill
|
// Feeding a Fill node's paint input: wire it up and place it one chain-width left and a step below the Fill
|
||||||
self.network_interface.set_input(&target, NodeInput::node(node_id, 0), &[]);
|
self.network_interface.set_input(&target, NodeInput::node(node_id, 0), &[]);
|
||||||
|
|||||||
@@ -980,6 +980,13 @@ impl NodeNetworkInterface {
|
|||||||
let Some(feeder_position) = self.position(&feeder, network_path) else { return };
|
let Some(feeder_position) = self.position(&feeder, network_path) else { return };
|
||||||
|
|
||||||
self.shift_node(node_id, feeder_position - node_position, network_path);
|
self.shift_node(node_id, feeder_position - node_position, network_path);
|
||||||
|
|
||||||
|
// A chain feeder derives its position from its distance to the layer, which this insertion already grew,
|
||||||
|
// so shifting it would move it twice and cost it its place in the chain
|
||||||
|
if !self.is_absolute(&feeder, network_path) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Deduplicate, since `UpstreamFlow` can yield a shared node more than once and we must shift each node only once.
|
// Deduplicate, since `UpstreamFlow` can yield a shared node more than once and we must shift each node only once.
|
||||||
let upstream_nodes: HashSet<NodeId> = self.upstream_flow_back_from_nodes(vec![feeder], network_path, FlowType::UpstreamFlow).collect();
|
let upstream_nodes: HashSet<NodeId> = self.upstream_flow_back_from_nodes(vec![feeder], network_path, FlowType::UpstreamFlow).collect();
|
||||||
for upstream_node in &upstream_nodes {
|
for upstream_node in &upstream_nodes {
|
||||||
|
|||||||
@@ -897,6 +897,11 @@ impl NodeNetworkInterface {
|
|||||||
self.query(network_path, "is_artboard", |view| Ok(view.is_artboard(node_id))).unwrap_or_default()
|
self.query(network_path, "is_artboard", |view| Ok(view.is_artboard(node_id))).unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether the node is a Merge node by identity, meaning it is the generic layer wrapper rather than a specialized node displayed as a layer.
|
||||||
|
pub fn is_merge(&self, node_id: &NodeId, network_path: &[NodeId]) -> bool {
|
||||||
|
self.query(network_path, "is_merge", |view| Ok(view.is_merge(node_id))).unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
/// All artboard layers that participate in the scene, excluding disconnected Artboard nodes.
|
/// All artboard layers that participate in the scene, excluding disconnected Artboard nodes.
|
||||||
pub fn all_artboards(&self) -> HashSet<LayerNodeIdentifier> {
|
pub fn all_artboards(&self) -> HashSet<LayerNodeIdentifier> {
|
||||||
// O(n * (nodes + wires)) since connected_to_output performs a graph walk per artboard candidate
|
// O(n * (nodes + wires)) since connected_to_output performs a graph walk per artboard candidate
|
||||||
|
|||||||
@@ -123,6 +123,14 @@ impl<'a, 'p> NetworkView<'a, 'p> {
|
|||||||
.is_some_and(|reference| reference == DefinitionIdentifier::Network("Artboard".into()))
|
.is_some_and(|reference| reference == DefinitionIdentifier::Network("Artboard".into()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether the node is a Merge node by identity, regardless of whether it currently participates in the scene.
|
||||||
|
pub fn is_merge(&self, node_id: &NodeId) -> bool {
|
||||||
|
self.reference(node_id)
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
.is_some_and(|reference| reference == DefinitionIdentifier::Network("Merge".into()))
|
||||||
|
}
|
||||||
|
|
||||||
/// The uneditable name in the Properties panel which represents the function name of the node implementation.
|
/// The uneditable name in the Properties panel which represents the function name of the node implementation.
|
||||||
pub fn implementation_name(&self, node_id: &NodeId) -> String {
|
pub fn implementation_name(&self, node_id: &NodeId) -> String {
|
||||||
self.reference(node_id)
|
self.reference(node_id)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
|
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
|
||||||
use crate::messages::portfolio::document::node_graph::document_node_definitions::{self, DefinitionIdentifier};
|
use crate::messages::portfolio::document::node_graph::document_node_definitions::{self, DefinitionIdentifier};
|
||||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||||
use crate::messages::portfolio::document::utility_types::network_interface::{FlowType, InputConnector, NodeNetworkInterface, NodeTemplate};
|
use crate::messages::portfolio::document::utility_types::network_interface::{FlowType, InputConnector, NodeNetworkInterface, NodeTemplate, OutputConnector};
|
||||||
use crate::messages::prelude::*;
|
use crate::messages::prelude::*;
|
||||||
use glam::{DAffine2, DVec2};
|
use glam::{DAffine2, DVec2};
|
||||||
use graph_craft::document::value::TaggedValue;
|
use graph_craft::document::value::TaggedValue;
|
||||||
@@ -285,15 +285,97 @@ pub fn gradient_chain_target_input(layer: LayerNodeIdentifier, network_interface
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Try to find a "Gradient Value" node that is connected to a "Fill" node, or to a layer directly.
|
/// Try to find the paint value node feeding a 'Fill' node, or a layer directly.
|
||||||
pub fn get_upstream_gradient_value_node_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<NodeId> {
|
fn get_upstream_paint_value_node_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface, identifier: ProtoNodeIdentifier) -> Option<NodeId> {
|
||||||
let target_input = gradient_chain_target_input(layer, network_interface);
|
let target_input = gradient_chain_target_input(layer, network_interface);
|
||||||
let walk_from = network_interface.upstream_output_connector(&target_input, &[])?.node_id()?;
|
let walk_from = network_interface.upstream_output_connector(&target_input, &[])?.node_id()?;
|
||||||
|
let reference = DefinitionIdentifier::ProtoNode(identifier);
|
||||||
|
|
||||||
network_interface
|
network_interface
|
||||||
.upstream_flow_back_from_nodes(vec![walk_from], &[], FlowType::HorizontalFlow)
|
.upstream_flow_back_from_nodes(vec![walk_from], &[], FlowType::HorizontalFlow)
|
||||||
.take_while(|node_id| !network_interface.is_layer(node_id, &[]))
|
.take_while(|node_id| !network_interface.is_layer(node_id, &[]))
|
||||||
.find(|node_id| network_interface.reference(node_id, &[]).as_ref() == Some(&DefinitionIdentifier::ProtoNode(graphene_std::math_nodes::gradient_value::IDENTIFIER)))
|
.find(|node_id| network_interface.reference(node_id, &[]).as_ref() == Some(&reference))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Try to find a 'Gradient Value' node that is connected to a 'Fill' node, or to a layer directly.
|
||||||
|
pub fn get_upstream_gradient_value_node_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<NodeId> {
|
||||||
|
get_upstream_paint_value_node_id(layer, network_interface, graphene_std::math_nodes::gradient_value::IDENTIFIER)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A whole-expanse paint to clear off a 'Merge' layer so a paint tool can start its own chain there.
|
||||||
|
pub struct ReplaceablePaintChain {
|
||||||
|
/// Where the fresh paint attaches, which is also the link severed to reach it.
|
||||||
|
pub attachment_input: InputConnector,
|
||||||
|
/// The paint's nodes, ordered downstream to upstream, empty when there is nothing to clear away.
|
||||||
|
pub nodes: Vec<NodeId>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The whole-expanse paint on a 'Merge' layer that a paint tool may take over, or `None`
|
||||||
|
/// when the layer isn't one or its chain holds anything the tools didn't put there.
|
||||||
|
/// The walk stops at the first node the rest of the graph also draws from, since repainting through it would change
|
||||||
|
/// what those other consumers see, and it passes through 'Transform' nodes so a swap keeps the layer's placement.
|
||||||
|
pub fn replaceable_paint_chain(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<ReplaceablePaintChain> {
|
||||||
|
if !network_interface.is_merge(&layer.to_node(), &[]) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A generator discards its primary input, so the chain ends at whichever one is reached
|
||||||
|
let generators = [graphene_std::math_nodes::color_value::IDENTIFIER, graphene_std::math_nodes::gradient_value::IDENTIFIER];
|
||||||
|
let setters = [
|
||||||
|
graphene_std::math_nodes::gradient_form::IDENTIFIER,
|
||||||
|
graphene_std::math_nodes::gradient_spread::IDENTIFIER,
|
||||||
|
graphene_std::math_nodes::gradient_positions::IDENTIFIER,
|
||||||
|
graphene_std::math_nodes::gradient_midpoints::IDENTIFIER,
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut nodes = Vec::new();
|
||||||
|
let mut input = InputConnector::layer_secondary_input(layer.to_node());
|
||||||
|
// Trails the walk over the 'Transform' nodes that survive, then holds still once the paint to clear away begins
|
||||||
|
let mut attachment_input = input;
|
||||||
|
|
||||||
|
while let Some(upstream) = network_interface.upstream_output_connector(&input, &[]) {
|
||||||
|
let node_id = upstream.node_id()?;
|
||||||
|
|
||||||
|
// Arriving here means this layer consumes the node, so a further consumer makes it shared
|
||||||
|
let shared = network_interface
|
||||||
|
.with_outward_wires(&[], |outward_wires| outward_wires.get(&OutputConnector::primary_output(node_id)).is_some_and(|inputs| inputs.len() > 1))
|
||||||
|
.unwrap_or(true);
|
||||||
|
if shared {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(DefinitionIdentifier::ProtoNode(identifier)) = network_interface.reference(&node_id, &[]) else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
if identifier == graphene_std::transform_nodes::transform::IDENTIFIER {
|
||||||
|
input = InputConnector::primary_input(node_id);
|
||||||
|
if nodes.is_empty() {
|
||||||
|
attachment_input = input;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let generator = generators.contains(&identifier);
|
||||||
|
if !generator && !setters.contains(&identifier) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
nodes.push(node_id);
|
||||||
|
|
||||||
|
if generator {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
input = InputConnector::primary_input(node_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(ReplaceablePaintChain { attachment_input, nodes })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Try to find a 'Color Value' node that is connected to a 'Fill' node, or to a layer directly.
|
||||||
|
pub fn get_upstream_color_value_node_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<NodeId> {
|
||||||
|
get_upstream_paint_value_node_id(layer, network_interface, graphene_std::math_nodes::color_value::IDENTIFIER)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the node connected to Fill's fill input, if any.
|
/// Get the node connected to Fill's fill input, if any.
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
use super::tool_prelude::*;
|
use super::tool_prelude::*;
|
||||||
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
|
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
|
||||||
|
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||||
|
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeNetworkInterface};
|
||||||
use crate::messages::tool::common_functionality::color_selector::solid;
|
use crate::messages::tool::common_functionality::color_selector::solid;
|
||||||
use crate::messages::tool::common_functionality::graph_modification_utils::NodeGraphLayer;
|
use crate::messages::tool::common_functionality::graph_modification_utils::{NodeGraphLayer, get_upstream_color_value_node_id, gradient_chain_target_input, replaceable_paint_chain};
|
||||||
use graphene_std::color::SRGBA8;
|
use graphene_std::color::SRGBA8;
|
||||||
use graphene_std::raster::color::Color;
|
use graphene_std::raster::color::Color;
|
||||||
|
use graphene_std::subpath::Subpath;
|
||||||
|
use graphene_std::vector::PointId;
|
||||||
use graphene_std::vector::style::FillChoice;
|
use graphene_std::vector::style::FillChoice;
|
||||||
|
|
||||||
#[derive(Default, ExtractField)]
|
#[derive(Default, ExtractField)]
|
||||||
@@ -140,10 +144,16 @@ impl Fsm for FillToolFsmState {
|
|||||||
let use_secondary = input.keyboard.get(Key::Shift as usize);
|
let use_secondary = input.keyboard.get(Key::Shift as usize);
|
||||||
let preview_color = if use_secondary { global_tool_data.secondary_color } else { global_tool_data.primary_color };
|
let preview_color = if use_secondary { global_tool_data.secondary_color } else { global_tool_data.primary_color };
|
||||||
|
|
||||||
// Get the layer the user is hovering over
|
// Pattern the layer the fill would land on, over its whole expanse when the color is its entire content
|
||||||
if let Some(layer) = document.click(input, viewport) {
|
if let Some(layer) = fill_target_layer(document, input, viewport) {
|
||||||
let color_hex = SRGBA8::from(preview_color).to_css_hex();
|
let color_hex = SRGBA8::from(preview_color).to_css_hex();
|
||||||
overlay_context.fill_path_pattern(document.metadata().layer_outline(layer), document.metadata().transform_to_viewport(layer), &color_hex);
|
|
||||||
|
if paints_whole_expanse(layer, &document.network_interface) {
|
||||||
|
let expanse = whole_expanse_rect(layer, document, overlay_context.viewport.size().into_dvec2());
|
||||||
|
overlay_context.fill_path_pattern(std::iter::once(expanse), DAffine2::IDENTITY, &color_hex);
|
||||||
|
} else {
|
||||||
|
overlay_context.fill_path_pattern(document.metadata().layer_outline(layer), document.metadata().transform_to_viewport(layer), &color_hex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self
|
self
|
||||||
@@ -154,9 +164,11 @@ impl Fsm for FillToolFsmState {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
(FillToolFsmState::Ready, color_event) => {
|
(FillToolFsmState::Ready, color_event) => {
|
||||||
let Some(layer_identifier) = document.click(input, viewport) else {
|
let Some(layer_identifier) = fill_target_layer(document, input, viewport) else { return self };
|
||||||
return self;
|
|
||||||
};
|
// A whole-expanse color chain (existing, or newly started on a blank layer) routes to its 'Color Value' node; geometry gets its Fill set
|
||||||
|
let route_to_color_chain = routes_to_color_chain(layer_identifier, &document.network_interface);
|
||||||
|
|
||||||
// If the layer is a raster layer, don't fill it, wait till the flood fill tool is implemented
|
// If the layer is a raster layer, don't fill it, wait till the flood fill tool is implemented
|
||||||
if NodeGraphLayer::is_raster_layer(layer_identifier, &mut document.network_interface) {
|
if NodeGraphLayer::is_raster_layer(layer_identifier, &mut document.network_interface) {
|
||||||
return self;
|
return self;
|
||||||
@@ -168,10 +180,14 @@ impl Fsm for FillToolFsmState {
|
|||||||
};
|
};
|
||||||
|
|
||||||
responses.add(DocumentMessage::AddTransaction);
|
responses.add(DocumentMessage::AddTransaction);
|
||||||
responses.add(GraphOperationMessage::FillColorSet {
|
if route_to_color_chain {
|
||||||
layer: layer_identifier,
|
responses.add(GraphOperationMessage::ColorValueSet { layer: layer_identifier, color });
|
||||||
color: Some(color),
|
} else {
|
||||||
});
|
responses.add(GraphOperationMessage::FillColorSet {
|
||||||
|
layer: layer_identifier,
|
||||||
|
color: Some(color),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
FillToolFsmState::Filling
|
FillToolFsmState::Filling
|
||||||
}
|
}
|
||||||
@@ -202,6 +218,43 @@ impl Fsm for FillToolFsmState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether the fill lands on the layer's 'Color Value' node rather than on geometry's Fill, which includes taking over
|
||||||
|
/// a layer the Gradient tool painted, since a whole-expanse gradient gives way to a solid color.
|
||||||
|
fn routes_to_color_chain(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> bool {
|
||||||
|
get_upstream_color_value_node_id(layer, network_interface).is_some() || replaceable_paint_chain(layer, network_interface).is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The layer the fill acts on: the one under the cursor, or else a selected layer painted through a color chain,
|
||||||
|
/// since blank layers and whole-expanse colors render no clickable geometry.
|
||||||
|
fn fill_target_layer(document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, viewport: &ViewportMessageHandler) -> Option<LayerNodeIdentifier> {
|
||||||
|
document.click(input, viewport).or_else(|| {
|
||||||
|
document
|
||||||
|
.network_interface
|
||||||
|
.selected_nodes()
|
||||||
|
.selected_visible_layers(&document.network_interface)
|
||||||
|
.find(|&layer| routes_to_color_chain(layer, &document.network_interface))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the color is the layer's whole content rather than paint applied to its geometry, meaning there is no
|
||||||
|
/// outline to pattern and the preview covers the layer's whole expanse instead.
|
||||||
|
fn paints_whole_expanse(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> bool {
|
||||||
|
gradient_chain_target_input(layer, network_interface) == InputConnector::layer_secondary_input(layer.to_node()) && routes_to_color_chain(layer, network_interface)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The viewport-space area a whole-expanse color paints: the artboard containing the layer, since the color fills it,
|
||||||
|
/// or else the visible viewport for a layer living outside any artboard.
|
||||||
|
fn whole_expanse_rect(layer: LayerNodeIdentifier, document: &DocumentMessageHandler, viewport_size: DVec2) -> Subpath<PointId> {
|
||||||
|
let containing_artboard = layer
|
||||||
|
.ancestors(document.metadata())
|
||||||
|
.find(|&ancestor| ancestor != LayerNodeIdentifier::ROOT_PARENT && document.network_interface.is_artboard(&ancestor.to_node(), &[]));
|
||||||
|
|
||||||
|
match containing_artboard.and_then(|artboard| document.metadata().bounding_box_viewport(artboard)) {
|
||||||
|
Some([min, max]) => Subpath::new_rectangle(min, max),
|
||||||
|
None => Subpath::new_rectangle(DVec2::ZERO, viewport_size),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test_fill {
|
mod test_fill {
|
||||||
pub use crate::test_utils::test_prelude::*;
|
pub use crate::test_utils::test_prelude::*;
|
||||||
@@ -262,4 +315,302 @@ mod test_fill {
|
|||||||
assert_eq!(fills.len(), 1);
|
assert_eq!(fills.len(), 1);
|
||||||
assert_eq!(SRGBA8::from(fills[0]), SRGBA8::from(Color::YELLOW));
|
assert_eq!(SRGBA8::from(fills[0]), SRGBA8::from(Color::YELLOW));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn blank_layer_gets_whole_expanse_color() {
|
||||||
|
use crate::messages::tool::common_functionality::graph_modification_utils::get_upstream_color_value_node_id;
|
||||||
|
use graph_craft::document::NodeId;
|
||||||
|
use graph_craft::document::value::TaggedValue;
|
||||||
|
|
||||||
|
let mut editor = EditorTestUtils::create();
|
||||||
|
editor.new_document().await;
|
||||||
|
editor
|
||||||
|
.handle_message(GraphOperationMessage::NewCustomLayer {
|
||||||
|
id: NodeId::new(),
|
||||||
|
nodes: Vec::new(),
|
||||||
|
parent: LayerNodeIdentifier::ROOT_PARENT,
|
||||||
|
insert_index: 0,
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
let layer = editor.active_document().metadata().all_layers().next().unwrap();
|
||||||
|
editor.handle_message(NodeGraphMessage::SelectedNodesSet { nodes: vec![layer.to_node()] }).await;
|
||||||
|
|
||||||
|
editor.select_primary_color(Color::GREEN).await;
|
||||||
|
editor.click_tool(ToolType::Fill, MouseKeys::LEFT, DVec2::new(2., 2.), ModifierKeys::empty()).await;
|
||||||
|
|
||||||
|
let document = editor.active_document();
|
||||||
|
let color_value_id = get_upstream_color_value_node_id(layer, &document.network_interface).expect("the fill should start a Color Value chain");
|
||||||
|
let color_input = document
|
||||||
|
.network_interface
|
||||||
|
.document_network()
|
||||||
|
.nodes
|
||||||
|
.get(&color_value_id)
|
||||||
|
.and_then(|node| node.input(graphene_std::math_nodes::color_value::ColorInput))
|
||||||
|
.and_then(|input| input.as_value());
|
||||||
|
assert!(matches!(color_input, Some(TaggedValue::Color(color)) if *color == Color::GREEN));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn replaces_a_whole_expanse_gradient_with_a_solid_color() {
|
||||||
|
use crate::messages::tool::common_functionality::graph_modification_utils::{get_upstream_color_value_node_id, get_upstream_gradient_value_node_id};
|
||||||
|
use graph_craft::document::NodeId;
|
||||||
|
use graph_craft::document::value::TaggedValue;
|
||||||
|
|
||||||
|
let mut editor = EditorTestUtils::create();
|
||||||
|
editor.new_document().await;
|
||||||
|
editor
|
||||||
|
.handle_message(GraphOperationMessage::NewCustomLayer {
|
||||||
|
id: NodeId::new(),
|
||||||
|
nodes: Vec::new(),
|
||||||
|
parent: LayerNodeIdentifier::ROOT_PARENT,
|
||||||
|
insert_index: 0,
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
let layer = editor.active_document().metadata().all_layers().next().unwrap();
|
||||||
|
editor.handle_message(NodeGraphMessage::SelectedNodesSet { nodes: vec![layer.to_node()] }).await;
|
||||||
|
|
||||||
|
// Paint the layer's whole expanse with a gradient, which the Fill tool then takes over
|
||||||
|
editor.drag_tool(ToolType::Gradient, 0., 0., 100., 0., ModifierKeys::empty()).await;
|
||||||
|
assert!(get_upstream_gradient_value_node_id(layer, &editor.active_document().network_interface).is_some());
|
||||||
|
|
||||||
|
editor.select_primary_color(Color::GREEN).await;
|
||||||
|
editor.click_tool(ToolType::Fill, MouseKeys::LEFT, DVec2::new(2., 2.), ModifierKeys::empty()).await;
|
||||||
|
|
||||||
|
let document = editor.active_document();
|
||||||
|
let color_value_id = get_upstream_color_value_node_id(layer, &document.network_interface).expect("the fill should start a Color Value chain");
|
||||||
|
let color_input = document
|
||||||
|
.network_interface
|
||||||
|
.document_network()
|
||||||
|
.nodes
|
||||||
|
.get(&color_value_id)
|
||||||
|
.and_then(|node| node.input(graphene_std::math_nodes::color_value::ColorInput))
|
||||||
|
.and_then(|input| input.as_value());
|
||||||
|
assert!(matches!(color_input, Some(TaggedValue::Color(color)) if *color == Color::GREEN));
|
||||||
|
|
||||||
|
// The replaced gradient nodes are gone from the graph rather than left orphaned
|
||||||
|
let gradient_reference = DefinitionIdentifier::ProtoNode(graphene_std::math_nodes::gradient_value::IDENTIFIER);
|
||||||
|
let leftover_gradient_nodes = document
|
||||||
|
.network_interface
|
||||||
|
.document_network()
|
||||||
|
.nodes
|
||||||
|
.keys()
|
||||||
|
.filter(|node_id| document.network_interface.reference(node_id, &[]).as_ref() == Some(&gradient_reference))
|
||||||
|
.count();
|
||||||
|
assert_eq!(leftover_gradient_nodes, 0, "the gradient it replaced should be deleted");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn replacing_a_shared_gradient_leaves_it_for_its_other_layer() {
|
||||||
|
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, OutputConnector};
|
||||||
|
use crate::messages::tool::common_functionality::graph_modification_utils::{get_upstream_color_value_node_id, get_upstream_gradient_value_node_id};
|
||||||
|
use graph_craft::document::NodeId;
|
||||||
|
|
||||||
|
let mut editor = EditorTestUtils::create();
|
||||||
|
editor.new_document().await;
|
||||||
|
|
||||||
|
let painted_id = NodeId::new();
|
||||||
|
let sharing_id = NodeId::new();
|
||||||
|
for id in [painted_id, sharing_id] {
|
||||||
|
editor
|
||||||
|
.handle_message(GraphOperationMessage::NewCustomLayer {
|
||||||
|
id,
|
||||||
|
nodes: Vec::new(),
|
||||||
|
parent: LayerNodeIdentifier::ROOT_PARENT,
|
||||||
|
insert_index: 0,
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
let (painted, sharing) = (LayerNodeIdentifier::new_unchecked(painted_id), LayerNodeIdentifier::new_unchecked(sharing_id));
|
||||||
|
|
||||||
|
// Give one layer a whole-expanse gradient, then wire that same 'Gradient Value' node into the other layer
|
||||||
|
editor.handle_message(NodeGraphMessage::SelectedNodesSet { nodes: vec![painted_id] }).await;
|
||||||
|
editor.drag_tool(ToolType::Gradient, 0., 0., 100., 0., ModifierKeys::empty()).await;
|
||||||
|
let gradient_value_id = get_upstream_gradient_value_node_id(painted, &editor.active_document().network_interface).expect("the drag should start a gradient chain");
|
||||||
|
editor
|
||||||
|
.active_document_mut()
|
||||||
|
.network_interface
|
||||||
|
.create_wire(&OutputConnector::primary_output(gradient_value_id), &InputConnector::layer_secondary_input(sharing_id), &[]);
|
||||||
|
|
||||||
|
editor.handle_message(NodeGraphMessage::SelectedNodesSet { nodes: vec![painted_id] }).await;
|
||||||
|
editor.select_primary_color(Color::GREEN).await;
|
||||||
|
editor.click_tool(ToolType::Fill, MouseKeys::LEFT, DVec2::new(2., 2.), ModifierKeys::empty()).await;
|
||||||
|
|
||||||
|
let document = editor.active_document();
|
||||||
|
assert!(
|
||||||
|
get_upstream_color_value_node_id(painted, &document.network_interface).is_some(),
|
||||||
|
"the filled layer should get its own Color Value chain"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
document.network_interface.document_network().nodes.contains_key(&gradient_value_id),
|
||||||
|
"a gradient the rest of the graph still draws from must not be deleted"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
get_upstream_gradient_value_node_id(sharing, &document.network_interface),
|
||||||
|
Some(gradient_value_id),
|
||||||
|
"the other layer should keep its gradient"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn replacing_a_gradient_shared_through_a_transform_leaves_the_other_layer_alone() {
|
||||||
|
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, OutputConnector};
|
||||||
|
use crate::messages::tool::common_functionality::graph_modification_utils::{get_fill_node_id_with_direct_fill_input, get_upstream_color_value_node_id, get_upstream_gradient_value_node_id};
|
||||||
|
use graph_craft::document::NodeId;
|
||||||
|
|
||||||
|
let mut editor = EditorTestUtils::create();
|
||||||
|
editor.new_document().await;
|
||||||
|
|
||||||
|
// An ellipse layer whose Fill is painted by the same chain that fills the blank layer's whole expanse
|
||||||
|
editor.drag_tool(ToolType::Ellipse, 0., 0., 100., 100., ModifierKeys::empty()).await;
|
||||||
|
let ellipse = editor.active_document().metadata().all_layers().next().unwrap();
|
||||||
|
|
||||||
|
let painted_id = NodeId::new();
|
||||||
|
editor
|
||||||
|
.handle_message(GraphOperationMessage::NewCustomLayer {
|
||||||
|
id: painted_id,
|
||||||
|
nodes: Vec::new(),
|
||||||
|
parent: LayerNodeIdentifier::ROOT_PARENT,
|
||||||
|
insert_index: 0,
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
let painted = LayerNodeIdentifier::new_unchecked(painted_id);
|
||||||
|
editor.handle_message(NodeGraphMessage::SelectedNodesSet { nodes: vec![painted_id] }).await;
|
||||||
|
|
||||||
|
// Nudging leaves a 'Transform' node, and the gradient drag paints through it
|
||||||
|
editor
|
||||||
|
.handle_message(DocumentMessage::NudgeSelectedLayers {
|
||||||
|
delta_x: 10.,
|
||||||
|
delta_y: 0.,
|
||||||
|
resize: Key::Shift,
|
||||||
|
resize_opposite: Key::Alt,
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
editor.drag_tool(ToolType::Gradient, 0., 0., 100., 0., ModifierKeys::empty()).await;
|
||||||
|
let gradient_value_id = get_upstream_gradient_value_node_id(painted, &editor.active_document().network_interface).expect("the drag should start a gradient chain");
|
||||||
|
let shared_transform_id = editor
|
||||||
|
.active_document()
|
||||||
|
.network_interface
|
||||||
|
.upstream_output_connector(&InputConnector::layer_secondary_input(painted_id), &[])
|
||||||
|
.and_then(|output| output.node_id())
|
||||||
|
.expect("the nudge should leave a Transform node feeding the layer");
|
||||||
|
|
||||||
|
// Branch that same Transform into the ellipse's Fill, so both layers draw from it
|
||||||
|
let ellipse_fill_id = get_fill_node_id_with_direct_fill_input(ellipse, &editor.active_document().network_interface).expect("the ellipse should have a Fill node");
|
||||||
|
editor.active_document_mut().network_interface.create_wire(
|
||||||
|
&OutputConnector::primary_output(shared_transform_id),
|
||||||
|
&InputConnector::node(ellipse_fill_id, graphene_std::vector::fill::FillInput),
|
||||||
|
&[],
|
||||||
|
);
|
||||||
|
|
||||||
|
editor.handle_message(NodeGraphMessage::SelectedNodesSet { nodes: vec![painted_id] }).await;
|
||||||
|
editor.select_primary_color(Color::GREEN).await;
|
||||||
|
editor.click_tool(ToolType::Fill, MouseKeys::LEFT, DVec2::new(2., 2.), ModifierKeys::empty()).await;
|
||||||
|
|
||||||
|
let document = editor.active_document();
|
||||||
|
assert!(
|
||||||
|
get_upstream_color_value_node_id(painted, &document.network_interface).is_some(),
|
||||||
|
"the filled layer should get its own Color Value chain"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
document.network_interface.document_network().nodes.contains_key(&gradient_value_id),
|
||||||
|
"the gradient still painting the ellipse must not be deleted"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
document
|
||||||
|
.network_interface
|
||||||
|
.upstream_output_connector(&InputConnector::node(ellipse_fill_id, graphene_std::vector::fill::FillInput), &[])
|
||||||
|
.and_then(|output| output.node_id()),
|
||||||
|
Some(shared_transform_id),
|
||||||
|
"the ellipse should keep being painted by the shared chain"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
get_upstream_gradient_value_node_id(ellipse, &document.network_interface),
|
||||||
|
Some(gradient_value_id),
|
||||||
|
"the ellipse's fill should still resolve to the gradient"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn nudged_blank_layer_keeps_its_transform() {
|
||||||
|
use crate::messages::portfolio::document::utility_types::network_interface::InputConnector;
|
||||||
|
use crate::messages::tool::common_functionality::graph_modification_utils::get_upstream_color_value_node_id;
|
||||||
|
use graph_craft::document::NodeId;
|
||||||
|
|
||||||
|
let mut editor = EditorTestUtils::create();
|
||||||
|
editor.new_document().await;
|
||||||
|
editor
|
||||||
|
.handle_message(GraphOperationMessage::NewCustomLayer {
|
||||||
|
id: NodeId::new(),
|
||||||
|
nodes: Vec::new(),
|
||||||
|
parent: LayerNodeIdentifier::ROOT_PARENT,
|
||||||
|
insert_index: 0,
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
let layer = editor.active_document().metadata().all_layers().next().unwrap();
|
||||||
|
editor.handle_message(NodeGraphMessage::SelectedNodesSet { nodes: vec![layer.to_node()] }).await;
|
||||||
|
|
||||||
|
// Nudging an empty layer leaves a 'Transform' node in its chain, even though the layer still paints nothing
|
||||||
|
editor
|
||||||
|
.handle_message(DocumentMessage::NudgeSelectedLayers {
|
||||||
|
delta_x: 10.,
|
||||||
|
delta_y: 0.,
|
||||||
|
resize: Key::Shift,
|
||||||
|
resize_opposite: Key::Alt,
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
let layer_content_input = InputConnector::layer_secondary_input(layer.to_node());
|
||||||
|
let transform_id = editor
|
||||||
|
.active_document()
|
||||||
|
.network_interface
|
||||||
|
.upstream_output_connector(&layer_content_input, &[])
|
||||||
|
.and_then(|output| output.node_id())
|
||||||
|
.expect("the nudge should leave a Transform node feeding the layer");
|
||||||
|
|
||||||
|
editor.select_primary_color(Color::GREEN).await;
|
||||||
|
editor.click_tool(ToolType::Fill, MouseKeys::LEFT, DVec2::new(2., 2.), ModifierKeys::empty()).await;
|
||||||
|
|
||||||
|
let network_interface = &editor.active_document().network_interface;
|
||||||
|
let color_value_id = get_upstream_color_value_node_id(layer, network_interface).expect("the fill should start a Color Value chain");
|
||||||
|
assert_eq!(
|
||||||
|
network_interface.upstream_output_connector(&layer_content_input, &[]).and_then(|output| output.node_id()),
|
||||||
|
Some(transform_id),
|
||||||
|
"the Transform node should still feed the layer"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
network_interface
|
||||||
|
.upstream_output_connector(&InputConnector::primary_input(transform_id), &[])
|
||||||
|
.and_then(|output| output.node_id()),
|
||||||
|
Some(color_value_id),
|
||||||
|
"the color should be painted through the preserved Transform node"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn node_displayed_as_layer_gets_no_color_chain() {
|
||||||
|
use crate::messages::tool::common_functionality::graph_modification_utils::get_upstream_color_value_node_id;
|
||||||
|
|
||||||
|
let mut editor = EditorTestUtils::create();
|
||||||
|
editor.new_document().await;
|
||||||
|
|
||||||
|
// A generator node displayed as a layer looks empty from the outside, but its secondary input is a parameter of its own
|
||||||
|
let rectangle = editor
|
||||||
|
.create_node_by_name(DefinitionIdentifier::ProtoNode(graphene_std::vector::generator_nodes::rectangle::IDENTIFIER))
|
||||||
|
.await;
|
||||||
|
let network_interface = &mut editor.active_document_mut().network_interface;
|
||||||
|
network_interface.set_to_node_or_layer(&rectangle, &[], true);
|
||||||
|
let layer = LayerNodeIdentifier::new(rectangle, network_interface);
|
||||||
|
network_interface.move_layer_to_stack(layer, LayerNodeIdentifier::ROOT_PARENT, 0, &[]);
|
||||||
|
|
||||||
|
editor.handle_message(NodeGraphMessage::SelectedNodesSet { nodes: vec![rectangle] }).await;
|
||||||
|
assert!(editor.active_document().metadata().all_layers().any(|other| other == layer), "the node should sit in the layer stack");
|
||||||
|
|
||||||
|
editor.select_primary_color(Color::GREEN).await;
|
||||||
|
editor.click_tool(ToolType::Fill, MouseKeys::LEFT, DVec2::new(2., 2.), ModifierKeys::empty()).await;
|
||||||
|
|
||||||
|
let document = editor.active_document();
|
||||||
|
assert!(
|
||||||
|
get_upstream_color_value_node_id(layer, &document.network_interface).is_none(),
|
||||||
|
"only a 'Merge' layer may have a whole-expanse color chain started on it"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ use crate::messages::portfolio::document::utility_types::document_metadata::Laye
|
|||||||
use crate::messages::portfolio::document::utility_types::network_interface::{FlowType, NodeNetworkInterface};
|
use crate::messages::portfolio::document::utility_types::network_interface::{FlowType, NodeNetworkInterface};
|
||||||
use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
|
use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
|
||||||
use crate::messages::tool::common_functionality::graph_modification_utils::{
|
use crate::messages::tool::common_functionality::graph_modification_utils::{
|
||||||
self, NodeGraphLayer, get_fill_node_id_with_direct_fill_input, get_gradient_stops, get_upstream_gradient_value_node_id, gradient_chain_target_input, reverse_direction_tooltip_description,
|
self, NodeGraphLayer, get_fill_node_id_with_direct_fill_input, get_gradient_stops, get_upstream_gradient_value_node_id, gradient_chain_target_input, replaceable_paint_chain,
|
||||||
|
reverse_direction_tooltip_description,
|
||||||
};
|
};
|
||||||
use crate::messages::tool::common_functionality::snapping::{SnapCandidatePoint, SnapConstraint, SnapData, SnapManager, SnapTypeConfiguration};
|
use crate::messages::tool::common_functionality::snapping::{SnapCandidatePoint, SnapConstraint, SnapData, SnapManager, SnapTypeConfiguration};
|
||||||
use glam::DMat2;
|
use glam::DMat2;
|
||||||
@@ -1437,14 +1438,14 @@ impl Fsm for GradientToolFsmState {
|
|||||||
GradientToolFsmState::Drawing { drag_hint: hint }
|
GradientToolFsmState::Drawing { drag_hint: hint }
|
||||||
} else {
|
} else {
|
||||||
let document_mouse = document.metadata().document_to_viewport.inverse().transform_point2(mouse);
|
let document_mouse = document.metadata().document_to_viewport.inverse().transform_point2(mouse);
|
||||||
// List-based gradients render no geometry, so a click on empty canvas yields no layer.
|
// List-based gradients and blank layers render no geometry, so a click on empty canvas yields no layer.
|
||||||
// Fall back to a selected gradient list layer so the user can drag a fresh gradient line anywhere.
|
// Fall back to a selected gradient list layer, or one blank enough to take a fresh whole-expanse gradient.
|
||||||
let selected_layer = document.click_based_on_position(document_mouse).or_else(|| {
|
let selected_layer = document.click_based_on_position(document_mouse).or_else(|| {
|
||||||
document
|
document
|
||||||
.network_interface
|
.network_interface
|
||||||
.selected_nodes()
|
.selected_nodes()
|
||||||
.selected_visible_layers(&document.network_interface)
|
.selected_visible_layers(&document.network_interface)
|
||||||
.find(|&layer| get_gradient_stops(layer, &document.network_interface).is_some())
|
.find(|&layer| get_gradient_stops(layer, &document.network_interface).is_some() || replaceable_paint_chain(layer, &document.network_interface).is_some())
|
||||||
});
|
});
|
||||||
|
|
||||||
// Apply the gradient to the selected layer
|
// Apply the gradient to the selected layer
|
||||||
@@ -1485,7 +1486,12 @@ impl Fsm for GradientToolFsmState {
|
|||||||
gradient_form: tool_options.gradient_form,
|
gradient_form: tool_options.gradient_form,
|
||||||
gradient_spread: tool_options.gradient_spread,
|
gradient_spread: tool_options.gradient_spread,
|
||||||
},
|
},
|
||||||
GradientSource::Direct,
|
// A blank layer, or one holding only the other tool's paint, starts a whole-expanse gradient chain; a layer with content gets its Fill painted
|
||||||
|
if replaceable_paint_chain(layer, &document.network_interface).is_some() {
|
||||||
|
GradientSource::Chain
|
||||||
|
} else {
|
||||||
|
GradientSource::Direct
|
||||||
|
},
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
let mut selected_gradient = SelectedGradient::new(gradient, appearance, source, layer, document);
|
let mut selected_gradient = SelectedGradient::new(gradient, appearance, source, layer, document);
|
||||||
@@ -2973,4 +2979,184 @@ mod test_gradient {
|
|||||||
.expect("the positions input should exist");
|
.expect("the positions input should exist");
|
||||||
assert!(matches!(positions_input, NodeInput::Node { .. }), "the wired positions input must survive the baked write");
|
assert!(matches!(positions_input, NodeInput::Node { .. }), "the wired positions input must survive the baked write");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn drag_on_blank_layer_starts_gradient_chain() {
|
||||||
|
use crate::messages::tool::common_functionality::graph_modification_utils::get_gradient_stops;
|
||||||
|
use graph_craft::document::NodeId;
|
||||||
|
|
||||||
|
let mut editor = EditorTestUtils::create();
|
||||||
|
editor.new_document().await;
|
||||||
|
editor
|
||||||
|
.handle_message(GraphOperationMessage::NewCustomLayer {
|
||||||
|
id: NodeId::new(),
|
||||||
|
nodes: Vec::new(),
|
||||||
|
parent: LayerNodeIdentifier::ROOT_PARENT,
|
||||||
|
insert_index: 0,
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
let layer = editor.active_document().metadata().all_layers().next().unwrap();
|
||||||
|
editor.handle_message(NodeGraphMessage::SelectedNodesSet { nodes: vec![layer.to_node()] }).await;
|
||||||
|
|
||||||
|
editor.drag_tool(ToolType::Gradient, 0., 0., 100., 0., ModifierKeys::empty()).await;
|
||||||
|
|
||||||
|
let document = editor.active_document();
|
||||||
|
assert!(
|
||||||
|
get_upstream_gradient_value_node_id(layer, &document.network_interface).is_some(),
|
||||||
|
"the drag should start a gradient chain"
|
||||||
|
);
|
||||||
|
let stops = get_gradient_stops(layer, &document.network_interface).expect("the new chain should resolve stops");
|
||||||
|
assert_eq!(stops.len(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn replaces_a_whole_expanse_color_with_a_gradient() {
|
||||||
|
use crate::messages::tool::common_functionality::graph_modification_utils::{get_gradient_stops, get_upstream_color_value_node_id};
|
||||||
|
use graph_craft::document::NodeId;
|
||||||
|
|
||||||
|
let mut editor = EditorTestUtils::create();
|
||||||
|
editor.new_document().await;
|
||||||
|
editor
|
||||||
|
.handle_message(GraphOperationMessage::NewCustomLayer {
|
||||||
|
id: NodeId::new(),
|
||||||
|
nodes: Vec::new(),
|
||||||
|
parent: LayerNodeIdentifier::ROOT_PARENT,
|
||||||
|
insert_index: 0,
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
let layer = editor.active_document().metadata().all_layers().next().unwrap();
|
||||||
|
editor.handle_message(NodeGraphMessage::SelectedNodesSet { nodes: vec![layer.to_node()] }).await;
|
||||||
|
|
||||||
|
// Nudging leaves a 'Transform' node that must survive the swap, and the fill paints the whole expanse
|
||||||
|
editor
|
||||||
|
.handle_message(DocumentMessage::NudgeSelectedLayers {
|
||||||
|
delta_x: 10.,
|
||||||
|
delta_y: 0.,
|
||||||
|
resize: Key::Shift,
|
||||||
|
resize_opposite: Key::Alt,
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
editor.select_primary_color(Color::GREEN).await;
|
||||||
|
editor.click_tool(ToolType::Fill, MouseKeys::LEFT, DVec2::new(2., 2.), ModifierKeys::empty()).await;
|
||||||
|
assert!(get_upstream_color_value_node_id(layer, &editor.active_document().network_interface).is_some());
|
||||||
|
|
||||||
|
editor.drag_tool(ToolType::Gradient, 0., 0., 100., 0., ModifierKeys::empty()).await;
|
||||||
|
|
||||||
|
let document = editor.active_document();
|
||||||
|
assert!(
|
||||||
|
get_upstream_gradient_value_node_id(layer, &document.network_interface).is_some(),
|
||||||
|
"the drag should start a gradient chain"
|
||||||
|
);
|
||||||
|
assert_eq!(get_gradient_stops(layer, &document.network_interface).expect("the new chain should resolve stops").len(), 2);
|
||||||
|
assert!(
|
||||||
|
get_upstream_color_value_node_id(layer, &document.network_interface).is_none(),
|
||||||
|
"the color it replaced should be gone from the chain"
|
||||||
|
);
|
||||||
|
|
||||||
|
// The layer's own placement is not the paint's, so nudging survives a swap
|
||||||
|
let transform_reference = DefinitionIdentifier::ProtoNode(graphene_std::transform_nodes::transform::IDENTIFIER);
|
||||||
|
let chain_transforms = document
|
||||||
|
.network_interface
|
||||||
|
.upstream_flow_back_from_nodes(vec![layer.to_node()], &[], super::FlowType::HorizontalFlow)
|
||||||
|
.filter(|node_id| document.network_interface.reference(node_id, &[]).as_ref() == Some(&transform_reference))
|
||||||
|
.count();
|
||||||
|
assert_eq!(chain_transforms, 1, "the layer's Transform node should survive the swap");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn chain_started_past_a_transform_sits_beside_it() {
|
||||||
|
use graph_craft::document::NodeId;
|
||||||
|
|
||||||
|
let mut editor = EditorTestUtils::create();
|
||||||
|
editor.new_document().await;
|
||||||
|
let layer_id = NodeId::new();
|
||||||
|
editor
|
||||||
|
.handle_message(GraphOperationMessage::NewCustomLayer {
|
||||||
|
id: layer_id,
|
||||||
|
nodes: Vec::new(),
|
||||||
|
parent: LayerNodeIdentifier::ROOT_PARENT,
|
||||||
|
insert_index: 0,
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
let layer = LayerNodeIdentifier::new_unchecked(layer_id);
|
||||||
|
editor.handle_message(NodeGraphMessage::SelectedNodesSet { nodes: vec![layer_id] }).await;
|
||||||
|
// Sitting away from where fresh nodes are dropped, so the chain's placement can't coincide with it
|
||||||
|
editor.active_document_mut().network_interface.shift_node(&layer_id, IVec2::new(20, 4), &[]);
|
||||||
|
|
||||||
|
editor.drag_tool(ToolType::Gradient, 0., 0., 100., 0., ModifierKeys::empty()).await;
|
||||||
|
let transform_id = editor
|
||||||
|
.active_document()
|
||||||
|
.network_interface
|
||||||
|
.upstream_output_connector(&InputConnector::layer_secondary_input(layer_id), &[])
|
||||||
|
.and_then(|output| output.node_id())
|
||||||
|
.expect("the drag's placement should leave a Transform node feeding the layer");
|
||||||
|
|
||||||
|
// Each chain node sits one chain width left of the one it feeds, so the new value node lands beside the Transform
|
||||||
|
let network_interface = &editor.active_document().network_interface;
|
||||||
|
let gradient_value_id = get_upstream_gradient_value_node_id(layer, network_interface).expect("the drag should start a gradient chain");
|
||||||
|
let layer_position = network_interface.position(&layer_id, &[]).expect("the layer should have a position");
|
||||||
|
assert_eq!(network_interface.position(&transform_id, &[]), Some(layer_position - IVec2::new(crate::consts::NODE_CHAIN_WIDTH, 0)));
|
||||||
|
assert_eq!(
|
||||||
|
network_interface.position(&gradient_value_id, &[]),
|
||||||
|
Some(layer_position - IVec2::new(2 * crate::consts::NODE_CHAIN_WIDTH, 0))
|
||||||
|
);
|
||||||
|
assert!(network_interface.is_chain(&gradient_value_id, &[]), "the value node should stay part of the layer's chain");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn drag_on_nudged_blank_layer_reuses_its_transform() {
|
||||||
|
use crate::messages::tool::common_functionality::graph_modification_utils::get_gradient_stops;
|
||||||
|
use graph_craft::document::NodeId;
|
||||||
|
|
||||||
|
let mut editor = EditorTestUtils::create();
|
||||||
|
editor.new_document().await;
|
||||||
|
editor
|
||||||
|
.handle_message(GraphOperationMessage::NewCustomLayer {
|
||||||
|
id: NodeId::new(),
|
||||||
|
nodes: Vec::new(),
|
||||||
|
parent: LayerNodeIdentifier::ROOT_PARENT,
|
||||||
|
insert_index: 0,
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
let layer = editor.active_document().metadata().all_layers().next().unwrap();
|
||||||
|
editor.handle_message(NodeGraphMessage::SelectedNodesSet { nodes: vec![layer.to_node()] }).await;
|
||||||
|
|
||||||
|
// Nudging an empty layer leaves a 'Transform' node in its chain, which the gradient then adopts as its placement
|
||||||
|
editor
|
||||||
|
.handle_message(DocumentMessage::NudgeSelectedLayers {
|
||||||
|
delta_x: 10.,
|
||||||
|
delta_y: 0.,
|
||||||
|
resize: Key::Shift,
|
||||||
|
resize_opposite: Key::Alt,
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
let layer_content_input = InputConnector::layer_secondary_input(layer.to_node());
|
||||||
|
let transform_id = editor
|
||||||
|
.active_document()
|
||||||
|
.network_interface
|
||||||
|
.upstream_output_connector(&layer_content_input, &[])
|
||||||
|
.and_then(|output| output.node_id())
|
||||||
|
.expect("the nudge should leave a Transform node feeding the layer");
|
||||||
|
|
||||||
|
editor.drag_tool(ToolType::Gradient, 0., 0., 100., 0., ModifierKeys::empty()).await;
|
||||||
|
|
||||||
|
let network_interface = &editor.active_document().network_interface;
|
||||||
|
let gradient_value_id = get_upstream_gradient_value_node_id(layer, network_interface).expect("the drag should start a gradient chain");
|
||||||
|
assert_eq!(get_gradient_stops(layer, network_interface).expect("the new chain should resolve stops").len(), 2);
|
||||||
|
|
||||||
|
// The gradient paints through the nudge's Transform node rather than adding a second one
|
||||||
|
let transform_reference = DefinitionIdentifier::ProtoNode(graphene_std::transform_nodes::transform::IDENTIFIER);
|
||||||
|
let chain_transforms = network_interface
|
||||||
|
.upstream_flow_back_from_nodes(vec![layer.to_node()], &[], super::FlowType::HorizontalFlow)
|
||||||
|
.filter(|node_id| network_interface.reference(node_id, &[]).as_ref() == Some(&transform_reference))
|
||||||
|
.count();
|
||||||
|
assert_eq!(chain_transforms, 1, "the gradient should adopt the existing Transform node");
|
||||||
|
assert_eq!(
|
||||||
|
network_interface
|
||||||
|
.upstream_output_connector(&InputConnector::primary_input(transform_id), &[])
|
||||||
|
.and_then(|output| output.node_id()),
|
||||||
|
Some(gradient_value_id),
|
||||||
|
"the gradient should be painted through the preserved Transform node"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user