Migrate remaining node graph data types from Vec to Table (#4067)

* Move Vec<String> to Table<String>

* Remove old VecDVec2

* Move Vec<u8> to Table<u8>

* Move Vec<f64> to Table<f64>

* Move [f64; 4] to Table<f64>

* Move Vec<NodeId> to Table<NodeId>

* Tidy up the TaggedValue variants

* Move Vec<BrushStroke> to Table<BrushStroke>

* Add missing type implementations

* Fix tests

---------
This commit is contained in:
Keavon Chambers 2026-04-28 13:44:25 -07:00 committed by GitHub
parent cf150b5cff
commit b396d17211
25 changed files with 277 additions and 341 deletions

View File

@ -169,12 +169,15 @@ fn generate_layout(introspected_data: &Arc<dyn std::any::Any + Send + Sync + 'st
Table<Raster<GPU>>,
Table<Color>,
Table<GradientStops>,
Table<String>,
Table<NodeId>,
Table<f64>,
Table<u8>,
GradientStops,
f64,
u32,
u64,
bool,
Vec<String>,
String,
Option<f64>,
DVec2,
@ -214,46 +217,6 @@ trait TableRowLayout {
}
}
impl<T: TableRowLayout> TableRowLayout for Vec<T> {
fn type_name() -> &'static str {
"Vec"
}
fn identifier(&self) -> String {
format!("Vec<{}> ({} element{})", T::type_name(), self.len(), if self.len() == 1 { "" } else { "s" })
}
fn element_page(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
if let Some(step) = data.desired_path.get(data.current_depth).cloned() {
match step {
PathStep::Element(index) => {
if let Some(row) = self.get(index) {
data.current_depth += 1;
let result = row.layout_with_breadcrumb(data);
data.current_depth -= 1;
return result;
} else {
warn!("Desired path truncated");
data.desired_path.truncate(data.current_depth);
}
}
PathStep::Attribute { .. } => {
warn!("Attribute path step inside a Vec is unsupported");
data.desired_path.truncate(data.current_depth);
}
}
}
let mut rows = self
.iter()
.enumerate()
.map(|(index, row)| vec![TextLabel::new(format!("{index}")).narrow(true).widget_instance(), row.cell_widget(PathStep::Element(index))])
.collect::<Vec<_>>();
rows.insert(0, column_headings(&["", "element"]));
vec![LayoutGroup::table(rows, false)]
}
}
impl<T: TableRowLayout> TableRowLayout for Table<T> {
fn type_name() -> &'static str {
"Table"
@ -616,6 +579,21 @@ impl TableRowLayout for f64 {
}
}
impl TableRowLayout for u8 {
fn type_name() -> &'static str {
"Byte"
}
fn identifier(&self) -> String {
format!("{self:02X}")
}
fn cell_widget(&self, _target: PathStep) -> WidgetInstance {
TextLabel::new(self.identifier()).narrow(true).widget_instance()
}
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
vec![LayoutGroup::row(vec![self.cell_widget(PathStep::Element(0))])]
}
}
impl TableRowLayout for u32 {
fn type_name() -> &'static str {
"Number (u32)"
@ -774,6 +752,26 @@ impl TableRowLayout for AlphaBlending {
}
}
impl TableRowLayout for NodeId {
fn type_name() -> &'static str {
"NodeId"
}
fn identifier(&self) -> String {
format!("Node {self}")
}
fn cell_widget(&self, _target: PathStep) -> WidgetInstance {
let node_id = *self;
TextButton::new("Go to Node")
.tooltip_description("Click to select the node with this ID in the graph.")
.on_update(move |_| NodeGraphMessage::SelectedNodesSet { nodes: vec![node_id] }.into())
.narrow(true)
.widget_instance()
}
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
vec![LayoutGroup::row(vec![self.cell_widget(PathStep::Element(0))])]
}
}
impl TableRowLayout for Option<NodeId> {
fn type_name() -> &'static str {
"NodeId"
@ -812,8 +810,13 @@ macro_rules! known_table_row_types {
Table<Raster<GPU>>,
Table<Color>,
Table<GradientStops>,
Table<String>,
Table<NodeId>,
Table<f64>,
Table<u8>,
GradientStops,
Color,
NodeId,
Option<NodeId>,
AlphaBlending,
DAffine2,
@ -822,11 +825,11 @@ macro_rules! known_table_row_types {
Vec2,
Option<f64>,
f64,
u8,
u32,
u64,
bool,
String,
Vec<String>,
Vector,
Raster<CPU>,
Raster<GPU>,

View File

@ -491,8 +491,9 @@ impl<'a> ModifyInputsContext<'a> {
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(stroke.join_miter_limit), false), false);
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::PaintOrderInput::INDEX);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::PaintOrder(stroke.paint_order), false), false);
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::DashLengthsInput::<Vec<f64>>::INDEX);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::VecF64(stroke.dash_lengths), false), true);
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::DashLengthsInput::<graphene_std::table::Table<f64>>::INDEX);
let dash_lengths_table = stroke.dash_lengths.into_iter().map(graphene_std::table::TableRow::new_from_element).collect();
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64Table(dash_lengths_table), false), true);
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::DashOffsetInput::INDEX);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(stroke.dash_offset), false), true);
}
@ -579,7 +580,8 @@ impl<'a> ModifyInputsContext<'a> {
let Some(brush_node_id) = self.existing_network_node_id("Brush", true) else {
return;
};
self.set_input_with_refresh(InputConnector::node(brush_node_id, 1), NodeInput::value(TaggedValue::BrushStrokes(strokes), false), false);
let strokes_table = strokes.into_iter().map(graphene_std::table::TableRow::new_from_element).collect();
self.set_input_with_refresh(InputConnector::node(brush_node_id, 1), NodeInput::value(TaggedValue::BrushStrokeTable(strokes_table), false), false);
}
pub fn resize_artboard(&mut self, location: IVec2, dimensions: IVec2) {

View File

@ -1391,7 +1391,7 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
}),
inputs: vec![
NodeInput::value(TaggedValue::Raster(Default::default()), true),
NodeInput::value(TaggedValue::BrushStrokes(Vec::new()), false),
NodeInput::value(TaggedValue::BrushStrokeTable(Default::default()), false),
NodeInput::value(TaggedValue::BrushCache(BrushCache::default()), false),
],
..Default::default()

View File

