Replace the IntoPaint trait with direct Graphic-typed node inputs (#4442)

* Let the Fill and Stroke paint inputs take Item<Graphic>, replacing the IntoPaint trait

* Rename the FIll node's "fill" input to "paint"

* Let the graphic-consuming nodes take List<Graphic> directly, relying on the embedding adapters

* Remove outdated todo comments

* Re-save the demo art
This commit is contained in:
Keavon Chambers
2026-08-17 03:10:02 -07:00
committed by GitHub
parent d63362718f
commit ef6d430f97
27 changed files with 176 additions and 280 deletions

View File

@@ -103,6 +103,7 @@ pub struct DocumentMessageHandler {
pub properties_panel_collapsed_sections: Vec<NodeId>,
/// The full Git commit hash of the Graphite repository that was used to build the editor.
/// We save this to provide a hint about which version of the editor was used to create the document.
#[serde(skip_deserializing, default)]
pub commit_hash: String,
/// The current pan, tilt, and zoom state of the viewport's view of the document canvas.
pub document_ptz: PTZ,

View File

@@ -435,7 +435,7 @@ impl<'a> ModifyInputsContext<'a> {
let Some(fill_node_id) = existing_fill_node_id.or_else(|| self.existing_chain_hosted_node_id(graphene_std::vector_nodes::fill::IDENTIFIER, true)) else {
return;
};
let input_connector = InputConnector::node(fill_node_id, graphene_std::vector::fill::FillInput);
let input_connector = InputConnector::node(fill_node_id, graphene_std::vector::fill::PaintInput);
let backup_input_connector = InputConnector::node(fill_node_id, graphene_std::vector::fill::BackupColorInput);
// The backup remembers the last solid color, so the red-slash "none" choice leaves it untouched
@@ -462,7 +462,7 @@ impl<'a> ModifyInputsContext<'a> {
// Skip the rerender on all but the last input so the whole update triggers a single graph run
self.set_input_with_refresh(
InputConnector::node(fill_node_id, graphene_std::vector::fill::FillInput),
InputConnector::node(fill_node_id, graphene_std::vector::fill::PaintInput),
NodeInput::value(TaggedValue::GradientRamp(ramp), false),
true,
);

View File

@@ -1762,7 +1762,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
let is_text_node = reference.as_ref().is_some_and(|r| *r == DefinitionIdentifier::ProtoNode(graphene_std::text::text::IDENTIFIER));
let is_stroke_node = reference.as_ref().is_some_and(|r| *r == DefinitionIdentifier::ProtoNode(graphene_std::vector::stroke::IDENTIFIER));
let is_fill_node = reference.as_ref().is_some_and(|r| *r == DefinitionIdentifier::ProtoNode(graphene_std::vector::fill::IDENTIFIER));
let is_fill_input = is_fill_node && input_index == graphene_std::vector::fill::FillInput::INDEX;
let is_fill_input = is_fill_node && input_index == graphene_std::vector::fill::PaintInput::INDEX;
let is_shape_generator_node = reference.as_ref().is_some_and(|r| {
[regular_polygon::IDENTIFIER, star::IDENTIFIER, arc::IDENTIFIER, spiral::IDENTIFIER, grid::IDENTIFIER, arrow::IDENTIFIER]
.into_iter()

View File

@@ -2415,9 +2415,9 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
}
// Pass blank_assist=false because the assist slot is filled below ("Reverse Stops" button when in gradient mode)
let mut widgets_first_row = start_widgets(&ParameterWidgetsInfo::new(node_id, FillInput, false, context));
let mut widgets_first_row = start_widgets(&ParameterWidgetsInfo::new(node_id, PaintInput, false, context));
if get_document_node(node_id, context).is_ok_and(|node| node.input(FillInput).is_some_and(|input| input.is_exposed())) {
if get_document_node(node_id, context).is_ok_and(|node| node.input(PaintInput).is_some_and(|input| input.is_exposed())) {
return vec![LayoutGroup::row(widgets_first_row)];
}
@@ -2426,7 +2426,7 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
let layer = root_layer_for_chain_node(node_id, context);
let fill = match get_document_node(node_id, context) {
Ok(document_node) => match document_node.input_value(FillInput) {
Ok(document_node) => match document_node.input_value(PaintInput) {
Some(TaggedValue::Color(color)) => ResolvedFill::Solid(Some(*color)),
Some(value) if value.is_no_paint() => ResolvedFill::Solid(None),
Some(TaggedValue::GradientRamp(_)) => {
@@ -2474,7 +2474,7 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
.on_update(update_value(
move |_| TaggedValue::GradientRamp(GradientRamp::from(stops.reversed(settings.cyclic)).with_settings(settings)),
node_id,
FillInput,
PaintInput,
))
.widget_instance();
widgets_first_row.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
@@ -2499,7 +2499,7 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
let mut messages = vec![
NodeGraphMessage::SetInputValue {
node_id,
input_index: FillInput::INDEX,
input_index: PaintInput::INDEX,
value: color.map_or_else(TaggedValue::no_paint, TaggedValue::Color).into(),
}
.into(),
@@ -2521,7 +2521,7 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
messages: Box::new([
NodeGraphMessage::SetInputValue {
node_id,
input_index: FillInput::INDEX,
input_index: PaintInput::INDEX,
value: TaggedValue::GradientRamp(ramp.clone()).into(),
}
.into(),
@@ -2559,11 +2559,11 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
let entries = vec![
RadioEntryData::new("solid")
.label("Solid")
.on_update(update_value(move |_| backup_color.map_or_else(TaggedValue::no_paint, TaggedValue::Color), node_id, FillInput))
.on_update(update_value(move |_| backup_color.map_or_else(TaggedValue::no_paint, TaggedValue::Color), node_id, PaintInput))
.on_commit(commit_value),
RadioEntryData::new("gradient")
.label("Gradient")
.on_update(update_value(move |_| TaggedValue::GradientRamp(backup_gradient.clone()), node_id, FillInput))
.on_update(update_value(move |_| TaggedValue::GradientRamp(backup_gradient.clone()), node_id, PaintInput))
.on_commit(commit_value),
];

View File

@@ -725,7 +725,7 @@ fn find_fill_node(document: &DocumentMessageHandler) -> (Vec<graph_craft::docume
fn fill_paint_value(document: &DocumentMessageHandler) -> graph_craft::document::value::TaggedValue {
let (network_path, node_id) = find_fill_node(document);
let network = document.network_interface.nested_network(&network_path).expect("the found network path should resolve");
let input = network.nodes[&node_id].input(graphene_std::vector::fill::FillInput).expect("Fill should have a paint input");
let input = network.nodes[&node_id].input(graphene_std::vector::fill::PaintInput).expect("Fill should have a paint input");
input.as_value().expect("the paint input should hold a value").clone()
}
@@ -740,7 +740,7 @@ async fn none_fill_survives_document_reopen() {
editor
.handle_message(NodeGraphMessage::SetInputValue {
node_id: fill_node_id,
input_index: graphene_std::vector::fill::FillInput::INDEX,
input_index: graphene_std::vector::fill::PaintInput::INDEX,
value: graph_craft::document::value::TaggedValue::no_paint().into(),
})
.await;
@@ -785,7 +785,7 @@ async fn legacy_four_input_fill_migrates_to_the_split_transform_shape() {
let fill_node = &network.nodes[&node_id];
assert_eq!(fill_node.inputs.len(), 7, "the legacy Fill should upgrade to the 7-input shape");
let paint = fill_node.input(graphene_std::vector::fill::FillInput);
let paint = fill_node.input(graphene_std::vector::fill::PaintInput);
assert!(
matches!(paint, Some(graph_craft::document::NodeInput::Node { .. })),
"the wired legacy fill should keep its connection, but became {paint:?}"
@@ -844,7 +844,7 @@ async fn eight_input_fill_migrates_the_spread_input_into_the_ramp() {
assert_eq!(fill_node.inputs.len(), 7, "the eight-input Fill should fold down to the 7-input shape");
let paint = fill_node.input_value(graphene_std::vector::fill::FillInput);
let paint = fill_node.input_value(graphene_std::vector::fill::PaintInput);
let Some(TaggedValue::GradientRamp(ramp)) = paint else {
panic!("the fill input should keep its gradient ramp value, but became {paint:?}");
};

View File

@@ -1910,7 +1910,7 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
// This must run before the stale-List-default cleanup below, which would otherwise adopt the definition's default paint.
{
let legacy_no_paint = TaggedValue::TypeDefault(list!(graphene_std::Graphic));
let paint_parameters: &[ParameterRef] = &[graphene_std::vector::fill::FillInput.into(), graphene_std::vector::stroke::PaintInput.into()];
let paint_parameters: &[ParameterRef] = &[graphene_std::vector::fill::PaintInput.into(), graphene_std::vector::stroke::PaintInput.into()];
for parameter in paint_parameters {
if reference != DefinitionIdentifier::ProtoNode(parameter.node_identifier.clone()) {
continue;

View File

@@ -272,14 +272,14 @@ pub fn get_viewport_center(layer: LayerNodeIdentifier, network_interface: &NodeN
pub fn get_fill_node_id_with_direct_fill_input(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<NodeId> {
let fill_node_id = NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name(&DefinitionIdentifier::ProtoNode(graphene_std::vector::fill::IDENTIFIER))?;
let fill_node = network_interface.document_network().nodes.get(&fill_node_id)?;
matches!(fill_node.input(graphene_std::vector::fill::FillInput)?, NodeInput::Value { .. }).then_some(fill_node_id)
matches!(fill_node.input(graphene_std::vector::fill::PaintInput)?, NodeInput::Value { .. }).then_some(fill_node_id)
}
/// Determine the input connector where the gradient chain enters the layer.
/// Returns Fill's fill input if the layer has a "Fill" node, otherwise returns the layer's content input.
pub fn gradient_chain_target_input(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> InputConnector {
if let Some(fill_node_id) = NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name(&DefinitionIdentifier::ProtoNode(graphene_std::vector::fill::IDENTIFIER)) {
InputConnector::node(fill_node_id, graphene_std::vector::fill::FillInput)
InputConnector::node(fill_node_id, graphene_std::vector::fill::PaintInput)
} else {
InputConnector::layer_secondary_input(layer.to_node())
}
@@ -382,7 +382,7 @@ pub fn get_upstream_color_value_node_id(layer: LayerNodeIdentifier, network_inte
pub fn get_fill_input_node_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<NodeId> {
let fill_node_id = NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name(&DefinitionIdentifier::ProtoNode(graphene_std::vector::fill::IDENTIFIER))?;
let fill_node = network_interface.document_network().nodes.get(&fill_node_id)?;
let NodeInput::Node { node_id, .. } = fill_node.input(graphene_std::vector::fill::FillInput)? else {
let NodeInput::Node { node_id, .. } = fill_node.input(graphene_std::vector::fill::PaintInput)? else {
return None;
};
Some(*node_id)
@@ -410,7 +410,7 @@ pub fn get_gradient_stops(layer: LayerNodeIdentifier, network_interface: &NodeNe
.document_network()
.nodes
.get(&fill_node_id)
.and_then(|node| node.input(graphene_std::vector::fill::FillInput))
.and_then(|node| node.input(graphene_std::vector::fill::PaintInput))
.and_then(|input| input.as_value())
.and_then(|value| if let TaggedValue::GradientRamp(ramp) = value { Some(Gradient::from(ramp)) } else { None });
}
@@ -499,7 +499,7 @@ pub fn gradient_orientation_rightward(transform: glam::DAffine2) -> bool {
/// Get the current fill of a layer from the closest "Fill" node.
pub fn get_fill_color(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<Color> {
let TaggedValue::Color(color) = NodeGraphLayer::new(layer, network_interface).parameter_value(graphene_std::vector::fill::FillInput)? else {
let TaggedValue::Color(color) = NodeGraphLayer::new(layer, network_interface).parameter_value(graphene_std::vector::fill::PaintInput)? else {
return None;
};
Some(*color)
@@ -812,7 +812,7 @@ pub struct FillNodeGradient {
pub fn read_fill_node_gradient(fill_node: &DocumentNode, bounding_box: impl FnOnce() -> [DVec2; 2]) -> Option<FillNodeGradient> {
use graphene_std::vector::fill;
let TaggedValue::GradientRamp(ramp) = fill_node.input(fill::FillInput)?.as_value()? else {
let TaggedValue::GradientRamp(ramp) = fill_node.input(fill::PaintInput)?.as_value()? else {
return None;
};
let settings = GradientSettings::from(ramp);
@@ -885,7 +885,7 @@ pub fn selected_fill_state(document: &DocumentMessageHandler) -> Option<Selected
let fill_choice = (|| {
let fill_node = document.network_interface.document_network().nodes.get(&fill_node_id)?;
match fill_node.input(graphene_std::vector::fill::FillInput)?.as_value()? {
match fill_node.input(graphene_std::vector::fill::PaintInput)?.as_value()? {
TaggedValue::Color(color) => Some(FillChoice::Solid(*color)),
TaggedValue::GradientRamp(ramp) => Some(FillChoice::Gradient(ramp.clone())),
value if value.is_no_paint() => Some(FillChoice::None),

View File

@@ -269,7 +269,7 @@ mod test_fill {
Err(e) => panic!("Failed to evaluate graph: {e}"),
};
instrumented.grab_all_input::<fill::FillInput, Item<Color>>(&editor.runtime).collect()
instrumented.grab_all_input::<fill::PaintInput, Item<Color>>(&editor.runtime).collect()
}
#[tokio::test]
@@ -498,7 +498,7 @@ mod test_fill {
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),
&InputConnector::node(ellipse_fill_id, graphene_std::vector::fill::PaintInput),
&[],
);
@@ -518,7 +518,7 @@ mod test_fill {
assert_eq!(
document
.network_interface
.upstream_output_connector(&InputConnector::node(ellipse_fill_id, graphene_std::vector::fill::FillInput), &[])
.upstream_output_connector(&InputConnector::node(ellipse_fill_id, graphene_std::vector::fill::PaintInput), &[])
.and_then(|output| output.node_id()),
Some(shared_transform_id),
"the ellipse should keep being painted by the shared chain"

View File

@@ -2165,7 +2165,7 @@ mod test_gradient {
let fill_node_id = get_fill_node_id_with_direct_fill_input(layer, &document.network_interface)?;
let fill_node = document.network_interface.document_network().nodes.get(&fill_node_id)?;
let (stops, gradient_spread) = match fill_node.input(fill::FillInput)?.as_value()? {
let (stops, gradient_spread) = match fill_node.input(fill::PaintInput)?.as_value()? {
TaggedValue::GradientRamp(ramp) => (Gradient::from(ramp), ramp.gradient_spread),
_ => return None,
};
@@ -2306,7 +2306,7 @@ mod test_gradient {
editor
.handle_message(NodeGraphMessage::CreateWire {
output_connector: OutputConnector::primary_output(gradient_node_id),
input_connector: InputConnector::node(fill_node_id, fill::FillInput),
input_connector: InputConnector::node(fill_node_id, fill::PaintInput),
})
.await;
@@ -3033,7 +3033,7 @@ mod test_gradient {
editor
.handle_message(NodeGraphMessage::CreateWire {
output_connector: OutputConnector::primary_output(gradient_value_id),
input_connector: InputConnector::node(fill_node_id, graphene_std::vector::fill::FillInput),
input_connector: InputConnector::node(fill_node_id, graphene_std::vector::fill::PaintInput),
})
.await;
editor