mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 22:28:10 +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:
committed by
Dennis Kobert
parent
4da366acf1
commit
b635e6fa02
@@ -20,6 +20,10 @@ pub enum GraphOperationMessage {
|
||||
layer: LayerNodeIdentifier,
|
||||
color: Option<Color>,
|
||||
},
|
||||
ColorValueSet {
|
||||
layer: LayerNodeIdentifier,
|
||||
color: Color,
|
||||
},
|
||||
FillGradientSet {
|
||||
layer: LayerNodeIdentifier,
|
||||
#[serde(skip)]
|
||||
|
||||
@@ -40,6 +40,11 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
|
||||
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 {
|
||||
layer,
|
||||
gradient,
|
||||
@@ -324,7 +329,7 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
|
||||
GraphOperationMessage::NewColorFillLayer { node_id, color, parent, insert_index } => {
|
||||
let mut modify_inputs = ModifyInputsContext::new(network_interface, responses);
|
||||
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, &[]);
|
||||
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::network_interface::{self, FlowType, InputConnector, NodeNetworkInterface};
|
||||
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 graph_craft::application_io::resource::ResourceId;
|
||||
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);
|
||||
}
|
||||
|
||||
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)
|
||||
.expect("Color Value node does not exist")
|
||||
.node_template_input_override([Some(NodeInput::value(TaggedValue::None, false)), Some(NodeInput::value(TaggedValue::Color(color), false))]);
|
||||
|
||||
let color_value_id = NodeId::new();
|
||||
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) {
|
||||
@@ -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.
|
||||
pub fn gradient_stops_set(&mut self, stops: Gradient) {
|
||||
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 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
|
||||
if starts_layer_chain && self.network_interface.upstream_output_connector(&target, &[]).is_some() {
|
||||
log::error!("Refusing to start a gradient chain ahead of existing layer content");
|
||||
return;
|
||||
}
|
||||
// The 'Gradient 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 paint_chain = if starts_layer_chain {
|
||||
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 {
|
||||
return;
|
||||
@@ -525,9 +585,9 @@ impl<'a> ModifyInputsContext<'a> {
|
||||
let node_id = NodeId::new();
|
||||
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
|
||||
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 {
|
||||
// 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), &[]);
|
||||
|
||||
@@ -980,6 +980,13 @@ impl NodeNetworkInterface {
|
||||
let Some(feeder_position) = self.position(&feeder, network_path) else { return };
|
||||
|
||||
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.
|
||||
let upstream_nodes: HashSet<NodeId> = self.upstream_flow_back_from_nodes(vec![feeder], network_path, FlowType::UpstreamFlow).collect();
|
||||
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()
|
||||
}
|
||||
|
||||
/// 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.
|
||||
pub fn all_artboards(&self) -> HashSet<LayerNodeIdentifier> {
|
||||
// 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()))
|
||||
}
|
||||
|
||||
/// 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.
|
||||
pub fn implementation_name(&self, node_id: &NodeId) -> String {
|
||||
self.reference(node_id)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
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::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 glam::{DAffine2, DVec2};
|
||||
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.
|
||||
pub fn get_upstream_gradient_value_node_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<NodeId> {
|
||||
/// Try to find the paint value node feeding a 'Fill' node, or a layer directly.
|
||||
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 walk_from = network_interface.upstream_output_connector(&target_input, &[])?.node_id()?;
|
||||
let reference = DefinitionIdentifier::ProtoNode(identifier);
|
||||
|
||||
network_interface
|
||||
.upstream_flow_back_from_nodes(vec![walk_from], &[], FlowType::HorizontalFlow)
|
||||
.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.
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
use super::tool_prelude::*;
|
||||
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::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::raster::color::Color;
|
||||
use graphene_std::subpath::Subpath;
|
||||
use graphene_std::vector::PointId;
|
||||
use graphene_std::vector::style::FillChoice;
|
||||
|
||||
#[derive(Default, ExtractField)]
|
||||
@@ -140,10 +144,16 @@ impl Fsm for FillToolFsmState {
|
||||
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 };
|
||||
|
||||
// Get the layer the user is hovering over
|
||||
if let Some(layer) = document.click(input, viewport) {
|
||||
// Pattern the layer the fill would land on, over its whole expanse when the color is its entire content
|
||||
if let Some(layer) = fill_target_layer(document, input, viewport) {
|
||||
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
|
||||
@@ -154,9 +164,11 @@ impl Fsm for FillToolFsmState {
|
||||
self
|
||||
}
|
||||
(FillToolFsmState::Ready, color_event) => {
|
||||
let Some(layer_identifier) = document.click(input, viewport) else {
|
||||
return self;
|
||||
};
|
||||
let Some(layer_identifier) = fill_target_layer(document, input, viewport) else { 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 NodeGraphLayer::is_raster_layer(layer_identifier, &mut document.network_interface) {
|
||||
return self;
|
||||
@@ -168,10 +180,14 @@ impl Fsm for FillToolFsmState {
|
||||
};
|
||||
|
||||
responses.add(DocumentMessage::AddTransaction);
|
||||
responses.add(GraphOperationMessage::FillColorSet {
|
||||
layer: layer_identifier,
|
||||
color: Some(color),
|
||||
});
|
||||
if route_to_color_chain {
|
||||
responses.add(GraphOperationMessage::ColorValueSet { layer: layer_identifier, color });
|
||||
} else {
|
||||
responses.add(GraphOperationMessage::FillColorSet {
|
||||
layer: layer_identifier,
|
||||
color: Some(color),
|
||||
});
|
||||
}
|
||||
|
||||
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)]
|
||||
mod test_fill {
|
||||
pub use crate::test_utils::test_prelude::*;
|
||||
@@ -262,4 +315,302 @@ mod test_fill {
|
||||
assert_eq!(fills.len(), 1);
|
||||
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::tool::common_functionality::auto_panning::AutoPanning;
|
||||
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 glam::DMat2;
|
||||
@@ -1437,14 +1438,14 @@ impl Fsm for GradientToolFsmState {
|
||||
GradientToolFsmState::Drawing { drag_hint: hint }
|
||||
} else {
|
||||
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.
|
||||
// Fall back to a selected gradient list layer so the user can drag a fresh gradient line anywhere.
|
||||
// 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, or one blank enough to take a fresh whole-expanse gradient.
|
||||
let selected_layer = document.click_based_on_position(document_mouse).or_else(|| {
|
||||
document
|
||||
.network_interface
|
||||
.selected_nodes()
|
||||
.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
|
||||
@@ -1485,7 +1486,12 @@ impl Fsm for GradientToolFsmState {
|
||||
gradient_form: tool_options.gradient_form,
|
||||
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);
|
||||
@@ -2973,4 +2979,184 @@ mod test_gradient {
|
||||
.expect("the positions input should exist");
|
||||
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