@ -210,14 +210,10 @@ pub(crate) fn property_from_type(
Some(x) if x == TypeId::of::<String>() => text_widget(default_info).into(),
Some(x) if x == TypeId::of::<DVec2>() => vec2_widget(default_info, "X", "Y", "", None, false),
Some(x) if x == TypeId::of::<DAffine2>() => transform_widget(default_info, &mut extra_widgets),
// ==========================
// PRIMITIVE COLLECTION TYPES
// ==========================
Some(x) if x == TypeId::of::<Vec<f64>>() => array_of_number_widget(default_info, TextInput::default()).into(),
Some(x) if x == TypeId::of::<Vec<DVec2>>() => array_of_vec2_widget(default_info, TextInput::default()).into(),
// ===========
// TABLE TYPES
// ===========
Some(x) if x == TypeId::of::<Table<f64>>() => array_of_number_widget(default_info, TextInput::default()).into(),
Some(x) if x == TypeId::of::<Table<Color>>() => color_widget(default_info, ColorInput::default().allow_none(true)),
Some(x) if x == TypeId::of::<Table<GradientStops>>() => color_widget(default_info, ColorInput::default().allow_none(false)),
// ============
@ -776,7 +772,7 @@ pub fn array_of_number_widget(parameter_widgets_info: ParameterWidgetsInfo, text
.map(str::parse::<f64>)
.collect::<Result<Vec<_>, _>>()
.ok()
.map(TaggedValue::VecF64)
.map(|values| TaggedValue::F64Table(values.into_iter().map(graphene_std::table::TableRow::new_from_element).collect()))
};
let Some(document_node) = document_node else { return Vec::new() };
@ -784,43 +780,11 @@ pub fn array_of_number_widget(parameter_widgets_info: ParameterWidgetsInfo, text
log::warn!("A widget failed to be built because its node's input index is invalid.");
return vec![];
};
if let Some(TaggedValue::VecF64(x)) = &input.as_non_exposed_value() {
if let Some(TaggedValue::F64Table(table)) = &input.as_non_exposed_value() {
widgets.extend_from_slice(&[
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
text_input
.value(x.iter().map(|v| v.to_string()).collect::<Vec<_>>().join(", "))
.on_update(optionally_update_value(move |x: &TextInput| from_string(&x.value), node_id, index))
.widget_instance(),
])
}
widgets
}
pub fn array_of_vec2_widget(parameter_widgets_info: ParameterWidgetsInfo, text_props: TextInput) -> Vec<WidgetInstance> {
let ParameterWidgetsInfo { document_node, node_id, index, .. } = parameter_widgets_info;
let mut widgets = start_widgets(parameter_widgets_info);
let from_string = |string: &str| {
string
.split(|c: char| !c.is_alphanumeric() && !matches!(c, '.' | '+' | '-'))
.filter(|x| !x.is_empty())
.map(|x| x.parse::<f64>().ok())
.collect::<Option<Vec<_>>>()
.map(|numbers| numbers.chunks_exact(2).map(|values| DVec2::new(values[0], values[1])).collect())
.map(TaggedValue::VecDVec2)
};
let Some(document_node) = document_node else { return Vec::new() };
let Some(input) = document_node.inputs.get(index) else {
log::warn!("A widget failed to be built because its node's input index is invalid.");
return vec![];
};
if let Some(TaggedValue::VecDVec2(x)) = &input.as_non_exposed_value() {
widgets.extend_from_slice(&[
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
text_props
.value(x.iter().map(|v| format!("({}, {})", v.x, v.y)).collect::<Vec<_>>().join(", "))
.value(table.iter_element_values().map(|v| v.to_string()).collect::<Vec<_>>().join(", "))
.on_update(optionally_update_value(move |x: &TextInput| from_string(&x.value), node_id, index))
.widget_instance(),
])
@ -1839,13 +1803,13 @@ pub(crate) fn rectangle_properties(node_id: NodeId, context: &mut NodeProperties
};
let uniform_val = match input.as_non_exposed_value() {
Some(TaggedValue::F64(x)) => *x,
Some(TaggedValue::F64Array4(x)) => x[0],
Some(TaggedValue::F64Table(table)) => table.iter_element_values().copied().next().unwrap_or(0.),
_ => 0.,
};
let individual_val = match input.as_non_exposed_value() {
Some(&TaggedValue::F64Array4(x)) => x,
Some(&TaggedValue::F64(x)) => [x; 4],
_ => [0.; 4],
Some(&TaggedValue::F64(x)) => vec![x; 4],
Some(TaggedValue::F64Table(table)) => table.iter_element_values().copied().collect(),
_ => vec![0.; 4],
};
// Uniform/individual radio input widget
@ -1868,6 +1832,7 @@ pub(crate) fn rectangle_properties(node_id: NodeId, context: &mut NodeProperties
]),
})
.on_commit(commit_value);
let individual_val_for_switch = individual_val.clone();
let individual = RadioEntryData::new("Individual")
.label("Individual")
.on_update(move |_| Message::Batched {
@ -1881,7 +1846,7 @@ pub(crate) fn rectangle_properties(node_id: NodeId, context: &mut NodeProperties
NodeGraphMessage::SetInputValue {
node_id,
input_index: CornerRadiusInput::<f64>::INDEX,
value: TaggedValue::F64Array4(individual_val),
value: TaggedValue::F64Table(individual_val_for_switch.iter().copied().map(graphene_std::table::TableRow::new_from_element).collect()),
}
.into(),
]),
@ -1899,11 +1864,7 @@ pub(crate) fn rectangle_properties(node_id: NodeId, context: &mut NodeProperties
.map(str::parse::<f64>)
.collect::<Result<Vec<f64>, _>>()
.ok()
.map(|v| {
let arr: Box<[f64; 4]> = v.into_boxed_slice().try_into().unwrap_or_default();
*arr
})
.map(TaggedValue::F64Array4)
.map(|values| TaggedValue::F64Table(values.into_iter().take(4).map(graphene_std::table::TableRow::new_from_element).collect()))
};
TextInput::default()
.value(individual_val.iter().map(|v| v.to_string()).collect::<Vec<_>>().join(", "))
@ -2296,11 +2257,10 @@ pub fn stroke_properties(node_id: NodeId, context: &mut NodePropertiesContext) -
_ => &StrokeJoin::Miter,
};
let dash_lengths_val = match &document_node.inputs[DashLengthsInput::<Vec<f64>>::INDEX].as_value() {
Some(TaggedValue::VecF64(x)) => x,
_ => &vec![],
let has_dash_lengths = match &document_node.inputs[DashLengthsInput::<Table<f64>>::INDEX].as_value() {
Some(TaggedValue::F64Table(table)) => table.is_empty(),
_ => true,
};
let has_dash_lengths = dash_lengths_val.is_empty();
let miter_limit_disabled = join_value != &StrokeJoin::Miter;
let color = color_widget(
@ -2325,7 +2285,7 @@ pub fn stroke_properties(node_id: NodeId, context: &mut NodePropertiesContext) -
.property_row();
let disabled_number_input = NumberInput::default().unit(" px").disabled(has_dash_lengths);
let dash_lengths = array_of_number_widget(
ParameterWidgetsInfo::new(node_id, DashLengthsInput::<Vec<f64>>::INDEX, true, context),
ParameterWidgetsInfo::new(node_id, DashLengthsInput::<Table<f64>>::INDEX, true, context),
TextInput::default().centered(true),
);
let number_input = disabled_number_input;

View File

@ -21,22 +21,14 @@ pub enum FrontendGraphDataType {
impl FrontendGraphDataType {
pub fn from_type(input: &Type) -> Self {
match TaggedValue::from_type_or_none(input) {
TaggedValue::U32(_)
| TaggedValue::U64(_)
| TaggedValue::F32(_)
| TaggedValue::F64(_)
| TaggedValue::DVec2(_)
| TaggedValue::F64Array4(_)
| TaggedValue::VecF64(_)
| TaggedValue::VecDVec2(_)
| TaggedValue::DAffine2(_) => Self::Number,
TaggedValue::U32(_) | TaggedValue::U64(_) | TaggedValue::F32(_) | TaggedValue::F64(_) | TaggedValue::DVec2(_) | TaggedValue::F64Table(_) | TaggedValue::DAffine2(_) => Self::Number,
TaggedValue::Artboard(_) => Self::Artboard,
TaggedValue::Graphic(_) => Self::Graphic,
TaggedValue::Raster(_) => Self::Raster,
TaggedValue::Vector(_) => Self::Vector,
TaggedValue::Color(_) => Self::Color,
TaggedValue::Gradient(_) | TaggedValue::GradientTable(_) => Self::Gradient,
TaggedValue::String(_) | TaggedValue::VecString(_) => Self::Typography,
TaggedValue::String(_) | TaggedValue::StringTable(_) => Self::Typography,
_ => Self::General,
}
}

View File

@ -53,15 +53,9 @@ impl TypeSource {
};
match self.compiled_nested_type() {
Some(nested_type) => match TaggedValue::from_type_or_none(nested_type) {
TaggedValue::U32(_)
| TaggedValue::U64(_)
| TaggedValue::F32(_)
| TaggedValue::F64(_)
| TaggedValue::DVec2(_)
| TaggedValue::F64Array4(_)
| TaggedValue::VecF64(_)
| TaggedValue::VecDVec2(_)
| TaggedValue::DAffine2(_) => FrontendGraphDataType::Number,
TaggedValue::U32(_) | TaggedValue::U64(_) | TaggedValue::F32(_) | TaggedValue::F64(_) | TaggedValue::DVec2(_) | TaggedValue::F64Table(_) | TaggedValue::DAffine2(_) => {
FrontendGraphDataType::Number
}
TaggedValue::Artboard(_) => FrontendGraphDataType::Artboard,
TaggedValue::Graphic(_) => FrontendGraphDataType::Graphic,
TaggedValue::Raster(_) => FrontendGraphDataType::Raster,

View File

@ -9,11 +9,9 @@ use glam::{DVec2, IVec2};
use graph_craft::document::DocumentNode;
use graph_craft::document::{DocumentNodeImplementation, NodeInput, value::TaggedValue};
use graphene_std::ProtoNodeIdentifier;
use graphene_std::subpath::Subpath;
use graphene_std::text::{TextAlign, TypesettingConfig};
use graphene_std::transform::ScaleType;
use graphene_std::uuid::NodeId;
use graphene_std::vector::Vector;
use graphene_std::vector::style::{PaintOrder, StrokeAlign};
use std::collections::HashMap;
use std::f64::consts::PI;
@ -1112,80 +1110,6 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
document.network_interface.set_input(&InputConnector::node(*node_id, 9), old_inputs[4].clone(), network_path);
}
// Upgrade the old "Spline" node to the new "Spline" node
if reference == DefinitionIdentifier::ProtoNode(graphene_std::vector::spline::IDENTIFIER)
|| reference == DefinitionIdentifier::ProtoNode(ProtoNodeIdentifier::new("graphene_core::vector::generator_nodes::SplineNode"))
|| reference == DefinitionIdentifier::ProtoNode(ProtoNodeIdentifier::new("graphene_core::vector::SplineNode"))
|| reference == DefinitionIdentifier::ProtoNode(ProtoNodeIdentifier::new("graphene_core::vector::SplinesFromPointsNode"))
{
// Retrieve the proto node identifier and verify it is the old "Spline" node, otherwise skip it if this is the new "Spline" node
let identifier = document
.network_interface
.implementation(node_id, network_path)
.and_then(|implementation| implementation.get_proto_node());
if identifier.map(|identifier| identifier.as_str()) != Some("graphene_core::vector::generator_nodes::SplineNode") {
return None;
}
// Obtain the document node for the given node ID, extract the vector points, and create a Vector path from the list of points
let node = document.network_interface.document_node(node_id, network_path)?;
let Some(TaggedValue::VecDVec2(points)) = node.inputs.get(1).and_then(|tagged_value| tagged_value.as_value()) else {
log::error!("The old Spline node's input at index 1 is not a TaggedValue::VecDVec2");
return None;
};
let vector = Vector::from_subpath(Subpath::from_anchors(points.to_vec(), false));
// Retrieve the output connectors linked to the "Spline" node's output connector
let Some(spline_outputs) = document.network_interface.outward_wires(network_path)?.get(&OutputConnector::node(*node_id, 0)).cloned() else {
log::error!("Vec of InputConnector Spline node is connected to its output connector 0.");
return None;
};
// Get the node's current position in the graph
let Some(node_position) = document.network_interface.position(node_id, network_path) else {
log::error!("Could not get position of spline node.");
return None;
};
// Get the "Path" node definition and fill it in with the Vector path and default vector modification
let Some(path_node_type) = resolve_network_node_type("Path") else {
log::error!("Path node does not exist.");
return None;
};
let modification = Box::new(graphene_std::vector::VectorModification::create_from_vector(&vector));
let path_node = path_node_type.node_template_input_override([None, Some(NodeInput::value(TaggedValue::VectorModification(modification), false))]);
// Get the "Spline" node definition and wire it up with the "Path" node as input
let Some(spline_node_type) = resolve_proto_node_type(graphene_std::vector::spline::IDENTIFIER) else {
log::error!("Spline node does not exist.");
return None;
};
let spline_node = spline_node_type.node_template_input_override([Some(NodeInput::node(NodeId(1), 0))]);
// Create a new node group with the "Path" and "Spline" nodes and generate new node IDs for them
let nodes = vec![(NodeId(1), path_node), (NodeId(0), spline_node)];
let new_ids = nodes.iter().map(|(id, _)| (*id, NodeId::new())).collect::<HashMap<_, _>>();
let new_spline_id = *new_ids.get(&NodeId(0))?;
let new_path_id = *new_ids.get(&NodeId(1))?;
// Remove the old "Spline" node from the document
document.network_interface.delete_nodes(vec![*node_id], false, network_path);
// Insert the new "Path" and "Spline" nodes into the network interface with generated IDs
document.network_interface.insert_node_group(nodes.clone(), new_ids, network_path);
// Reposition the new "Spline" node to match the original "Spline" node's position
document.network_interface.shift_node(&new_spline_id, node_position, network_path);
// Reposition the new "Path" node with an offset relative to the original "Spline" node's position
document.network_interface.shift_node(&new_path_id, node_position + IVec2::new(-7, 0), network_path);
// Redirect each output connection from the old node to the new "Spline" node's output connector
for input_connector in spline_outputs {
document.network_interface.set_input(&input_connector, NodeInput::node(new_spline_id, 0), network_path);
}
}
// Upgrade Text node to include line height and character spacing, which were previously hardcoded to 1, from https://github.com/GraphiteEditor/Graphite/pull/2016
if reference == DefinitionIdentifier::ProtoNode(graphene_std::text::text::IDENTIFIER) && inputs_count == 8 {
let mut template: NodeTemplate = resolve_document_node_type(&reference)?.default_node_template();

View File

@ -321,8 +321,8 @@ impl BrushToolData {
if reference == DefinitionIdentifier::Network("Brush".into()) && node_id != layer.to_node() {
let points_input = node.inputs.get(1)?;
let Some(TaggedValue::BrushStrokes(strokes)) = points_input.as_value() else { continue };
self.strokes.clone_from(strokes);
let Some(TaggedValue::BrushStrokeTable(strokes)) = points_input.as_value() else { continue };
self.strokes = strokes.iter_element_values().cloned().collect();
return Some(layer);
}

View File

@ -216,7 +216,7 @@ pub enum DocumentNodeMetadata {
impl DocumentNodeMetadata {
pub fn ty(&self) -> Type {
match self {
DocumentNodeMetadata::DocumentNodePath => concrete!(Vec<NodeId>),
DocumentNodeMetadata::DocumentNodePath => concrete!(core_types::table::Table<NodeId>),
}
}
}
@ -930,7 +930,10 @@ impl NodeNetwork {
let (tagged_value, exposed) = match previous_export {
NodeInput::Value { tagged_value, exposed } => (tagged_value, exposed),
NodeInput::Reflection(reflect) => match reflect {
DocumentNodeMetadata::DocumentNodePath => (TaggedValue::NodePath(path.to_vec()).into(), false),
DocumentNodeMetadata::DocumentNodePath => {
let table: core_types::table::Table<NodeId> = path.iter().copied().map(core_types::table::TableRow::new_from_element).collect();
(TaggedValue::NodeIdTable(table).into(), false)
}
},
previous_export => {
*export = previous_export;

View File

@ -4,28 +4,27 @@ use crate::proto::{Any as DAny, FutureAny};
use brush_nodes::brush_cache::BrushCache;
use brush_nodes::brush_stroke::BrushStroke;
use core_types::table::Table;
use core_types::transform::Footprint;
use core_types::uuid::NodeId;
use core_types::{CacheHash, Color, ContextFeatures, MemoHash, Node, Type};
use dyn_any::DynAny;
pub use dyn_any::StaticType;
use glam::{Affine2, Vec2};
pub use glam::{DAffine2, DVec2, IVec2, UVec2};
use graphic_types::Artboard;
use graphic_types::Graphic;
use graphic_types::Vector;
use graphic_types::raster_types::Image;
use graphic_types::raster_types::{CPU, Raster};
use graphic_types::vector_types::vector;
use graphic_types::vector_types::vector::ReferencePoint;
use graphic_types::vector_types::vector::style::Fill;
use graphic_types::vector_types::vector::style::GradientStops;
use graphic_types::raster_types::{CPU, Image, Raster};
use graphic_types::vector_types::vector::style::{Fill, Gradient, GradientStops, Stroke};
use graphic_types::vector_types::vector::{self, ReferencePoint};
use graphic_types::{Artboard, Graphic, Vector};
use raster_nodes::curve::Curve;
use rendering::RenderMetadata;
use std::fmt::Display;
use std::hash::Hash;
use std::marker::PhantomData;
use std::str::FromStr;
pub use std::sync::Arc;
use text_nodes::Font;
use text_nodes::vector_types::GradientStop;
use vector::VectorModification;
pub struct TaggedValueTypeError;
@ -166,27 +165,14 @@ macro_rules! tagged_value {
}
tagged_value! {
// ===============
// PRIMITIVE TYPES
// ===============
F32(f32),
F64(f64),
U32(u32),
U64(u64),
Bool(bool),
String(String),
// ========================
// LISTS OF PRIMITIVE TYPES
// ========================
#[serde(alias = "VecF32")] // TODO: Eventually remove this alias document upgrade code
VecF64(Vec<f64>),
VecDVec2(Vec<DVec2>),
F64Array4([f64; 4]),
VecString(Vec<String>),
NodePath(Vec<NodeId>),
// ===========
// TABLE TYPES
// ===========
StringTable(Table<String>),
#[serde(deserialize_with = "core_types::misc::migrate_vec_f64_to_table")] // TODO: Eventually remove this migration document upgrade code
#[serde(alias = "VecF64", alias = "VecF32", alias = "F64Array4")]
F64Table(Table<f64>),
NodeIdTable(Table<NodeId>),
#[serde(deserialize_with = "graphic_types::migrations::migrate_vector")] // TODO: Eventually remove this migration document upgrade code
#[serde(alias = "VectorData")]
Vector(Table<Vector>),
@ -205,24 +191,32 @@ tagged_value! {
#[serde(deserialize_with = "graphic_types::vector_types::gradient::migrate_gradient_stops")] // TODO: Eventually remove this migration document upgrade code
#[serde(alias = "GradientPositions", alias = "GradientStops")]
GradientTable(Table<GradientStops>),
#[serde(deserialize_with = "brush_nodes::migrations::migrate_brush_strokes_to_table")] // TODO: Eventually remove this migration document upgrade code
#[serde(alias = "BrushStrokes")]
BrushStrokeTable(Table<BrushStroke>),
// ============
// STRUCT TYPES
// SCALAR TYPES
// ============
F32(f32),
F64(f64),
U32(u32),
U64(u64),
Bool(bool),
String(String),
FVec2(Vec2),
FAffine2(Affine2),
#[serde(alias = "IVec2", alias = "UVec2")]
DVec2(DVec2),
DAffine2(DAffine2),
Stroke(graphic_types::vector_types::vector::style::Stroke),
Gradient(graphic_types::vector_types::vector::style::Gradient),
Font(text_nodes::Font),
BrushStrokes(Vec<BrushStroke>),
Stroke(Stroke),
Gradient(Gradient),
Font(Font),
BrushCache(BrushCache),
DocumentNode(DocumentNode),
ContextFeatures(ContextFeatures),
Curve(raster_nodes::curve::Curve),
Footprint(core_types::transform::Footprint),
VectorModification(Box<vector::VectorModification>),
Curve(Curve),
Footprint(Footprint),
VectorModification(Box<VectorModification>),
ImageData(Image<Color>),
// ==========
// ENUM TYPES

View File

@ -951,7 +951,7 @@ mod test {
// If this assert fails: These NodeIds seem to be changing when you modify TaggedValue, just update them.
assert_eq!(
ids,
vec![NodeId(2791689253855410677), NodeId(11246167042277902310), NodeId(1014827049498980779), NodeId(4864562752646903491)]
vec![NodeId(12189222519765806511), NodeId(15012204941197567462), NodeId(15525229164021892418), NodeId(1252248957706694248)]
);
}

View File

@ -80,7 +80,6 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => f64]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => u32]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => u64]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Vec<f64>]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => BlendMode]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => ImageTexture]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::transform::ReferencePoint]),
@ -92,19 +91,18 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::style::StrokeAlign]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::style::Stroke]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::style::Gradient]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Vec<graphene_std::uuid::NodeId>]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Box<graphene_std::vector::VectorModification>]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::CentroidType]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::PointSpacingType]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Option<f64>]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Option<NodeId>]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Vec<DVec2>]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Vec<String>]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => [f64; 4]]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Vec<NodeId>]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Table<String>]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Table<NodeId>]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Table<f64>]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Table<u8>]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Graphic]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::text::Font]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Vec<BrushStroke>]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Table<BrushStroke>]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => BrushCache]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => DocumentNode]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::raster::curve::Curve]),
@ -155,11 +153,10 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Table<Color>]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Image<Color>]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Table<GradientStops>]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Vec<DVec2>]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Vec<NodeId>]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Vec<f64>]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Vec<f32>]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Vec<String>]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Table<String>]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Table<NodeId>]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Table<f64>]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Table<u8>]),
#[cfg(target_family = "wasm")]
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => CanvasHandle]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => f64]),
@ -177,14 +174,13 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Option<f64>]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Option<Color>]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Option<NodeId>]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => [f64; 4]]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Graphic]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => glam::f32::Vec2]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => glam::f32::Affine2]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::style::Stroke]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::style::Gradient]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::text::Font]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Vec<BrushStroke>]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Table<BrushStroke>]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => BrushCache]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => DocumentNode]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::ContextFeatures]),

