mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 14:18:04 +08:00
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:
2
demo-artwork/changing-seasons.graphite
generated
2
demo-artwork/changing-seasons.graphite
generated
File diff suppressed because one or more lines are too long
2
demo-artwork/isometric-fountain.graphite
generated
2
demo-artwork/isometric-fountain.graphite
generated
File diff suppressed because one or more lines are too long
2
demo-artwork/marbled-mandelbrot.graphite
generated
2
demo-artwork/marbled-mandelbrot.graphite
generated
File diff suppressed because one or more lines are too long
2
demo-artwork/painted-dreams.graphite
generated
2
demo-artwork/painted-dreams.graphite
generated
File diff suppressed because one or more lines are too long
2
demo-artwork/parametric-dunescape.graphite
generated
2
demo-artwork/parametric-dunescape.graphite
generated
File diff suppressed because one or more lines are too long
2
demo-artwork/procedural-string-lights.graphite
generated
2
demo-artwork/procedural-string-lights.graphite
generated
File diff suppressed because one or more lines are too long
2
demo-artwork/red-dress.graphite
generated
2
demo-artwork/red-dress.graphite
generated
File diff suppressed because one or more lines are too long
2
demo-artwork/valley-of-spires.graphite
generated
2
demo-artwork/valley-of-spires.graphite
generated
File diff suppressed because one or more lines are too long
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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),
|
||||
];
|
||||
|
||||
|
||||
@@ -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:?}");
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -376,7 +376,8 @@ fn position_value_raises_into_the_into_group_reducer() {
|
||||
assert_eq!(anchors.len(), 1, "The single position should group as one anchor point");
|
||||
}
|
||||
|
||||
// The 'Colors to Gradient' node turns an entire `List<Color>` wire into one gradient with those colors as its stops
|
||||
// The 'Colors to Gradient' node turns an entire color wire into one gradient with those colors as its stops,
|
||||
// reaching its `List<Graphic>` connector through the embedding adapter
|
||||
#[test]
|
||||
fn color_list_wraps_through_the_colors_to_gradient_node() {
|
||||
let color_node = ProtoNode::value(ConstructionArgs::Value(TaggedValue::Color(graphene_std::Color::WHITE).into()), vec![NodeId(0)]);
|
||||
@@ -384,20 +385,23 @@ fn color_list_wraps_through_the_colors_to_gradient_node() {
|
||||
let mut raise_node = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(0)]), vec![NodeId(1)]);
|
||||
raise_node.identifier = ProtoNodeIdentifier::new("graphene_core::ops::ItemToListNode<Color>");
|
||||
|
||||
let mut colors_to_gradient_node = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(1)]), vec![NodeId(2)]);
|
||||
let mut graphic_adapter = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(1)]), vec![NodeId(2)]);
|
||||
graphic_adapter.identifier = ProtoNodeIdentifier::new("input_adapter<Graphic>");
|
||||
|
||||
let mut colors_to_gradient_node = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(2)]), vec![NodeId(3)]);
|
||||
colors_to_gradient_node.identifier = ProtoNodeIdentifier::new("graphic_nodes::graphic::ColorsToGradientNode");
|
||||
|
||||
let network = ProtoNetwork {
|
||||
inputs: vec![],
|
||||
output: NodeId(2),
|
||||
nodes: vec![(NodeId(0), color_node), (NodeId(1), raise_node), (NodeId(2), colors_to_gradient_node)],
|
||||
output: NodeId(3),
|
||||
nodes: vec![(NodeId(0), color_node), (NodeId(1), raise_node), (NodeId(2), graphic_adapter), (NodeId(3), colors_to_gradient_node)],
|
||||
};
|
||||
let mut typing_context = TypingContext::new(&crate::node_registry::NODE_REGISTRY);
|
||||
typing_context.update(&network).expect("A List<Color> wire should resolve the node's List<Color> implementation");
|
||||
typing_context.update(&network).expect("A List<Color> wire should embed into the node's List<Graphic> connector");
|
||||
let tree = futures::executor::block_on(BorrowTree::new(network, &typing_context)).expect("The node constructor should instantiate");
|
||||
|
||||
let context: Context = None;
|
||||
let result: Option<Item<graphene_std::vector::Gradient>> = futures::executor::block_on(tree.eval(NodeId(2), context));
|
||||
let result: Option<Item<graphene_std::vector::Gradient>> = futures::executor::block_on(tree.eval(NodeId(3), context));
|
||||
let gradient = result.expect("The color list should arrive wrapped as a gradient");
|
||||
assert_eq!(gradient.element().len(), 1, "The single color should become the gradient's one stop");
|
||||
}
|
||||
|
||||
@@ -3,12 +3,9 @@
|
||||
//! while cover-specific data rides the inner `Item<Cover>`, reusing `ATTR_TRANSFORM` for the stroke-authoring space.
|
||||
|
||||
use crate::graphic::Graphic;
|
||||
use core_types::Color;
|
||||
use core_types::graphene_hash::CacheHash;
|
||||
use core_types::list::{ATTR_ALIGN, ATTR_APPEARANCE, ATTR_CAP, ATTR_DASH_OFFSET, ATTR_DASH_PATTERN, ATTR_JOIN, ATTR_JOIN_MITER_LIMIT, ATTR_PAINT, ATTR_TRANSFORM, ATTR_WEIGHT, Item, List};
|
||||
use raster_types::{CPU, GPU, Raster};
|
||||
use vector_types::vector::style::{DashPattern, Stroke};
|
||||
use vector_types::{Gradient, Vector};
|
||||
|
||||
/// The geometry-to-region operator a coverage applies before painting:
|
||||
/// the interior of the geometry (fill) or the region swept along its outline (stroke).
|
||||
@@ -275,95 +272,10 @@ pub fn stamp_coverage<T>(item: &mut Item<T>, coverage: Coverage, paint: Graphic,
|
||||
item.attribute_mut_or_insert_default::<Appearance>(ATTR_APPEARANCE).replace_or_insert(coverage, paint, placement);
|
||||
}
|
||||
|
||||
// ================
|
||||
// TRAIT: IntoPaint
|
||||
// ================
|
||||
|
||||
/// Converts the types accepted by a paint input into the canonical `Graphic` stored in the `ATTR_PAINT` attribute.
|
||||
/// `List<Graphic>` deliberately has no impl: a multi-element paint is a type error.
|
||||
pub trait IntoPaint: Clone + Send + Sync + Default + std::fmt::Debug + PartialEq + CacheHash + 'static {
|
||||
fn into_paint(self) -> Graphic;
|
||||
}
|
||||
|
||||
impl IntoPaint for Item<Graphic> {
|
||||
fn into_paint(self) -> Graphic {
|
||||
// Wrapping to keep the record's attributes would nest the paint as a group, changing how it renders
|
||||
self.into_element()
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoPaint for Item<Vector> {
|
||||
fn into_paint(self) -> Graphic {
|
||||
Graphic::VectorList(List::new_from_item(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoPaint for Item<Raster<CPU>> {
|
||||
fn into_paint(self) -> Graphic {
|
||||
Graphic::RasterCPUList(List::new_from_item(self))
|
||||
}
|
||||
}
|
||||
|
||||
// No Item<Raster<GPU>> impl: GPU rasters have no Default, which the trait bounds require of the element
|
||||
|
||||
impl IntoPaint for Item<Color> {
|
||||
fn into_paint(self) -> Graphic {
|
||||
Graphic::ColorList(List::new_from_item(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoPaint for Item<Gradient> {
|
||||
fn into_paint(self) -> Graphic {
|
||||
Graphic::GradientList(List::new_from_item(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoPaint for Item<String> {
|
||||
fn into_paint(self) -> Graphic {
|
||||
Graphic::TextList(List::new_from_item(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoPaint for List<Vector> {
|
||||
fn into_paint(self) -> Graphic {
|
||||
Graphic::VectorList(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoPaint for List<Raster<CPU>> {
|
||||
fn into_paint(self) -> Graphic {
|
||||
Graphic::RasterCPUList(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoPaint for List<Raster<GPU>> {
|
||||
fn into_paint(self) -> Graphic {
|
||||
Graphic::RasterGPUList(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoPaint for List<Color> {
|
||||
fn into_paint(self) -> Graphic {
|
||||
Graphic::ColorList(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoPaint for List<Gradient> {
|
||||
fn into_paint(self) -> Graphic {
|
||||
Graphic::GradientList(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoPaint for List<String> {
|
||||
fn into_paint(self) -> Graphic {
|
||||
Graphic::TextList(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use core_types::list::ATTR_POSITION;
|
||||
use core_types::Color;
|
||||
use glam::{DAffine2, DVec2};
|
||||
use vector_types::vector::style::{StrokeAlign, StrokeCap, StrokeJoin};
|
||||
|
||||
@@ -470,24 +382,4 @@ mod tests {
|
||||
appearance.replace_or_insert(Coverage::new_fill(), solid_paint(Color::RED), CoverPlacement::Above);
|
||||
assert!(appearance.has_painted_cover(Cover::Fill));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_paint_becomes_one_graphic_holding_every_element() {
|
||||
let mut colors = List::new_from_element(Color::RED);
|
||||
colors.push(Item::new_from_element(Color::BLUE));
|
||||
|
||||
let paint = colors.into_paint();
|
||||
let Graphic::ColorList(inner) = &paint else { panic!("expected a color graphic") };
|
||||
assert_eq!(inner.len(), 2, "a list paint is one graphic holding all its elements");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn item_paint_keeps_its_attributes_on_the_inner_row() {
|
||||
let color = Item::new_from_element(Color::RED).with_attribute(ATTR_POSITION, 0.25_f64);
|
||||
|
||||
let paint = color.into_paint();
|
||||
let Graphic::ColorList(inner) = &paint else { panic!("expected a color graphic") };
|
||||
assert_eq!(inner.len(), 1);
|
||||
assert_eq!(inner.attribute::<f64>(ATTR_POSITION, 0), Some(&0.25));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -950,12 +950,8 @@ mod tests {
|
||||
assert!(!graphic_list.attribute_keys().any(|key| key == ATTR_EDITOR_LAYER_PATH));
|
||||
}
|
||||
|
||||
// Round-tripping through that wrapper must not collapse the items' distinct stamps onto item 0's
|
||||
#[test]
|
||||
fn round_trip_through_the_wrapper_preserves_per_item_layer_paths() {
|
||||
let flattened: List<Vector> = vector_list_stamped_with_layers([7, 9]).into_flattened_list();
|
||||
|
||||
let layers = (0..flattened.len())
|
||||
fn layer_stamps(flattened: &List<Vector>) -> Vec<Option<NodeId>> {
|
||||
(0..flattened.len())
|
||||
.map(|index| {
|
||||
flattened
|
||||
.attribute_cloned_or_default::<NodeIdPath>(ATTR_EDITOR_LAYER_PATH, index)
|
||||
@@ -964,9 +960,25 @@ mod tests {
|
||||
.next_back()
|
||||
.copied()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
.collect()
|
||||
}
|
||||
|
||||
assert_eq!(layers, [Some(NodeId(7)), Some(NodeId(9))]);
|
||||
// Round-tripping through that wrapper must not collapse the items' distinct stamps onto item 0's
|
||||
#[test]
|
||||
fn round_trip_through_the_wrapper_preserves_per_item_layer_paths() {
|
||||
let flattened: List<Vector> = vector_list_stamped_with_layers([7, 9]).into_flattened_list();
|
||||
|
||||
assert_eq!(layer_stamps(&flattened), [Some(NodeId(7)), Some(NodeId(9))]);
|
||||
}
|
||||
|
||||
// The embedding adapter reaches the same flattened stamps as the wrapper, each item carrying its own inside its variant
|
||||
#[test]
|
||||
fn embedding_each_item_preserves_per_item_layer_paths() {
|
||||
let embedded: List<Graphic> = vector_list_stamped_with_layers([7, 9]).into_iter().map(|item| Item::new_from_element(Graphic::from(item))).collect();
|
||||
|
||||
let flattened: List<Vector> = embedded.into_flattened_list();
|
||||
|
||||
assert_eq!(layer_stamps(&flattened), [Some(NodeId(7)), Some(NodeId(9))]);
|
||||
}
|
||||
|
||||
// Flattening must not invent attributes that neither the parent graphic nor the child carried
|
||||
|
||||
@@ -8,7 +8,7 @@ pub use raster_types;
|
||||
pub use vector_types;
|
||||
|
||||
// Re-export commonly used types at the crate root
|
||||
pub use appearance::{Appearance, Cover, CoverPlacement, Coverage, FillAndStroke, IntoPaint, stamp_coverage};
|
||||
pub use appearance::{Appearance, Cover, CoverPlacement, Coverage, FillAndStroke, stamp_coverage};
|
||||
pub use artboard::Artboard;
|
||||
pub use graphic::{Graphic, IntoGraphicList, TryFromGraphic, Vector};
|
||||
|
||||
|
||||
@@ -1002,9 +1002,8 @@ pub async fn flatten_graphic(_: impl Ctx, content: List<Graphic>, fully_flatten:
|
||||
|
||||
/// Converts a `Graphic[]` into a `Vector[]` by deeply flattening any vector content it contains, and discarding any non-vector content.
|
||||
#[node_macro::node(category("Vector"))]
|
||||
pub async fn flatten_vector<T: IntoGraphicList>(_: impl Ctx, #[implementations(List<Graphic>, List<Vector>)] content: T) -> List<Vector> {
|
||||
let graphic_list = content.into_graphic_list();
|
||||
let mut output: List<Vector> = graphic_list.clone().into_flattened_list();
|
||||
pub async fn flatten_vector(_: impl Ctx, content: List<Graphic>) -> List<Vector> {
|
||||
let mut output: List<Vector> = content.clone().into_flattened_list();
|
||||
|
||||
// TODO: Replace this snapshot hack with per-layer metadata driven by each layer's Monitor node.
|
||||
// TODO: Flattening here erases the upstream `List<Graphic>` hierarchy that editor metadata collection walks
|
||||
@@ -1014,20 +1013,20 @@ pub async fn flatten_vector<T: IntoGraphicList>(_: impl Ctx, #[implementations(L
|
||||
// TODO: The cleaner fix is to drive each layer's metadata from its own Monitor's captured `(Context, List<Graphic>)`,
|
||||
// TODO: at which point this attribute (and the equivalents in Boolean Operation, Solidify Stroke, Combine Paths,
|
||||
// TODO: Morph, Rasterize) become unnecessary.
|
||||
if !output.is_empty() && !is_lone_anonymous_leaf(&graphic_list) {
|
||||
if !output.is_empty() && !is_lone_anonymous_leaf(&content) {
|
||||
// Item 0 carries a composed transform inherited from the flattened input, but the merged_layers
|
||||
// already holds the original transforms; pre-compensate by item 0's inverse so the renderer's
|
||||
// `upstream_footprint *= item_0_transform` recursion cancels out and leaves the originals intact.
|
||||
let mut graphic_list = graphic_list;
|
||||
let mut merged_layers = content;
|
||||
let item_0_transform: DAffine2 = output.attribute_cloned_or_default(ATTR_TRANSFORM, 0);
|
||||
if item_0_transform.matrix2.determinant().abs() > f64::EPSILON {
|
||||
let inverse = item_0_transform.inverse();
|
||||
for transform in graphic_list.iter_attribute_values_mut_or_default::<DAffine2>(ATTR_TRANSFORM) {
|
||||
for transform in merged_layers.iter_attribute_values_mut_or_default::<DAffine2>(ATTR_TRANSFORM) {
|
||||
*transform = inverse * *transform;
|
||||
}
|
||||
}
|
||||
|
||||
output.set_attribute(ATTR_EDITOR_MERGED_LAYERS, 0, graphic_list);
|
||||
output.set_attribute(ATTR_EDITOR_MERGED_LAYERS, 0, merged_layers);
|
||||
}
|
||||
|
||||
output
|
||||
@@ -1035,25 +1034,25 @@ pub async fn flatten_vector<T: IntoGraphicList>(_: impl Ctx, #[implementations(L
|
||||
|
||||
/// Converts a `Graphic[]` into a `Raster[]` by deeply flattening any raster content it contains, and discarding any non-raster content.
|
||||
#[node_macro::node(category("Raster"))]
|
||||
pub async fn flatten_raster<T: IntoGraphicList>(_: impl Ctx, #[implementations(List<Graphic>, List<Raster<CPU>>)] content: T) -> List<Raster<CPU>> {
|
||||
pub async fn flatten_raster(_: impl Ctx, content: List<Graphic>) -> List<Raster<CPU>> {
|
||||
content.into_flattened_list()
|
||||
}
|
||||
|
||||
/// Converts a `Graphic[]` into a `Color[]` by deeply flattening any color content it contains, and discarding any non-color content.
|
||||
#[node_macro::node(category("General"))]
|
||||
pub async fn flatten_color<T: IntoGraphicList>(_: impl Ctx, #[implementations(List<Graphic>, List<Color>)] content: T) -> List<Color> {
|
||||
pub async fn flatten_color(_: impl Ctx, content: List<Graphic>) -> List<Color> {
|
||||
content.into_flattened_list()
|
||||
}
|
||||
|
||||
/// Converts a `Graphic[]` into a `Gradient[]` by deeply flattening any gradient content it contains, and discarding any non-gradient content.
|
||||
#[node_macro::node(category("General"))]
|
||||
pub async fn flatten_gradient<T: IntoGraphicList>(_: impl Ctx, #[implementations(List<Graphic>, List<Gradient>)] content: T) -> List<Gradient> {
|
||||
pub async fn flatten_gradient(_: impl Ctx, content: List<Graphic>) -> List<Gradient> {
|
||||
content.into_flattened_list()
|
||||
}
|
||||
|
||||
/// Constructs a gradient from a `Color[]`, where each color becomes a gradient stop. A `position` attribute on the colors places their stops along the ramp and a `midpoint` attribute skews each transition, while colors carrying neither are distributed evenly across the 0 to 1 range.
|
||||
#[node_macro::node(category("Gradient"), name("Colors to Gradient"))]
|
||||
fn colors_to_gradient<T: IntoGraphicList>(_: impl Ctx, #[implementations(List<Graphic>, List<Color>)] colors: T) -> Item<Gradient> {
|
||||
fn colors_to_gradient(_: impl Ctx, colors: List<Graphic>) -> Item<Gradient> {
|
||||
Item::new_from_element(Gradient::from(colors.into_flattened_list::<Color>()))
|
||||
}
|
||||
|
||||
@@ -1071,6 +1070,11 @@ mod test {
|
||||
elements.into_iter().map(Item::new_from_element).collect()
|
||||
}
|
||||
|
||||
/// Stands in for the embedding adapter a compiled graph inserts ahead of a `List<Graphic>` connector.
|
||||
fn embed_colors(colors: List<Color>) -> List<Graphic> {
|
||||
colors.into_iter().map(|item| Item::new_from_element(Graphic::from(item))).collect()
|
||||
}
|
||||
|
||||
fn elements<T: Clone>(list: &List<T>) -> Vec<T> {
|
||||
list.iter_element_values().cloned().collect()
|
||||
}
|
||||
@@ -1196,7 +1200,7 @@ mod test {
|
||||
let colors = gradient_to_colors((), Item::new_from_element(gradient.clone()));
|
||||
assert_eq!(elements(&colors), [Color::RED, Color::GREEN, Color::BLUE], "every stop should come out as its color");
|
||||
|
||||
let restored = colors_to_gradient((), colors);
|
||||
let restored = colors_to_gradient((), embed_colors(colors));
|
||||
assert_eq!(
|
||||
restored.element(),
|
||||
&gradient,
|
||||
@@ -1209,7 +1213,7 @@ mod test {
|
||||
let gradient = Gradient::from(vec![Color::RED, Color::GREEN, Color::BLUE]);
|
||||
assert!(!gradient.has_position_attribute(), "even spacing is stored as the attribute's absence");
|
||||
|
||||
let restored = colors_to_gradient((), gradient_to_colors((), Item::new_from_element(gradient.clone())));
|
||||
let restored = colors_to_gradient((), embed_colors(gradient_to_colors((), Item::new_from_element(gradient.clone()))));
|
||||
assert_eq!(restored.element(), &gradient, "a default ramp should round trip without gaining attributes it never had");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ pub use graphene_application_io as application_io;
|
||||
pub use graphene_core;
|
||||
pub use graphene_core::debug;
|
||||
pub use graphic_nodes;
|
||||
pub use graphic_types::{Appearance, Artboard, Cover, CoverPlacement, Coverage, Graphic, IntoPaint, Vector, stamp_coverage};
|
||||
pub use graphic_types::{Appearance, Artboard, Cover, CoverPlacement, Coverage, Graphic, Vector, stamp_coverage};
|
||||
pub use math_nodes;
|
||||
pub use path_bool_nodes;
|
||||
pub use raster_nodes;
|
||||
|
||||
@@ -11,7 +11,7 @@ use core_types::math::bbox::Bbox;
|
||||
use core_types::ops::Convert;
|
||||
use core_types::transform::Footprint;
|
||||
#[cfg(target_family = "wasm")]
|
||||
use core_types::{ATTR_EDITOR_MERGED_LAYERS, ATTR_TRANSFORM, WasmNotSend};
|
||||
use core_types::{ATTR_EDITOR_MERGED_LAYERS, ATTR_TRANSFORM};
|
||||
use core_types::{Color, Ctx};
|
||||
pub use graph_craft::application_io::resource::{Resource, ResourceHash};
|
||||
pub use graph_craft::application_io::*;
|
||||
@@ -20,15 +20,9 @@ pub use graph_craft::document::value::RenderOutputType;
|
||||
pub use graphene_canvas_utils as canvas_utils;
|
||||
#[cfg(target_family = "wasm")]
|
||||
use graphic_types::Graphic;
|
||||
#[cfg(target_family = "wasm")]
|
||||
use graphic_types::IntoGraphicList;
|
||||
#[cfg(target_family = "wasm")]
|
||||
use graphic_types::Vector;
|
||||
use graphic_types::raster_types::Image;
|
||||
use graphic_types::raster_types::{CPU, GPU, Raster};
|
||||
#[cfg(target_family = "wasm")]
|
||||
use graphic_types::vector_types::gradient::Gradient;
|
||||
#[cfg(target_family = "wasm")]
|
||||
use rendering::{Render, RenderParams, RenderSvgSegmentList, SvgRender};
|
||||
|
||||
fn parse_headers(headers: &str) -> reqwest::header::HeaderMap {
|
||||
@@ -202,22 +196,7 @@ async fn create_canvas(_: impl Ctx) -> Item<CanvasHandle> {
|
||||
/// Renders a view of the input graphic within an area defined by the *Footprint*.
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[node_macro::node(category(""))]
|
||||
async fn rasterize<T: WasmNotSend + Clone + 'n>(
|
||||
_: impl Ctx,
|
||||
#[implementations(
|
||||
List<Vector>,
|
||||
List<Raster<CPU>>,
|
||||
List<Graphic>,
|
||||
List<Color>,
|
||||
List<Gradient>,
|
||||
)]
|
||||
data: List<T>,
|
||||
footprint: Item<Footprint>,
|
||||
canvas: Item<CanvasHandle>,
|
||||
) -> List<Raster<CPU>>
|
||||
where
|
||||
List<T>: Render + Clone + graphic_types::IntoGraphicList,
|
||||
{
|
||||
async fn rasterize(_: impl Ctx, data: List<Graphic>, footprint: Item<Footprint>, canvas: Item<CanvasHandle>) -> List<Raster<CPU>> {
|
||||
let mut data = data;
|
||||
let mut canvas = canvas.into_element();
|
||||
use glam::{DAffine2, DVec2};
|
||||
@@ -231,7 +210,7 @@ where
|
||||
|
||||
// Snapshot the input as a List<Graphic> so the renderer can recurse into the original child layers
|
||||
// when collecting metadata, exposing their click targets to editor tools (same mechanism as Boolean Operation).
|
||||
let upstream_graphic_list = data.clone().into_graphic_list();
|
||||
let upstream_graphic_list = data.clone();
|
||||
|
||||
let mut render = SvgRender::new();
|
||||
let aabb = Bbox::from_transform(footprint.transform).to_axis_aligned_bbox();
|
||||
|
||||
@@ -19,11 +19,10 @@ pub use vector_types::vector::misc::BooleanOperation;
|
||||
|
||||
/// Combines the geometric forms of one or more closed paths into a new vector path that results from cutting or joining the paths by the chosen method.
|
||||
#[node_macro::node(category("Vector: Modifier"), memoize)]
|
||||
async fn boolean_operation<I: graphic_types::IntoGraphicList>(
|
||||
async fn boolean_operation(
|
||||
_: impl Ctx,
|
||||
/// The `List` of vector paths to perform the boolean operation on. Nested `List`s are automatically flattened.
|
||||
#[implementations(List<Graphic>, List<Vector>)]
|
||||
content: I,
|
||||
content: List<Graphic>,
|
||||
/// Which boolean operation to perform on the paths.
|
||||
///
|
||||
/// Union combines all paths while cutting out overlapping areas (even the interiors of a single path).
|
||||
@@ -33,7 +32,6 @@ async fn boolean_operation<I: graphic_types::IntoGraphicList>(
|
||||
operation: Item<BooleanOperation>,
|
||||
) -> Item<Vector> {
|
||||
let operation = operation.into_element();
|
||||
let content = content.into_graphic_list();
|
||||
|
||||
// The first index is the bottom of the stack
|
||||
let flattened = flatten_vector(&content);
|
||||
|
||||
@@ -15,7 +15,7 @@ use glam::{DAffine2, DMat2, DVec2};
|
||||
use graphic_types::Vector;
|
||||
use graphic_types::graphic::{bake_paint_transforms, is_paint_present};
|
||||
use graphic_types::raster_types::{CPU, GPU, Raster};
|
||||
use graphic_types::{Appearance, Cover, CoverPlacement, Coverage, Graphic, IntoGraphicList, IntoPaint, stamp_coverage};
|
||||
use graphic_types::{Appearance, Cover, CoverPlacement, Coverage, Graphic, IntoGraphicList, stamp_coverage};
|
||||
use kurbo::simplify::{SimplifyOptions, simplify_bezpath};
|
||||
use kurbo::{Affine, BezPath, DEFAULT_ACCURACY, Line, ParamCurve, ParamCurveArclen, PathEl, PathSeg, Shape};
|
||||
use rand::{Rng, SeedableRng};
|
||||
@@ -303,18 +303,12 @@ where
|
||||
|
||||
/// Applies a fill style to the vector content, giving an appearance to the area within the interior of the geometry.
|
||||
#[node_macro::node(category("Vector: Style"), path(graphene_core::vector), properties("fill_properties"))]
|
||||
async fn fill<V, F: IntoPaint + 'n + Send + 'static>(
|
||||
async fn fill<V>(
|
||||
_: impl Ctx,
|
||||
/// The content with vector paths to apply the fill style to.
|
||||
#[implementations(Vector, Vector, Vector, Vector, Vector, Vector, Graphic, Graphic, Graphic, Graphic, Graphic, Graphic)]
|
||||
#[implementations(Vector, Graphic)]
|
||||
content: Item<V>,
|
||||
/// The fill to paint the path with.
|
||||
#[default(Color::BLACK)]
|
||||
#[implementations(
|
||||
Item<Graphic>, List<Vector>, List<Color>, List<Gradient>, List<Raster<CPU>>, List<Raster<GPU>>,
|
||||
Item<Graphic>, List<Vector>, List<Color>, List<Gradient>, List<Raster<CPU>>, List<Raster<GPU>>,
|
||||
)]
|
||||
fill: F,
|
||||
#[default(Color::BLACK)] paint: Item<Graphic>,
|
||||
_backup_color: Item<Color>,
|
||||
#[default(Color::BLACK, Color::WHITE)] _backup_gradient: Item<Gradient>,
|
||||
_gradient_form: Item<GradientForm>,
|
||||
@@ -328,21 +322,25 @@ where
|
||||
let (_has_transform, _transform) = (_has_transform.into_element(), *_transform.element());
|
||||
|
||||
let mut content = content;
|
||||
let mut fill = fill.into_paint();
|
||||
// The paint is the element alone: keeping the wire envelope's attributes would nest the paint as a group, changing how it renders
|
||||
let mut paint = paint.into_element();
|
||||
|
||||
// Stamp the gradient styling inputs onto any gradient paint missing them, whether the paint arrived as a picker value or a wire
|
||||
if let Graphic::GradientList(gradient) = &mut fill {
|
||||
if gradient.iter_attribute_values::<GradientForm>(ATTR_GRADIENT_FORM).is_none() {
|
||||
for value in gradient.iter_attribute_values_mut_or_default::<GradientForm>(ATTR_GRADIENT_FORM) {
|
||||
*value = _gradient_form;
|
||||
}
|
||||
let (needs_form, needs_transform) = match &paint {
|
||||
Graphic::Gradient(item) => (item.attribute::<GradientForm>(ATTR_GRADIENT_FORM).is_none(), item.attribute::<DAffine2>(ATTR_TRANSFORM).is_none()),
|
||||
Graphic::GradientList(list) => (
|
||||
list.iter_attribute_values::<GradientForm>(ATTR_GRADIENT_FORM).is_none(),
|
||||
list.iter_attribute_values::<DAffine2>(ATTR_TRANSFORM).is_none(),
|
||||
),
|
||||
_ => (false, false),
|
||||
};
|
||||
|
||||
let stamped_transform = needs_transform.then(|| {
|
||||
// Without an explicit placement, derive one covering the paint target's bounding box (the CSS `auto` behavior)
|
||||
if _has_transform {
|
||||
return _transform;
|
||||
}
|
||||
|
||||
if gradient.iter_attribute_values::<DAffine2>(ATTR_TRANSFORM).is_none() {
|
||||
// Without an explicit placement, derive one covering the paint target's bounding box (the CSS `auto` behavior)
|
||||
let transform = if _has_transform {
|
||||
_transform
|
||||
} else {
|
||||
let mut bounds: Option<[DVec2; 2]> = None;
|
||||
content.for_each_vector_mut(|vector, _| {
|
||||
if let Some([min, max]) = vector.bounding_box() {
|
||||
@@ -362,33 +360,47 @@ where
|
||||
max.y = min.y + 1.;
|
||||
}
|
||||
initial_gradient_transform_for_bounding_box([min, max])
|
||||
};
|
||||
});
|
||||
|
||||
for value in gradient.iter_attribute_values_mut_or_default::<DAffine2>(ATTR_TRANSFORM) {
|
||||
match &mut paint {
|
||||
Graphic::Gradient(item) => {
|
||||
if needs_form {
|
||||
item.set_attribute(ATTR_GRADIENT_FORM, _gradient_form);
|
||||
}
|
||||
if let Some(transform) = stamped_transform {
|
||||
item.set_attribute(ATTR_TRANSFORM, transform);
|
||||
}
|
||||
}
|
||||
Graphic::GradientList(list) => {
|
||||
if needs_form {
|
||||
for value in list.iter_attribute_values_mut_or_default::<GradientForm>(ATTR_GRADIENT_FORM) {
|
||||
*value = _gradient_form;
|
||||
}
|
||||
}
|
||||
if let Some(transform) = stamped_transform {
|
||||
for value in list.iter_attribute_values_mut_or_default::<DAffine2>(ATTR_TRANSFORM) {
|
||||
*value = transform;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Appending follows the painter's algorithm: the most downstream paint node in the chain paints on top
|
||||
stamp_coverage(&mut content, Coverage::new_fill(), fill, CoverPlacement::Above);
|
||||
stamp_coverage(&mut content, Coverage::new_fill(), paint, CoverPlacement::Above);
|
||||
content
|
||||
}
|
||||
|
||||
/// Applies a stroke style to the vector content, giving an appearance to the area within the outline of the geometry.
|
||||
#[node_macro::node(category("Vector: Style"), path(graphene_core::vector), properties("stroke_properties"))]
|
||||
async fn stroke<V, P: IntoPaint + 'n + Send + 'static>(
|
||||
async fn stroke<V>(
|
||||
_: impl Ctx,
|
||||
/// The content with vector paths to apply the stroke style to.
|
||||
#[implementations(Vector, Vector, Vector, Vector, Vector, Vector, Graphic, Graphic, Graphic, Graphic, Graphic, Graphic)]
|
||||
#[implementations(Vector, Graphic)]
|
||||
content: Item<V>,
|
||||
/// The stroke paint.
|
||||
#[default(Color::BLACK)]
|
||||
#[implementations(
|
||||
Item<Graphic>, List<Vector>, List<Color>, List<Gradient>, List<Raster<CPU>>, List<Raster<GPU>>,
|
||||
Item<Graphic>, List<Vector>, List<Color>, List<Gradient>, List<Raster<CPU>>, List<Raster<GPU>>,
|
||||
)]
|
||||
paint: P,
|
||||
paint: Item<Graphic>,
|
||||
/// The stroke thickness.
|
||||
#[unit(" px")]
|
||||
#[default(2.)]
|
||||
@@ -433,7 +445,8 @@ where
|
||||
transform: DAffine2::IDENTITY,
|
||||
};
|
||||
|
||||
let paint = paint.into_paint();
|
||||
// The wire envelope is dropped for the same reason as in `fill` above
|
||||
let paint = paint.into_element();
|
||||
|
||||
// The coverage records the stroke's authoring space, so the item transform is composed in. Its translation
|
||||
// cancels out in every consumer, so it is cleared to let an otherwise-identity capture elide.
|
||||
@@ -1240,7 +1253,6 @@ async fn auto_tangents<V: MapVectorItems + 'n + Send>(
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: After the Graphic lowering refactor, measure a group as one enclosing box instead of one box per shape
|
||||
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
|
||||
async fn bounding_box<V: MapVectorItems + 'n + Send>(_: impl Ctx, #[implementations(Graphic, Vector)] content: Item<V>) -> Item<V> {
|
||||
V::map_vector_items(content, |content| {
|
||||
@@ -1260,7 +1272,6 @@ async fn bounding_box<V: MapVectorItems + 'n + Send>(_: impl Ctx, #[implementati
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: Accept graphic input once the Graphic lowering refactor gives group leaves a single Vector to measure
|
||||
#[node_macro::node(category("Vector: Measure"), path(core_types::vector))]
|
||||
async fn dimensions(_: impl Ctx, content: Item<Vector>) -> Item<DVec2> {
|
||||
let dimensions = content
|
||||
@@ -1714,7 +1725,6 @@ async fn separate_subpaths<V: ExpandVectorItems + 'n + Send>(_: impl Ctx, #[impl
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: Accept graphic input once the Graphic lowering refactor gives group leaves a single Vector to measure
|
||||
/// Determines if the subpath at the given index is closed, meaning its ends are connected together forming a loop.
|
||||
#[node_macro::node(name("Path is Closed"), category("Vector: Measure"), path(core_types::vector))]
|
||||
async fn path_is_closed(
|
||||
@@ -1751,9 +1761,9 @@ async fn map_points<V: MapVectorItems + 'n + Send>(
|
||||
|
||||
/// Combines every vector path across the input into a single compound path.
|
||||
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
|
||||
pub async fn combine_paths<T: IntoGraphicList>(_: impl Ctx, #[implementations(List<Graphic>, List<Vector>)] content: T) -> Item<Vector> {
|
||||
let graphic_list = content.into_graphic_list();
|
||||
let flattened = graphic_list.clone().into_flattened_list::<Vector>();
|
||||
pub async fn combine_paths(_: impl Ctx, content: List<Graphic>) -> Item<Vector> {
|
||||
let graphic_list = content.clone();
|
||||
let flattened = content.into_flattened_list::<Vector>();
|
||||
|
||||
// Create a `List` with one empty `Vector` element, then get a mutable reference to it which we append flattened subpaths to
|
||||
let mut output_list = List::new_from_element(Vector::default());
|
||||
@@ -2154,7 +2164,6 @@ async fn cut_segments<V: MapVectorItems + 'n + Send>(_: impl Ctx, #[implementati
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: Accept graphic input once the Graphic lowering refactor gives group leaves a single Vector to measure
|
||||
/// Determines the position of a point on the path, given by its progression from 0 to 1 along the path.
|
||||
///
|
||||
/// If multiple subpaths make up the path, the whole number part of the progression value selects the subpath and the decimal part determines the position along it.
|
||||
@@ -2192,7 +2201,6 @@ async fn position_on_path(
|
||||
Item::new_from_element(position)
|
||||
}
|
||||
|
||||
// TODO: Accept graphic input once the Graphic lowering refactor gives group leaves a single Vector to measure
|
||||
/// Determines the angle of the tangent at a point on the path, given by its progression from 0 to 1 along the path.
|
||||
///
|
||||
/// If multiple subpaths make up the path, the whole number part of the progression value selects the subpath and the decimal part determines the position along it.
|
||||
@@ -2483,11 +2491,10 @@ async fn offset_points<V: MapVectorItems + 'n + Send>(
|
||||
///
|
||||
/// *Progression* morphs through all objects. Interpolation is linear unless *Path* geometry is provided to control the trajectory between key objects. The **Origins to Polyline** node may be used to create a path with anchor points corresponding to each object. Other nodes can modify its path segments.
|
||||
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
|
||||
async fn morph<I: IntoGraphicList>(
|
||||
async fn morph(
|
||||
_: impl Ctx,
|
||||
/// The vector objects to interpolate between. Mixed graphic content is deeply flattened to keep only vector elements.
|
||||
#[implementations(List<Graphic>, List<Vector>)]
|
||||
content: I,
|
||||
content: List<Graphic>,
|
||||
/// The fractional part `[0, 1)` traverses the morph uniformly along the path. If the control path has multiple subpaths, each added integer selects the next subpath.
|
||||
progression: Item<Progression>,
|
||||
/// Swap the direction of the progression between objects or along the control path.
|
||||
@@ -2736,9 +2743,9 @@ async fn morph<I: IntoGraphicList>(
|
||||
let (progression, reverse, distribution) = (progression.into_element(), reverse.into_element(), distribution.into_element());
|
||||
|
||||
// Preserve original `List<Graphic>` as upstream data so this group layer's nested layers can be edited by the tools.
|
||||
let mut graphic_list_content = content.clone().into_graphic_list();
|
||||
let mut graphic_list_content = content.clone();
|
||||
|
||||
// If the input isn't a List<Vector>, we convert it into one by flattening any List<Graphic> content.
|
||||
// Only vector content can interpolate, so the rest is discarded by flattening.
|
||||
let content = content.into_flattened_list::<Vector>();
|
||||
|
||||
// Not enough elements to interpolate between, so we return the input as-is
|
||||
@@ -3459,7 +3466,6 @@ fn close_path<V: MapVectorItems + Send + Sync + 'static>(_: impl Ctx, #[implemen
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: Accept graphic input once the Graphic lowering refactor gives group leaves a single Vector to measure
|
||||
#[node_macro::node(category("Vector: Measure"), path(core_types::vector))]
|
||||
fn point_inside(_: impl Ctx, source: Item<Vector>, point: Item<DVec2>) -> Item<bool> {
|
||||
let point = point.into_element();
|
||||
@@ -3476,7 +3482,6 @@ async fn list_length(_: impl Ctx, content: ListDyn) -> Item<f64> {
|
||||
Item::new_from_element(content.len() as f64)
|
||||
}
|
||||
|
||||
// TODO: Accept graphic input once the Graphic lowering refactor gives group leaves a single Vector to measure
|
||||
#[node_macro::node(category("Vector: Measure"), path(graphene_core::vector))]
|
||||
async fn count_points(_: impl Ctx, content: Item<Vector>) -> Item<f64> {
|
||||
let count = content.element().point_domain.positions().len() as f64;
|
||||
@@ -3484,7 +3489,6 @@ async fn count_points(_: impl Ctx, content: Item<Vector>) -> Item<f64> {
|
||||
Item::new_from_element(count)
|
||||
}
|
||||
|
||||
// TODO: Accept graphic input once the Graphic lowering refactor gives group leaves a single Vector to measure
|
||||
/// Retrieves the vec2 position (in local space) of the anchor point at the specified index within a vector element.
|
||||
/// If no value exists at that index, the position (0, 0) is returned.
|
||||
#[node_macro::node(category("Vector: Measure"), path(graphene_core::vector))]
|
||||
@@ -3513,7 +3517,6 @@ async fn index_points(
|
||||
Item::new_from_element(positions[index])
|
||||
}
|
||||
|
||||
// TODO: Accept graphic input once the Graphic lowering refactor gives group leaves a single Vector to measure
|
||||
#[node_macro::node(category("Vector: Measure"), path(core_types::vector))]
|
||||
async fn path_length(_: impl Ctx, source: Item<Vector>) -> Item<f64> {
|
||||
let transform: DAffine2 = source.attribute_cloned_or_default(ATTR_TRANSFORM);
|
||||
@@ -3529,7 +3532,6 @@ async fn path_length(_: impl Ctx, source: Item<Vector>) -> Item<f64> {
|
||||
Item::new_from_element(length)
|
||||
}
|
||||
|
||||
// TODO: Accept graphic input once the Graphic lowering refactor gives group leaves a single Vector to measure
|
||||
#[node_macro::node(category("Vector: Measure"), path(core_types::vector))]
|
||||
async fn area(ctx: impl Ctx + CloneVarArgs + ExtractAll, content: impl Node<Context<'static>, Output = Item<Vector>>) -> Item<f64> {
|
||||
let new_ctx = OwnedContextImpl::from(ctx).with_footprint(Footprint::default()).into_context();
|
||||
@@ -3542,7 +3544,6 @@ async fn area(ctx: impl Ctx + CloneVarArgs + ExtractAll, content: impl Node<Cont
|
||||
Item::new_from_element(area)
|
||||
}
|
||||
|
||||
// TODO: Accept graphic input once the Graphic lowering refactor gives group leaves a single Vector to measure
|
||||
#[node_macro::node(category("Vector: Measure"), path(core_types::vector))]
|
||||
async fn centroid(ctx: impl Ctx + CloneVarArgs + ExtractAll, content: impl Node<Context<'static>, Output = Item<Vector>>, centroid_type: Item<CentroidType>) -> Item<DVec2> {
|
||||
let centroid_type = centroid_type.into_element();
|
||||
@@ -3609,6 +3610,11 @@ mod test {
|
||||
List::new_from_element(Vector::from_bezpath(bezpath))
|
||||
}
|
||||
|
||||
/// Stands in for the embedding adapter a compiled graph inserts ahead of a `List<Graphic>` connector.
|
||||
fn embed_vectors(vectors: List<Vector>) -> List<Graphic> {
|
||||
vectors.into_iter().map(|item| Item::new_from_element(Graphic::from(item))).collect()
|
||||
}
|
||||
|
||||
fn vector_item_from_bezpath(bezpath: BezPath) -> Item<Vector> {
|
||||
Item::new_from_element(Vector::from_bezpath(bezpath))
|
||||
}
|
||||
@@ -3923,7 +3929,7 @@ mod test {
|
||||
|
||||
let morphed = super::morph(
|
||||
Footprint::default(),
|
||||
rectangles,
|
||||
embed_vectors(rectangles),
|
||||
Item::new_from_element(0.5),
|
||||
Item::new_from_element(false),
|
||||
Item::new_from_element(InterpolationDistribution::default()),
|
||||
@@ -3949,7 +3955,7 @@ mod test {
|
||||
v
|
||||
};
|
||||
|
||||
let solid_fill = |color: Color| Appearance::new_single(Coverage::new_fill(), List::new_from_element(color).into_paint());
|
||||
let solid_fill = |color: Color| Appearance::new_single(Coverage::new_fill(), Graphic::from(List::new_from_element(color)));
|
||||
let item_a = Item::new_from_element(rect())
|
||||
.with_attribute(ATTR_TRANSFORM, DAffine2::IDENTITY)
|
||||
.with_attribute(ATTR_APPEARANCE, solid_fill(Color::RED));
|
||||
@@ -3962,7 +3968,7 @@ mod test {
|
||||
|
||||
let morphed = super::morph(
|
||||
Footprint::default(),
|
||||
content,
|
||||
embed_vectors(content),
|
||||
Item::new_from_element(0.5),
|
||||
Item::new_from_element(false),
|
||||
Item::new_from_element(InterpolationDistribution::default()),
|
||||
@@ -4001,8 +4007,8 @@ mod test {
|
||||
// The two endpoints list their covers in opposite paint orders, which pairing by position would cross
|
||||
let appearance = |fill: Color, stroke: Color, stroke_placement| {
|
||||
let mut appearance = Appearance::default();
|
||||
appearance.replace_or_insert(Coverage::new_fill(), List::new_from_element(fill).into_paint(), CoverPlacement::Above);
|
||||
appearance.replace_or_insert(Coverage::new_stroke(&Stroke::new(4.)), List::new_from_element(stroke).into_paint(), stroke_placement);
|
||||
appearance.replace_or_insert(Coverage::new_fill(), Graphic::from(List::new_from_element(fill)), CoverPlacement::Above);
|
||||
appearance.replace_or_insert(Coverage::new_stroke(&Stroke::new(4.)), Graphic::from(List::new_from_element(stroke)), stroke_placement);
|
||||
appearance
|
||||
};
|
||||
|
||||
@@ -4018,7 +4024,7 @@ mod test {
|
||||
|
||||
let morphed = super::morph(
|
||||
Footprint::default(),
|
||||
content,
|
||||
embed_vectors(content),
|
||||
Item::new_from_element(0.5),
|
||||
Item::new_from_element(false),
|
||||
Item::new_from_element(InterpolationDistribution::default()),
|
||||
|
||||
@@ -400,7 +400,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fill_paint_color_default_parses_against_its_list_wire() {
|
||||
fn fill_paint_color_default_parses_against_its_graphic_wire() {
|
||||
let node_registry = core_types::registry::NODE_REGISTRY.lock().unwrap();
|
||||
let metadata_registry = core_types::registry::NODE_METADATA.lock().unwrap();
|
||||
|
||||
@@ -414,7 +414,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
*paint,
|
||||
TaggedValue::Color(Color::BLACK),
|
||||
"The paint input's `Color::BLACK` default should parse against its `List<Graphic>` wire type"
|
||||
"The paint input's `Color::BLACK` default should parse against its `Item<Graphic>` wire type"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user