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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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

View File

@@ -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");
}

View File

@@ -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));
}
}

View File

@@ -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

View File

@@ -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};

View File

@@ -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");
}
}

View File

@@ -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;

View File

@@ -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();

View File

@@ -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);

View File

@@ -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,67 +322,85 @@ 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() {
bounds = Some(match bounds {
Some([bmin, bmax]) => [bmin.min(min), bmax.max(max)],
None => [min, max],
});
}
let mut bounds: Option<[DVec2; 2]> = None;
content.for_each_vector_mut(|vector, _| {
if let Some([min, max]) = vector.bounding_box() {
bounds = Some(match bounds {
Some([bmin, bmax]) => [bmin.min(min), bmax.max(max)],
None => [min, max],
});
}
});
// Nudge a degenerate axis so the gradient transform stays invertible, matching the editor's `nonzero_bounding_box`
let [min, mut max] = bounds.unwrap_or([DVec2::ZERO, DVec2::ONE]);
if max.x - min.x < 1e-10 {
max.x = min.x + 1.;
}
if max.y - min.y < 1e-10 {
max.y = min.y + 1.;
}
initial_gradient_transform_for_bounding_box([min, max])
};
// Nudge a degenerate axis so the gradient transform stays invertible, matching the editor's `nonzero_bounding_box`
let [min, mut max] = bounds.unwrap_or([DVec2::ZERO, DVec2::ONE]);
if max.x - min.x < 1e-10 {
max.x = min.x + 1.;
}
if max.y - min.y < 1e-10 {
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) {
*value = 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()),

View File

@@ -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"
);
}
}