View File

@ -87,3 +87,21 @@ pub fn migrate_color<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Resul
ColorFormat::ColorTable(color_table) => color_table,
})
}
// TODO: Eventually remove this migration document upgrade code
pub fn migrate_vec_f64_to_table<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<crate::table::Table<f64>, D::Error> {
use crate::table::{Table, TableRow};
use serde::Deserialize;
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(untagged))]
enum F64TableFormat {
VecF64(Vec<f64>),
F64Table(Table<f64>),
}
Ok(match F64TableFormat::deserialize(deserializer)? {
F64TableFormat::VecF64(values) => values.into_iter().map(TableRow::new_from_element).collect(),
F64TableFormat::F64Table(table) => table,
})
}

View File

@ -192,7 +192,7 @@ async fn brush(
/// Optional raster content that may be drawn onto.
mut image: Table<Raster<CPU>>,
/// The list of brush stroke paths drawn by the Brush tool, with each including both its coordinates and styles.
strokes: Vec<BrushStroke>,
strokes: Table<BrushStroke>,
/// Internal cache data used to accelerate rendering of the brush content.
cache: BrushCache,
) -> Table<Raster<CPU>> {
@ -205,11 +205,15 @@ async fn brush(
let bounds = Table::new_from_row(table_row.clone()).bounding_box(DAffine2::IDENTITY, false);
let [start, end] = if let RenderBoundingBox::Rectangle(rect) = bounds { rect } else { [DVec2::ZERO, DVec2::ZERO] };
let image_bbox = AxisAlignedBbox { start, end };
let stroke_bbox = strokes.iter().map(|s| s.bounding_box()).reduce(|a, b| a.union(&b)).unwrap_or(AxisAlignedBbox::ZERO);
let stroke_bbox = strokes.iter_element_values().map(|s| s.bounding_box()).reduce(|a, b| a.union(&b)).unwrap_or(AxisAlignedBbox::ZERO);
let bbox = if image_bbox.size().length() < 0.1 { stroke_bbox } else { stroke_bbox.union(&image_bbox) };
let background_bounds = bbox.to_transform();
let mut draw_strokes: Vec<_> = strokes.iter().filter(|&s| !matches!(s.style.blend_mode, BlendMode::Erase | BlendMode::Restore)).cloned().collect();
let mut draw_strokes: Vec<_> = strokes
.iter_element_values()
.filter(|&s| !matches!(s.style.blend_mode, BlendMode::Erase | BlendMode::Restore))
.cloned()
.collect();
let mut brush_plan = cache.compute_brush_plan(table_row, &draw_strokes);
@ -273,12 +277,12 @@ async fn brush(
actual_image = blend_with_mode(actual_image, stroke_texture, stroke.style.blend_mode, (stroke.style.color.a() * 100.) as f64);
}
let has_erase_or_restore_strokes = strokes.iter().any(|s| matches!(s.style.blend_mode, BlendMode::Erase | BlendMode::Restore));
let has_erase_or_restore_strokes = strokes.iter_element_values().any(|s| matches!(s.style.blend_mode, BlendMode::Erase | BlendMode::Restore));
if has_erase_or_restore_strokes {
let opaque_image = Image::new(bbox.size().x as u32, bbox.size().y as u32, Color::WHITE);
let mut erase_restore_mask = TableRow::new_from_element(Raster::new_cpu(opaque_image)).with_attribute("transform", background_bounds);
for stroke in strokes {
for stroke in strokes.into_iter().map(|row| row.into_element()) {
let mut brush_texture = cache.get_cached_brush(&stroke.style);
if brush_texture.is_none() {
let tex = create_brush_texture(&stroke.style).await;

View File

@ -1,3 +1,25 @@
pub mod brush;
pub mod brush_cache;
pub mod brush_stroke;
pub mod migrations {
use crate::brush_stroke::BrushStroke;
use core_types::table::{Table, TableRow};
// TODO: Eventually remove this migration document upgrade code
pub fn migrate_brush_strokes_to_table<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<Table<BrushStroke>, D::Error> {
use serde::Deserialize;
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(untagged)]
enum BrushStrokeTableFormat {
BrushStrokes(Vec<BrushStroke>),
BrushStrokeTable(Table<BrushStroke>),
}
Ok(match BrushStrokeTableFormat::deserialize(deserializer)? {
BrushStrokeTableFormat::BrushStrokes(strokes) => strokes.into_iter().map(TableRow::new_from_element).collect(),
BrushStrokeTableFormat::BrushStrokeTable(table) => table,
})
}
}

View File

@ -73,8 +73,6 @@ async fn quantize_real_time<T>(
Context -> DAffine2,
Context -> Footprint,
Context -> DVec2,
Context -> Vec<f64>,
Context -> Vec<String>,
Context -> Table<Vector>,
Context -> Table<Graphic>,
Context -> Table<Raster<CPU>>,
@ -82,6 +80,8 @@ async fn quantize_real_time<T>(
Context -> Table<Color>,
Context -> Table<Artboard>,
Context -> Table<GradientStops>,
Context -> Table<String>,
Context -> Table<f64>,
Context -> (),
)]
value: impl Node<'n, Context<'static>, Output = T>,
@ -113,8 +113,6 @@ async fn quantize_animation_time<T>(
Context -> DAffine2,
Context -> Footprint,
Context -> DVec2,
Context -> Vec<f64>,
Context -> Vec<String>,
Context -> Table<Vector>,
Context -> Table<Graphic>,
Context -> Table<Raster<CPU>>,
@ -122,6 +120,8 @@ async fn quantize_animation_time<T>(
Context -> Table<Color>,
Context -> Table<Artboard>,
Context -> Table<GradientStops>,
Context -> Table<String>,
Context -> Table<f64>,
Context -> (),
)]
value: impl Node<'n, Context<'static>, Output = T>,

View File

@ -26,11 +26,11 @@ async fn context_modification<T>(
Context -> DAffine2,
Context -> Footprint,
Context -> DVec2,
Context -> Vec<DVec2>,
Context -> Option<NodeId>,
Context -> Vec<NodeId>,
Context -> Vec<f64>,
Context -> Vec<String>,
Context -> Table<String>,
Context -> Table<NodeId>,
Context -> Table<f64>,
Context -> Table<u8>,
Context -> Table<Vector>,
Context -> Table<Graphic>,
Context -> Table<Raster<CPU>>,

View File

@ -16,11 +16,6 @@ pub fn index_elements<T: graphic_types::graphic::AtIndex + Clone + Default>(
_: impl Ctx,
/// The collection of data, such as a list or table.
#[implementations(
Vec<f64>,
Vec<u32>,
Vec<u64>,
Vec<DVec2>,
Vec<String>,
Table<Artboard>,
Table<Graphic>,
Table<Vector>,
@ -28,6 +23,10 @@ pub fn index_elements<T: graphic_types::graphic::AtIndex + Clone + Default>(
Table<Raster<GPU>>,
Table<Color>,
Table<GradientStops>,
Table<String>,
Table<f64>,
Table<u8>,
Table<NodeId>,
)]
collection: T,
/// The index of the item to retrieve, starting from 0 for the first item. Negative indices count backwards from the end of the collection, starting from -1 for the last item.
@ -53,11 +52,7 @@ pub fn omit_element<T: graphic_types::graphic::OmitIndex + Clone + Default>(
_: impl Ctx,
/// The collection of data, such as a list or table.
#[implementations(
Vec<f64>,
Vec<u32>,
Vec<u64>,
Vec<DVec2>,
Vec<String>,
Table<String>,
Table<Artboard>,
Table<Graphic>,
Table<Vector>,
@ -181,9 +176,10 @@ where
/// Used as the value source for stamping the `editor:layer` attribute on each row of a layer's output,
/// which lets editor tools (e.g. selection, click target routing) trace data back to its owning layer.
#[node_macro::node(category(""))]
pub fn parent_layer(_: impl Ctx, node_path: Vec<NodeId>) -> Option<NodeId> {
pub fn parent_layer(_: impl Ctx, node_path: Table<NodeId>) -> Option<NodeId> {
// Get the penultimate element of the node path, or None if the path is too short
node_path.get(node_path.len().wrapping_sub(2)).copied()
let index = node_path.len().wrapping_sub(2);
node_path.element(index).copied()
}
/// Writes a per-row attribute column on the input table. The value-producing input is evaluated once per row,
@ -208,13 +204,13 @@ async fn write_attribute<T: AnyHash + Clone + Send + Sync + core_types::CacheHas
name: String,
/// The node that produces the per-row value. Called once per row with the row index in context.
#[implementations(
Context -> f64, Context -> u32, Context -> bool, Context -> String, Context -> Vec<String>, Context -> DVec2, Context -> DAffine2, Context -> Option<NodeId>, Context -> Table<Color>, Context -> Table<GradientStops>,
Context -> f64, Context -> u32, Context -> bool, Context -> String, Context -> Vec<String>, Context -> DVec2, Context -> DAffine2, Context -> Option<NodeId>, Context -> Table<Color>, Context -> Table<GradientStops>,
Context -> f64, Context -> u32, Context -> bool, Context -> String, Context -> Vec<String>, Context -> DVec2, Context -> DAffine2, Context -> Option<NodeId>, Context -> Table<Color>, Context -> Table<GradientStops>,
Context -> f64, Context -> u32, Context -> bool, Context -> String, Context -> Vec<String>, Context -> DVec2, Context -> DAffine2, Context -> Option<NodeId>, Context -> Table<Color>, Context -> Table<GradientStops>,
Context -> f64, Context -> u32, Context -> bool, Context -> String, Context -> Vec<String>, Context -> DVec2, Context -> DAffine2, Context -> Option<NodeId>, Context -> Table<Color>, Context -> Table<GradientStops>,
Context -> f64, Context -> u32, Context -> bool, Context -> String, Context -> Vec<String>, Context -> DVec2, Context -> DAffine2, Context -> Option<NodeId>, Context -> Table<Color>, Context -> Table<GradientStops>,
Context -> f64, Context -> u32, Context -> bool, Context -> String, Context -> Vec<String>, Context -> DVec2, Context -> DAffine2, Context -> Option<NodeId>, Context -> Table<Color>, Context -> Table<GradientStops>,
Context -> f64, Context -> u32, Context -> bool, Context -> String, Context -> Table<String>, Context -> DVec2, Context -> DAffine2, Context -> Option<NodeId>, Context -> Table<Color>, Context -> Table<GradientStops>,
Context -> f64, Context -> u32, Context -> bool, Context -> String, Context -> Table<String>, Context -> DVec2, Context -> DAffine2, Context -> Option<NodeId>, Context -> Table<Color>, Context -> Table<GradientStops>,
Context -> f64, Context -> u32, Context -> bool, Context -> String, Context -> Table<String>, Context -> DVec2, Context -> DAffine2, Context -> Option<NodeId>, Context -> Table<Color>, Context -> Table<GradientStops>,
Context -> f64, Context -> u32, Context -> bool, Context -> String, Context -> Table<String>, Context -> DVec2, Context -> DAffine2, Context -> Option<NodeId>, Context -> Table<Color>, Context -> Table<GradientStops>,
Context -> f64, Context -> u32, Context -> bool, Context -> String, Context -> Table<String>, Context -> DVec2, Context -> DAffine2, Context -> Option<NodeId>, Context -> Table<Color>, Context -> Table<GradientStops>,
Context -> f64, Context -> u32, Context -> bool, Context -> String, Context -> Table<String>, Context -> DVec2, Context -> DAffine2, Context -> Option<NodeId>, Context -> Table<Color>, Context -> Table<GradientStops>,
Context -> f64, Context -> u32, Context -> bool, Context -> String, Context -> Table<String>, Context -> DVec2, Context -> DAffine2, Context -> Option<NodeId>, Context -> Table<Color>, Context -> Table<GradientStops>,
)]
value: impl Node<'n, Context<'static>, Output = U>,
) -> Table<T> {
@ -255,11 +251,14 @@ pub async fn legacy_layer_extend<T: 'n + Send + Clone>(
#[expose]
#[implementations(Table<Artboard>, Table<Graphic>, Table<Vector>, Table<Raster<CPU>>, Table<Raster<GPU>>, Table<Color>, Table<GradientStops>)]
new: Table<T>,
nested_node_path: Vec<NodeId>,
nested_node_path: Table<NodeId>,
) -> Table<T> {
// Get the penultimate element of the node path, or None if the path is too short
// This is used to get the ID of the user-facing parent layer-style node (which encapsulates this internal node).
let layer = nested_node_path.get(nested_node_path.len().wrapping_sub(2)).copied();
let layer = {
let index = nested_node_path.len().wrapping_sub(2);
nested_node_path.element(index).copied()
};
let mut base = base;
for mut row in new.into_iter() {

View File

@ -6,7 +6,7 @@ use canvas_utils::{Canvas, CanvasHandle};
use core_types::WasmNotSend;
#[cfg(target_family = "wasm")]
use core_types::math::bbox::Bbox;
use core_types::table::Table;
use core_types::table::{Table, TableRow};
#[cfg(target_family = "wasm")]
use core_types::transform::Footprint;
use core_types::{Color, Ctx};
@ -85,14 +85,15 @@ async fn post_request(
#[name("URL")]
url: String,
/// The binary data to include in the body of the POST request.
body: Vec<u8>,
body: Table<u8>,
/// Makes the request run in the background without waiting on a response. This is useful for triggering webhooks without blocking the continued execution of the graph.
discard_result: bool,
#[widget(ParsedWidgetOverride::Custom = "text_area")] headers: String,
) -> String {
let mut header_map = parse_headers(&headers);
header_map.insert("Content-Type", "application/octet-stream".parse().unwrap());
let request = reqwest::Client::new().post(url).body(body).headers(header_map);
let body_bytes: Vec<u8> = body.iter_element_values().copied().collect();
let request = reqwest::Client::new().post(url).body(body_bytes).headers(header_map);
if discard_result {
#[cfg(target_family = "wasm")]
@ -114,15 +115,15 @@ async fn post_request(
/// Converts a text string to raw binary data. Useful for transmission over HTTP or writing to files.
#[node_macro::node(category("Web Request"), name("String to Bytes"))]
fn string_to_bytes(_: impl Ctx, string: String) -> Vec<u8> {
string.into_bytes()
fn string_to_bytes(_: impl Ctx, string: String) -> Table<u8> {
string.into_bytes().into_iter().map(TableRow::new_from_element).collect()
}
/// Converts extracted raw RGBA pixel data from an input image. Each pixel becomes 4 sequential bytes. Useful for transmission over HTTP or writing to files.
#[node_macro::node(category("Web Request"), name("Image to Bytes"))]
fn image_to_bytes(_: impl Ctx, image: Table<Raster<CPU>>) -> Vec<u8> {
let Some(image) = image.element(0) else { return vec![] };
image.data.iter().flat_map(|color| color.to_rgba8_srgb().into_iter()).collect::<Vec<u8>>()
fn image_to_bytes(_: impl Ctx, image: Table<Raster<CPU>>) -> Table<u8> {
let Some(image) = image.element(0) else { return Table::new() };
image.data.iter().flat_map(|color| color.to_rgba8_srgb()).map(TableRow::new_from_element).collect()
}
/// Loads binary from URLs and local asset paths. Returns a transparent placeholder if the resource fails to load, allowing rendering to continue.
@ -188,7 +189,6 @@ async fn rasterize<T: WasmNotSend + Clone + 'n>(
where
Table<T>: Render + Clone + graphic_types::IntoGraphicTable,
{
use core_types::table::TableRow;
use glam::{DAffine2, DVec2};
if footprint.transform.matrix2.determinant() == 0. {

View File

@ -1,4 +1,5 @@
use core_types::Ctx;
use core_types::table::{Table, TableRow};
use serde_json::Value;
use crate::unescape_string;
@ -237,15 +238,15 @@ fn query_json_all(
/// Strips the surrounding double quotes from string values, returning the raw text. Other types are never wrapped in quotes.
#[default(true)]
unquote_strings: bool,
) -> Vec<String> {
) -> Table<String> {
let cleaned = strip_trailing_commas(&json);
let Ok(value): Result<Value, _> = serde_json::from_str(&cleaned) else { return Vec::new() };
let Some(segments) = parse_json_path(path.trim()) else { return Vec::new() };
let Ok(value): Result<Value, _> = serde_json::from_str(&cleaned) else { return Table::new() };
let Some(segments) = parse_json_path(path.trim()) else { return Table::new() };
let mut results = Vec::new();
resolve_all(&value, &segments, !unquote_strings, &mut results);
results
results.into_iter().map(TableRow::new_from_element).collect()
}
/// A parsed segment of a JSON access path.

View File

@ -9,7 +9,7 @@ use convert_case::{Boundary, Converter, pattern};
use core_types::Color;
use core_types::graphene_hash::CacheHash;
use core_types::registry::types::{SignedInteger, TextArea};
use core_types::table::Table;
use core_types::table::{Table, TableRow};
use core_types::{CloneVarArgs, Context, Ctx, ExtractAll, ExtractVarArgs, OwnedContextImpl};
use dyn_any::DynAny;
use glam::{DAffine2, DVec2};
@ -700,10 +700,10 @@ fn string_split(
/// "\n" (newline), "\r" (carriage return), "\t" (tab), "\0" (null), and "\\" (backslash).
#[default(true)]
delimiter_escaping: bool,
) -> Vec<String> {
) -> Table<String> {
let delimiter = if delimiter_escaping { unescape_string(delimiter) } else { delimiter };
string.split(&delimiter).map(str::to_string).collect()
string.split(&delimiter).map(str::to_string).map(TableRow::new_from_element).collect()
}
/// Joins a list of strings together with a separator between each pair. This is the inverse of the **String Split** node.
@ -713,7 +713,7 @@ fn string_split(
fn string_join(
_: impl Ctx,
/// The list of strings to join together.
strings: Vec<String>,
strings: Table<String>,
/// The text placed between each pair of strings.
#[default(", ")]
separator: String,
@ -724,26 +724,27 @@ fn string_join(
) -> String {
let separator = if separator_escaping { unescape_string(separator) } else { separator };
strings.join(&separator)
strings.iter_element_values().map(|s| s.as_str()).collect::<Vec<_>>().join(&separator)
}
/// Iterates over a list of strings, evaluating the mapped operation for each one. Use the **Read String** node to access the current string inside the loop.
#[node_macro::node(category("Text"))]
async fn map_string(
ctx: impl Ctx + CloneVarArgs + ExtractAll,
strings: Vec<String>,
strings: Table<String>,
#[expose]
#[implementations(Context -> String)]
mapped: impl Node<Context<'static>, Output = String>,
) -> Vec<String> {
let mut result = Vec::new();
) -> Table<String> {
let mut result = Table::new();
for (i, string) in strings.into_iter().enumerate() {
for (i, row) in strings.into_iter().enumerate() {
let string = row.into_element();
let owned_ctx = OwnedContextImpl::from(ctx.clone());
let owned_ctx = owned_ctx.with_vararg(Box::new(string)).with_index(i);
let mapped_strings = mapped.eval(owned_ctx.into_context()).await;
let mapped_string = mapped.eval(owned_ctx.into_context()).await;
result.push(mapped_strings);
result.push(TableRow::new_from_element(mapped_string));
}
result

View File

@ -1,5 +1,6 @@
use core_types::Ctx;
use core_types::registry::types::SignedInteger;
use core_types::table::{Table, TableRow};
/// Checks whether the string contains a match for the given regular expression pattern. Optionally restricts the match to only the start and/or end of the string.
#[node_macro::node(category("Text: Regex"))]
@ -92,9 +93,9 @@ fn regex_find(
case_insensitive: bool,
/// Make `^` and `$` match the start and end of each line, not just the whole string.
multiline: bool,
) -> Vec<String> {
) -> Table<String> {
if pattern.is_empty() {
return Vec::new();
return Table::new();
}
let flags = match (case_insensitive, multiline) {
@ -107,7 +108,7 @@ fn regex_find(
let Ok(regex) = fancy_regex::Regex::new(&full_pattern) else {
log::error!("Invalid regex pattern: {pattern}");
return Vec::new();
return Table::new();
};
// Collect all matches since we need to support negative indexing
@ -117,7 +118,7 @@ fn regex_find(
let resolved_index = if match_index < 0 {
let from_end = (-match_index) as usize;
if from_end > matches.len() {
return Vec::new();
return Table::new();
}
matches.len() - from_end
} else {
@ -125,11 +126,14 @@ fn regex_find(
};
let Some(captures) = matches.get(resolved_index) else {
return Vec::new();
return Table::new();
};
// Index 0 is the whole match, 1+ are capture groups
(0..captures.len()).map(|i| captures.get(i).map_or(String::new(), |m| m.as_str().to_string())).collect()
(0..captures.len())
.map(|i| captures.get(i).map_or(String::new(), |m| m.as_str().to_string()))
.map(TableRow::new_from_element)
.collect()
}
/// Finds all non-overlapping matches of a regular expression pattern in the string, returning a list of the matched substrings.
@ -144,9 +148,9 @@ fn regex_find_all(
case_insensitive: bool,
/// Make `^` and `$` match the start and end of each line, not just the whole string.
multiline: bool,
) -> Vec<String> {
) -> Table<String> {
if pattern.is_empty() {
return Vec::new();
return Table::new();
}
let flags = match (case_insensitive, multiline) {
@ -159,10 +163,15 @@ fn regex_find_all(
let Ok(regex) = fancy_regex::Regex::new(&full_pattern) else {
log::error!("Invalid regex pattern: {pattern}");
return Vec::new();
return Table::new();
};
regex.find_iter(&string).filter_map(|m| m.ok()).map(|m| m.as_str().to_string()).collect()
regex
.find_iter(&string)
.filter_map(|m| m.ok())
.map(|m| m.as_str().to_string())
.map(TableRow::new_from_element)
.collect()
}
/// Splits a string into a list of substrings pulled from between separator characters as matched by a regular expression.
@ -179,9 +188,9 @@ fn regex_split(
case_insensitive: bool,
/// Make `^` and `$` match the start and end of each line, not just the whole string.
multiline: bool,
) -> Vec<String> {
) -> Table<String> {
if pattern.is_empty() {
return vec![string];
return Table::new_from_element(string);
}
let flags = match (case_insensitive, multiline) {
@ -194,8 +203,8 @@ fn regex_split(
let Ok(regex) = fancy_regex::Regex::new(&full_pattern) else {
log::error!("Invalid regex pattern: {pattern}");
return vec![string];
return Table::new_from_element(string);
};
regex.split(&string).filter_map(|s| s.ok()).map(|s| s.to_string()).collect()
regex.split(&string).filter_map(|s| s.ok()).map(|s| s.to_string()).map(TableRow::new_from_element).collect()
}

View File

@ -18,22 +18,37 @@ impl CornerRadius for f64 {
Table::new_from_element(Vector::from_subpath(subpath::Subpath::new_rounded_rectangle(size / -2., size / 2., [clamped_radius; 4])))
}
}
impl CornerRadius for [f64; 4] {
impl CornerRadius for Table<f64> {
fn generate(self, size: DVec2, clamped: bool) -> Table<Vector> {
// Expand to four corners using the CSS `border-radius` shorthand rules.
// - `[a]` → `[a, a, a, a]`
// - `[a, b]` → `[a, b, a, b]`
// - `[a, b, c]` → `[a, b, c, b]`
// - `[a, b, c, d, …]` → `[a, b, c, d]`
// - `[]` → `[0, 0, 0, 0]`
let values: Vec<f64> = self.iter_element_values().copied().collect();
let radii: [f64; 4] = match values.as_slice() {
[] => [0., 0., 0., 0.],
&[a] => [a, a, a, a],
&[a, b] => [a, b, a, b],
&[a, b, c] => [a, b, c, b],
&[a, b, c, d, ..] => [a, b, c, d],
};
let clamped_radius = if clamped {
// Algorithm follows the CSS spec: <https://drafts.csswg.org/css-backgrounds/#corner-overlap>
let mut scale_factor: f64 = 1.;
for i in 0..4 {
let side_length = if i % 2 == 0 { size.x } else { size.y };
let adjacent_corner_radius_sum = self[i] + self[(i + 1) % 4];
let adjacent_corner_radius_sum = radii[i] + radii[(i + 1) % 4];
if side_length < adjacent_corner_radius_sum {
scale_factor = scale_factor.min(side_length / adjacent_corner_radius_sum);
}
}
self.map(|x| x * scale_factor)
radii.map(|x| x * scale_factor)
} else {
self
radii
};
Table::new_from_element(Vector::from_subpath(subpath::Subpath::new_rounded_rectangle(size / -2., size / 2., clamped_radius)))
}
@ -140,7 +155,7 @@ fn rectangle<T: CornerRadius>(
#[default(100)]
height: f64,
_individual_corner_radii: bool, // TODO: Move this to the bottom once we have a migration capability
#[implementations(f64, [f64; 4])] corner_radius: T,
#[implementations(f64, Table<f64>)] corner_radius: T,
#[default(true)] clamped: bool,
) -> Table<Vector> {
corner_radius.generate(DVec2::new(width, height), clamped)

View File

@ -7,7 +7,7 @@ use vector_types::vector::VectorModification;
/// Applies a differential modification to a vector path, associating changes made by the Pen and Path tools to indices of edited points and segments.
#[node_macro::node(category(""))]
async fn path_modify(_ctx: impl Ctx, mut vector: Table<Vector>, modification: Box<VectorModification>, node_path: Vec<NodeId>) -> Table<Vector> {
async fn path_modify(_ctx: impl Ctx, mut vector: Table<Vector>, modification: Box<VectorModification>, node_path: Table<NodeId>) -> Table<Vector> {
use core_types::table::TableRow;
if vector.is_empty() {
@ -15,8 +15,11 @@ async fn path_modify(_ctx: impl Ctx, mut vector: Table<Vector>, modification: Bo
}
modification.apply(vector.element_mut(0).expect("push should give one item"));
// Update the source node id
let this_node_path = node_path.iter().rev().nth(1).copied();
// Update the source node id (penultimate element in the path, identifying the user-facing layer node)
let this_node_path = {
let index = node_path.len().wrapping_sub(2);
node_path.element(index).copied()
};
let existing: Option<NodeId> = vector.attribute_cloned_or_default("editor:layer", 0);
vector.set_attribute("editor:layer", 0, existing.or(this_node_path));

View File

@ -179,9 +179,9 @@ impl IntoF64Vec for f64 {
vec![self]
}
}
impl IntoF64Vec for Vec<f64> {
impl IntoF64Vec for Table<f64> {
fn into_vec(self) -> Vec<f64> {
self
self.into_iter().map(|row| row.into_element()).collect()
}
}
impl IntoF64Vec for String {
@ -217,7 +217,7 @@ async fn stroke<V, L: IntoF64Vec>(
/// The order to paint the stroke on top of the fill, or the fill on top of the stroke.
paint_order: PaintOrder,
/// The stroke dash lengths. Each length forms a distance in a pattern where the first length is a dash, the second is a gap, and so on. If the list is an odd length, the pattern repeats with solid-gap roles reversed.
#[implementations(Vec<f64>, f64, String, Vec<f64>, f64, String)]
#[implementations(Table<f64>, f64, String, Table<f64>, f64, String)]
dash_lengths: L,
/// The phase offset distance from the starting point of the dash pattern.
#[unit(" px")]
@ -2850,11 +2850,6 @@ impl<T> Count for Table<T> {
self.len()
}
}
impl<T> Count for Vec<T> {
fn count(&self) -> usize {
self.len()
}
}
// TODO: Return u32, u64, or usize instead of f64 after #1621 is resolved and has allowed us to implement automatic type conversion in the node graph for nodes with generic type inputs.
// TODO: (Currently automatic type conversion only works for concrete types, via the Graphene preprocessor and not the full Graphene type system.)
@ -2868,9 +2863,10 @@ async fn count_elements<I: Count>(
Table<Raster<GPU>>,
Table<Color>,
Table<GradientStops>,
Vec<String>,
Vec<f64>,
Vec<DVec2>,
Table<String>,
Table<f64>,
Table<u8>,
Table<NodeId>,
)]
content: I,
) -> f64 {