Move source_node_id to "editor:layer" and Vector<Upstream> to "editor:merged_layers", backed by a new 'Write Attribute' node (#4061)

* Fix click target propagation with the Rasterize node

* Add the 'Write Attribute' node

* Remove tag_layer in favor of the new Write Attribute node, prune redundant attribute writes

* Replace the Vector<Upstream> type argument with the "editor:merged_layers" attribute
This commit is contained in:
Keavon Chambers 2026-04-28 03:07:23 -07:00
parent 76938eb69a
commit afc2c9178e
27 changed files with 312 additions and 376 deletions

View File

@ -275,6 +275,8 @@ impl<T: TableRowLayout> TableRowLayout for Table<T> {
Some(format_alpha_blending(value))
} else if let Some(&value) = ty.downcast_ref::<Option<NodeId>>() {
Some(value.map_or_else(|| "-".to_string(), |id| id.to_string()))
} else if let Some(value) = ty.downcast_ref::<Table<Graphic>>() {
Some(format!("{} Objects", value.len()))
} else {
None
}
@ -458,17 +460,6 @@ impl TableRowLayout for Vector {
TextLabel::new("Colinear Handle IDs").narrow(true).widget_instance(),
TextLabel::new(colinear).narrow(true).widget_instance(),
]);
table_rows.push(vec![
TextLabel::new("Upstream Nested Layers").narrow(true).widget_instance(),
TextLabel::new(if self.upstream_data.is_some() {
"Yes (this preserves references to its upstream nested layers for editing by tools)"
} else {
"No (this doesn't preserve references to its upstream nested layers for editing by tools)"
})
.narrow(true)
.widget_instance(),
]);
}
VectorTableTab::Points => {
table_rows.push(column_headings(&["", "position"]));

View File

@ -219,7 +219,7 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
node_template: NodeTemplate {
document_node: DocumentNode {
implementation: DocumentNodeImplementation::Network(NodeNetwork {
exports: vec![NodeInput::node(NodeId(4), 0)],
exports: vec![NodeInput::node(NodeId(5), 0)],
nodes: [
// Primary (bottom) input type coercion
DocumentNode {
@ -233,22 +233,33 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
implementation: DocumentNodeImplementation::ProtoNode(graphic::wrap_graphic::IDENTIFIER),
..Default::default()
},
// Store the ID of the parent node (which encapsulates this sub-network) in each row we are extending the table with.
// Derive the parent layer's NodeId from the document path
DocumentNode {
inputs: vec![NodeInput::node(NodeId(1), 0), NodeInput::Reflection(graph_craft::document::DocumentNodeMetadata::DocumentNodePath)],
implementation: DocumentNodeImplementation::ProtoNode(graphic::source_node_id::IDENTIFIER),
inputs: vec![NodeInput::Reflection(graph_craft::document::DocumentNodeMetadata::DocumentNodePath)],
implementation: DocumentNodeImplementation::ProtoNode(graphic::parent_layer::IDENTIFIER),
..Default::default()
},
// Stamp each row of the content with the parent layer's NodeId via the `editor:layer` attribute,
// so editor tools (e.g. selection, click target routing) can trace data back to its owning layer.
DocumentNode {
inputs: vec![
NodeInput::node(NodeId(1), 0),
NodeInput::value(TaggedValue::String(String::from("editor:layer")), false),
NodeInput::node(NodeId(2), 0),
],
implementation: DocumentNodeImplementation::ProtoNode(graphic::write_attribute::IDENTIFIER),
..Default::default()
},
// The monitor node is used to display a thumbnail in the UI
DocumentNode {
inputs: vec![NodeInput::node(NodeId(2), 0)],
inputs: vec![NodeInput::node(NodeId(3), 0)],
implementation: DocumentNodeImplementation::ProtoNode(memo::monitor::IDENTIFIER),
skip_deduplication: true,
..Default::default()
},
DocumentNode {
call_argument: generic!(T),
inputs: vec![NodeInput::node(NodeId(0), 0), NodeInput::node(NodeId(3), 0)],
inputs: vec![NodeInput::node(NodeId(0), 0), NodeInput::node(NodeId(4), 0)],
implementation: DocumentNodeImplementation::ProtoNode(graphic::extend::IDENTIFIER),
..Default::default()
},
@ -272,6 +283,7 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
network_metadata: Some(NodeNetworkMetadata {
persistent_metadata: NodeNetworkPersistentMetadata {
node_metadata: [
// 0: to_graphic
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(-21, -3)),
@ -279,6 +291,7 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
},
..Default::default()
},
// 1: wrap_graphic
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(-21, -1)),
@ -286,6 +299,15 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
},
..Default::default()
},
// 2: parent_layer
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(-21, 1)),
..Default::default()
},
..Default::default()
},
// 3: write_attribute
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(-14, -1)),
@ -293,6 +315,7 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
},
..Default::default()
},
// 4: monitor
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(-7, -1)),
@ -300,6 +323,7 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
},
..Default::default()
},
// 5: extend
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, -3)),
@ -328,7 +352,7 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
node_template: NodeTemplate {
document_node: DocumentNode {
implementation: DocumentNodeImplementation::Network(NodeNetwork {
exports: vec![NodeInput::node(NodeId(3), 0)],
exports: vec![NodeInput::node(NodeId(4), 0)],
nodes: [
// Ensure this ID is kept in sync with the ID in set_alias so that the name input is kept in sync with the alias
DocumentNode {
@ -344,16 +368,27 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
],
..Default::default()
},
// Store the ID of the parent node (which encapsulates this sub-network) in each row we are extending the table with.
// Derive the parent layer's NodeId from the document path
DocumentNode {
inputs: vec![NodeInput::node(NodeId(0), 0), NodeInput::Reflection(graph_craft::document::DocumentNodeMetadata::DocumentNodePath)],
implementation: DocumentNodeImplementation::ProtoNode(graphic::source_node_id::IDENTIFIER),
inputs: vec![NodeInput::Reflection(graph_craft::document::DocumentNodeMetadata::DocumentNodePath)],
implementation: DocumentNodeImplementation::ProtoNode(graphic::parent_layer::IDENTIFIER),
..Default::default()
},
// Stamp each row of the content with the parent layer's NodeId via the `editor:layer` attribute,
// so editor tools (e.g. selection, click target routing) can trace data back to its owning layer.
DocumentNode {
inputs: vec![
NodeInput::node(NodeId(0), 0),
NodeInput::value(TaggedValue::String(String::from("editor:layer")), false),
NodeInput::node(NodeId(1), 0),
],
implementation: DocumentNodeImplementation::ProtoNode(graphic::write_attribute::IDENTIFIER),
..Default::default()
},
// The monitor node is used to display a thumbnail in the UI.
// TODO: Check if thumbnail is reversed
DocumentNode {
inputs: vec![NodeInput::node(NodeId(1), 0)],
inputs: vec![NodeInput::node(NodeId(2), 0)],
implementation: DocumentNodeImplementation::ProtoNode(memo::monitor::IDENTIFIER),
call_argument: generic!(T),
skip_deduplication: true,
@ -362,7 +397,7 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
DocumentNode {
inputs: vec![
NodeInput::import(graphene_std::Type::Fn(Box::new(concrete!(Context)), Box::new(concrete!(Table<Artboard>))), 0),
NodeInput::node(NodeId(2), 0),
NodeInput::node(NodeId(3), 0),
],
implementation: DocumentNodeImplementation::ProtoNode(graphic::extend::IDENTIFIER),
..Default::default()
@ -418,6 +453,7 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
network_metadata: Some(NodeNetworkMetadata {
persistent_metadata: NodeNetworkPersistentMetadata {
node_metadata: [
// 0: create_artboard
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(-21, -3)),
@ -425,6 +461,15 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
},
..Default::default()
},
// 1: parent_layer
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(-21, 3)),
..Default::default()
},
..Default::default()
},
// 2: write_attribute
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(-14, -3)),
@ -432,6 +477,7 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
},
..Default::default()
},
// 3: monitor
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(-7, -3)),
@ -439,6 +485,7 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
},
..Default::default()
},
// 4: extend
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, -4)),

View File

@ -169,10 +169,6 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[
"graphene_core::graphic::LegacyLayerExtendNode",
],
},
NodeReplacement {
node: graphene_std::graphic_nodes::source_node_id::IDENTIFIER,
aliases: &["graphene_core::graphic::graphic::SourceNodeIdNode", "graphene_core::graphic::SourceNodeIdNode"],
},
NodeReplacement {
node: graphene_std::graphic::to_graphic::IDENTIFIER,
aliases: &[
@ -1024,6 +1020,16 @@ pub fn document_migration_reset_node_definition(document_serialized_content: &st
return true;
}
// The `source_node_id` proto node was removed in favor of `parent_layer` + `write_attribute`.
// Documents that still reference it inside their Merge or Artboard layer networks need those layer definitions
// reset to the current default so the new internal plumbing replaces the obsolete node.
if document_serialized_content.contains("graphic_nodes::graphic::SourceNodeIdNode")
|| document_serialized_content.contains("graphene_core::graphic::graphic::SourceNodeIdNode")
|| document_serialized_content.contains("graphene_core::graphic::SourceNodeIdNode")
{
return true;
}
false
}

View File

@ -97,6 +97,7 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
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]]),
@ -175,6 +176,7 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Table<Raster<GPU>>]),
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]),

View File

@ -366,10 +366,7 @@ pub fn simplify_identifier_name(ty: &str) -> String {
}
pub fn make_type_user_readable(ty: &str) -> String {
ty.replace("Option<Arc<OwnedContextImpl>>", "Context")
.replace("Vector<Option<Table<Graphic>>>", "Vector")
.replace("Raster<CPU>", "Raster")
.replace("Raster<GPU>", "Raster")
ty.replace("Option<Arc<OwnedContextImpl>>", "Context").replace("Raster<CPU>", "Raster").replace("Raster<GPU>", "Raster")
}
impl std::fmt::Debug for Type {

View File

@ -113,30 +113,11 @@ pub fn migrate_artboard<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Re
alpha_blending: Vec<AlphaBlending>,
}
// Attributes (transform, alpha_blending, editor:layer) are not serialized, so migration only needs
// to recover the elements. Per-row attribute values are populated at runtime by the node graph.
Ok(match ArtboardFormat::deserialize(deserializer)? {
ArtboardFormat::ArtboardGroup(artboard_group) => {
let mut table = Table::new();
for (artboard, source_node_id) in artboard_group.artboards {
table.push(
TableRow::new_from_element(artboard)
.with_attribute("transform", DAffine2::IDENTITY)
.with_attribute("alpha_blending", AlphaBlending::default())
.with_attribute("source_node_id", source_node_id),
);
}
table
}
ArtboardFormat::OldArtboardTable(old_table) => old_table
.element
.into_iter()
.zip(old_table.transform.into_iter().zip(old_table.alpha_blending))
.map(|(element, (transform, alpha_blending))| {
TableRow::new_from_element(element)
.with_attribute("transform", transform)
.with_attribute("alpha_blending", alpha_blending)
.with_attribute("source_node_id", None::<NodeId>)
})
.collect(),
ArtboardFormat::ArtboardGroup(artboard_group) => artboard_group.artboards.into_iter().map(|(artboard, _)| TableRow::new_from_element(artboard)).collect(),
ArtboardFormat::OldArtboardTable(old_table) => old_table.element.into_iter().map(TableRow::new_from_element).collect(),
ArtboardFormat::ArtboardTable(artboard_table) => artboard_table,
})
}

View File

@ -12,7 +12,7 @@ use raster_types::{CPU, GPU, Raster};
use vector_types::GradientStops;
// use vector_types::Vector;
pub type Vector = vector_types::Vector<Option<Table<Graphic>>>;
pub use vector_types::Vector;
/// The possible forms of graphical content that can be rendered by the Render node into either an image or SVG syntax.
#[derive(Clone, Debug, CacheHash, PartialEq, DynAny)]
@ -141,7 +141,7 @@ fn flatten_graphic_table<T>(content: Table<Graphic>, extract_variant: fn(Graphic
fn flatten_recursive<T>(output: &mut Table<T>, current_graphic_table: Table<Graphic>, extract_variant: fn(Graphic) -> Option<Table<T>>) {
for current_graphic_row in current_graphic_table.into_iter() {
let source_node_id: Option<NodeId> = current_graphic_row.attribute_cloned_or_default("source_node_id");
let layer: Option<NodeId> = current_graphic_row.attribute_cloned_or_default("editor:layer");
let current_transform: DAffine2 = current_graphic_row.attribute_cloned_or_default("transform");
let current_alpha_blending: AlphaBlending = current_graphic_row.attribute_cloned_or_default("alpha_blending");
@ -168,7 +168,7 @@ fn flatten_graphic_table<T>(content: Table<Graphic>, extract_variant: fn(Graphic
attributes.insert("transform", current_transform * row_transform);
attributes.insert("alpha_blending", compose_alpha_blending(current_alpha_blending, row_alpha_blending));
attributes.insert("source_node_id", source_node_id);
attributes.insert("editor:layer", layer);
output.push(TableRow::from_parts(element, attributes));
}
@ -504,70 +504,32 @@ pub fn migrate_graphic<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Res
Table(serde_json::Value),
}
// Attributes (transform, alpha_blending, editor:layer) are not serialized, so migration only needs
// to recover the elements. Per-row attribute values are populated at runtime by the node graph.
Ok(match GraphicFormat::deserialize(deserializer)? {
GraphicFormat::OldGraphicGroup(old) => {
let mut graphic_table = Table::new();
for (graphic, source_node_id) in old.elements {
graphic_table.push(
TableRow::new_from_element(graphic)
.with_attribute("transform", old.transform)
.with_attribute("alpha_blending", old.alpha_blending)
.with_attribute("source_node_id", source_node_id),
);
}
graphic_table
}
GraphicFormat::OldGraphicGroup(old) => old.elements.into_iter().map(|(graphic, _)| TableRow::new_from_element(graphic)).collect(),
GraphicFormat::OlderTableOldGraphicGroup(old) => old
.element
.into_iter()
.flat_map(|element| {
element.elements.into_iter().map(move |(graphic, source_node_id)| {
TableRow::new_from_element(graphic)
.with_attribute("transform", element.transform)
.with_attribute("alpha_blending", element.alpha_blending)
.with_attribute("source_node_id", source_node_id)
})
})
.flat_map(|element| element.elements.into_iter().map(|(graphic, _)| TableRow::new_from_element(graphic)))
.collect(),
GraphicFormat::OldTableOldGraphicGroup(old) => old
.element
.into_iter()
.flat_map(|element| {
element.elements.into_iter().map(move |(graphic, source_node_id)| {
TableRow::new_from_element(graphic)
.with_attribute("transform", element.transform)
.with_attribute("alpha_blending", element.alpha_blending)
.with_attribute("source_node_id", source_node_id)
})
})
.flat_map(|element| element.elements.into_iter().map(|(graphic, _)| TableRow::new_from_element(graphic)))
.collect(),
GraphicFormat::OldTableGraphicGroup(old) => old
.element
.into_iter()
.flat_map(|element| {
element.elements.into_iter().map(move |(graphic, source_node_id)| {
TableRow::new_from_element(graphic)
.with_attribute("transform", DAffine2::IDENTITY)
.with_attribute("alpha_blending", AlphaBlending::default())
.with_attribute("source_node_id", source_node_id)
})
})
.flat_map(|element| element.elements.into_iter().map(|(graphic, _)| TableRow::new_from_element(graphic)))
.collect(),
GraphicFormat::Table(value) => {
// Try to deserialize as either table format
if let Ok(old_table) = serde_json::from_value::<Table<GraphicGroup>>(value.clone()) {
let mut graphic_table = Table::new();
for index in 0..old_table.len() {
let row_transform: DAffine2 = old_table.attribute_cloned_or_default("transform", index);
let row_alpha_blending: AlphaBlending = old_table.attribute_cloned_or_default("alpha_blending", index);
for (graphic, source_node_id) in &old_table.element(index).unwrap().elements {
graphic_table.push(
TableRow::new_from_element(graphic.clone())
.with_attribute("transform", row_transform)
.with_attribute("alpha_blending", row_alpha_blending)
.with_attribute("source_node_id", *source_node_id),
);
for (graphic, _) in &old_table.element(index).unwrap().elements {
graphic_table.push(TableRow::new_from_element(graphic.clone()));
}
}
graphic_table

View File

@ -72,40 +72,17 @@ pub mod migrations {
Ok(match VectorFormat::deserialize(deserializer)? {
VectorFormat::Vector(vector) => Table::new_from_element(vector),
VectorFormat::OldVectorData(old) => {
let mut vector_table = Table::new_from_element(Vector {
style: old.style,
colinear_manipulators: old.colinear_manipulators,
point_domain: old.point_domain,
segment_domain: old.segment_domain,
region_domain: old.region_domain,
upstream_data: old.upstream_graphic_group,
});
vector_table.set_attribute("transform", 0, old.transform);
vector_table.set_attribute("alpha_blending", 0, old.alpha_blending);
vector_table
}
VectorFormat::OlderVectorTable(older_table) => older_table
.element
.into_iter()
.map(|element| {
TableRow::new_from_element(element)
.with_attribute("transform", DAffine2::IDENTITY)
.with_attribute("alpha_blending", AlphaBlending::default())
.with_attribute("source_node_id", None::<core_types::uuid::NodeId>)
})
.collect(),
VectorFormat::OldVectorTable(old_table) => old_table
.element
.into_iter()
.zip(old_table.transform.into_iter().zip(old_table.alpha_blending))
.map(|(element, (transform, alpha_blending))| {
TableRow::new_from_element(element)
.with_attribute("transform", transform)
.with_attribute("alpha_blending", alpha_blending)
.with_attribute("source_node_id", None::<core_types::uuid::NodeId>)
})
.collect(),
// Attributes (transform, alpha_blending, editor:layer) are not serialized, so migration only needs
// to recover the elements. Per-row attribute values are populated at runtime by the node graph.
VectorFormat::OldVectorData(old) => Table::new_from_element(Vector {
style: old.style,
colinear_manipulators: old.colinear_manipulators,
point_domain: old.point_domain,
segment_domain: old.segment_domain,
region_domain: old.region_domain,
}),
VectorFormat::OlderVectorTable(older_table) => older_table.element.into_iter().map(TableRow::new_from_element).collect(),
VectorFormat::OldVectorTable(old_table) => old_table.element.into_iter().map(TableRow::new_from_element).collect(),
VectorFormat::VectorTable(vector_table) => vector_table,
})
}

View File

@ -319,31 +319,14 @@ pub fn migrate_image_frame<'de, D: serde::Deserializer<'de>>(deserializer: D) ->
Table::new_from_element(Raster::new_cpu(table.element(0).unwrap().clone()))
}
// Attributes (transform, alpha_blending, editor:layer) are not serialized, so migration only needs
// to recover the elements. Per-row attribute values are populated at runtime by the node graph.
fn old_table_to_new_table<T>(old_table: OldTable<T>) -> Table<T> {
old_table
.element
.into_iter()
.zip(old_table.transform.into_iter().zip(old_table.alpha_blending))
.map(|(element, (transform, alpha_blending))| {
TableRow::new_from_element(element)
.with_attribute("transform", transform)
.with_attribute("alpha_blending", alpha_blending)
.with_attribute("source_node_id", None::<core_types::uuid::NodeId>)
})
.collect()
old_table.element.into_iter().map(TableRow::new_from_element).collect()
}
fn older_table_to_new_table<T>(old_table: OlderTable<T>) -> Table<T> {
old_table
.element
.into_iter()
.map(|element| {
TableRow::new_from_element(element)
.with_attribute("transform", DAffine2::IDENTITY)
.with_attribute("alpha_blending", AlphaBlending::default())
.with_attribute("source_node_id", None::<core_types::uuid::NodeId>)
})
.collect()
old_table.element.into_iter().map(TableRow::new_from_element).collect()
}
fn from_image_frame_table(image_frame: Table<ImageFrame<Color>>) -> Table<Raster<CPU>> {
@ -447,19 +430,12 @@ pub fn migrate_image_frame_row<'de, D: serde::Deserializer<'de>>(deserializer: D
RasterTableRow(TableRow<Raster<CPU>>),
}
// Attributes (transform, alpha_blending, editor:layer) are not serialized, so migration only needs
// to recover the element. Per-row attribute values are populated at runtime by the node graph.
Ok(match FormatVersions::deserialize(deserializer)? {
FormatVersions::Image(image) => TableRow::new_from_element(Raster::new_cpu(image))
.with_attribute("transform", DAffine2::IDENTITY)
.with_attribute("alpha_blending", AlphaBlending::default())
.with_attribute("source_node_id", None::<core_types::uuid::NodeId>),
FormatVersions::OldImageFrame(image_frame_with_transform_and_blending) => TableRow::new_from_element(Raster::new_cpu(image_frame_with_transform_and_blending.image))
.with_attribute("transform", image_frame_with_transform_and_blending.transform)
.with_attribute("alpha_blending", image_frame_with_transform_and_blending.alpha_blending)
.with_attribute("source_node_id", None::<core_types::uuid::NodeId>),
FormatVersions::ImageFrameTable(image_frame) => TableRow::new_from_element(Raster::new_cpu(image_frame.element(0).unwrap().image.clone()))
.with_attribute("transform", DAffine2::IDENTITY)
.with_attribute("alpha_blending", AlphaBlending::default())
.with_attribute("source_node_id", None::<core_types::uuid::NodeId>),
FormatVersions::Image(image) => TableRow::new_from_element(Raster::new_cpu(image)),
FormatVersions::OldImageFrame(old) => TableRow::new_from_element(Raster::new_cpu(old.image)),
FormatVersions::ImageFrameTable(image_frame) => TableRow::new_from_element(Raster::new_cpu(image_frame.element(0).unwrap().image.clone())),
FormatVersions::RasterTable(image_frame_table) => image_frame_table.into_iter().next().unwrap_or_default(),
FormatVersions::RasterTableRow(image_table_row) => image_table_row,
})

View File

@ -412,10 +412,10 @@ impl Render for Graphic {
metadata.upstream_footprints.insert(element_id, footprint);
// TODO: Find a way to handle more than the first row
if !table.is_empty() {
let source_node_id: Option<NodeId> = table.attribute_cloned_or_default("source_node_id", 0);
let layer: Option<NodeId> = table.attribute_cloned_or_default("editor:layer", 0);
let transform: DAffine2 = table.attribute_cloned_or_default("transform", 0);
metadata.first_element_source_id.insert(element_id, source_node_id);
metadata.first_element_source_id.insert(element_id, layer);
metadata.local_transforms.insert(element_id, transform);
}
}
@ -655,8 +655,8 @@ impl Render for Table<Artboard> {
fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, _element_id: Option<NodeId>) {
for index in 0..self.len() {
let source_node_id: Option<NodeId> = self.attribute_cloned_or_default("source_node_id", index);
self.element(index).unwrap().collect_metadata(metadata, footprint, source_node_id);
let layer: Option<NodeId> = self.attribute_cloned_or_default("editor:layer", index);
self.element(index).unwrap().collect_metadata(metadata, footprint, layer);
}
}
@ -805,16 +805,16 @@ impl Render for Table<Graphic> {
fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option<NodeId>) {
for index in 0..self.len() {
let row_transform: DAffine2 = self.attribute_cloned_or_default("transform", index);
let source_node_id: Option<NodeId> = self.attribute_cloned_or_default("source_node_id", index);
let layer: Option<NodeId> = self.attribute_cloned_or_default("editor:layer", index);
let element = self.element(index).unwrap();
let mut footprint = footprint;
footprint.transform *= row_transform;
if let Some(element_id) = source_node_id {
if let Some(element_id) = layer {
element.collect_metadata(metadata, footprint, Some(element_id));
} else {
// Recurse through anonymous wrapper rows to reach nested content with source_node_ids
// Recurse through anonymous wrapper rows to reach nested content with editor:layer tags
element.collect_metadata(metadata, footprint, None);
}
}
@ -860,9 +860,9 @@ impl Render for Table<Graphic> {
}
fn new_ids_from_hash(&mut self, _reference: Option<NodeId>) {
let (elements, source_node_ids) = self.element_and_attribute_slices_mut::<Option<NodeId>>("source_node_id");
for (element, source_node_id) in elements.iter_mut().zip(source_node_ids.iter()) {
element.new_ids_from_hash(*source_node_id);
let (elements, layers) = self.element_and_attribute_slices_mut::<Option<NodeId>>("editor:layer");
for (element, layer) in elements.iter_mut().zip(layers.iter()) {
element.new_ids_from_hash(*layer);
}
}
}
@ -938,8 +938,7 @@ impl Render for Table<Vector> {
let vector_row = Table::new_from_row(
TableRow::new_from_element(cloned_vector)
.with_attribute("transform", multiplied_transform)
.with_attribute("alpha_blending", alpha_blending)
.with_attribute("source_node_id", None::<NodeId>),
.with_attribute("alpha_blending", alpha_blending),
);
(id, mask_type, vector_row)
@ -1256,8 +1255,7 @@ impl Render for Table<Vector> {
let vector_table = Table::new_from_row(
TableRow::new_from_element(cloned_element)
.with_attribute("transform", row_transform)
.with_attribute("alpha_blending", alpha_blending)
.with_attribute("source_node_id", None::<NodeId>),
.with_attribute("alpha_blending", alpha_blending),
);
let bounds = element.bounding_box_with_transform(multiplied_transform).unwrap_or(layer_bounds);
@ -1329,10 +1327,10 @@ impl Render for Table<Vector> {
for index in 0..self.len() {
let Some(vector) = self.element(index) else { continue };
let transform: DAffine2 = self.attribute_cloned_or_default("transform", index);
let source_node_id: Option<NodeId> = self.attribute_cloned_or_default("source_node_id", index);
let layer: Option<NodeId> = self.attribute_cloned_or_default("editor:layer", index);
if let Some(element_id) = caller_element_id.or(source_node_id) {
// When recovering element_id from the row's source_node_id (because the caller
if let Some(element_id) = caller_element_id.or(layer) {
// When recovering element_id from the row's editor:layer tag (because the caller
// passed None), also store the transform metadata that Graphic::collect_metadata
// normally provides but skipped due to the None element_id.
if caller_element_id.is_none() {
@ -1365,7 +1363,7 @@ impl Render for Table<Vector> {
.stroke_bezier_paths()
.map(fill)
.map(|subpath| ClickTarget::new_with_subpath(subpath, stroke_width).into())
.chain(single_anchors_targets.into_iter())
.chain(single_anchors_targets)
.collect::<Vec<_>>();
metadata.click_targets.entry(element_id).or_insert(click_targets);
@ -1373,7 +1371,11 @@ impl Render for Table<Vector> {
metadata.vector_data.entry(element_id).or_insert_with(|| Arc::new(vector.clone()));
}
if let Some(upstream_nested_layers) = &vector.upstream_data {
// If this row carries a snapshot of upstream graphic content (e.g. it was produced by Boolean Operation,
// Flatten Path, Morph, or any other destructive merge), recurse into that snapshot so the editor can
// surface the original child layers' click targets.
let upstream_nested_layers = self.attribute_cloned_or_default::<Table<Graphic>>("editor:merged_layers", index);
if !upstream_nested_layers.is_empty() {
let mut upstream_footprint = footprint;
upstream_footprint.transform *= transform;
upstream_nested_layers.collect_metadata(metadata, upstream_footprint, None);
@ -1571,7 +1573,19 @@ impl Render for Table<Raster<CPU>> {
metadata.upstream_footprints.insert(element_id, footprint);
// TODO: Find a way to handle more than one row of the raster table
if !self.is_empty() {
metadata.local_transforms.insert(element_id, self.attribute_cloned_or_default("transform", 0));
let transform: DAffine2 = self.attribute_cloned_or_default("transform", 0);
metadata.local_transforms.insert(element_id, transform);
// If this raster carries a snapshot of upstream graphic content (e.g. it was produced by Rasterize,
// which destructively merges its inputs into pixels), recurse into that snapshot so the editor can
// surface the original child layers' click targets (the same mechanism Boolean Operation uses).
// The snapshot was captured before Rasterize shifted its input transforms to align with the rasterization
// area, so the children are already in the coordinate space matching `footprint` here — we must NOT
// multiply in `transform` (which is the rasterization area, not a layer-stack transform).
let upstream_nested_layers = self.attribute_cloned_or_default::<Table<Graphic>>("editor:merged_layers", 0);
if !upstream_nested_layers.is_empty() {
upstream_nested_layers.collect_metadata(metadata, footprint, None);
}
}
}
@ -1649,7 +1663,19 @@ impl Render for Table<Raster<GPU>> {
metadata.upstream_footprints.insert(element_id, footprint);
// TODO: Find a way to handle more than one row of the raster table
if !self.is_empty() {
metadata.local_transforms.insert(element_id, self.attribute_cloned_or_default("transform", 0));
let transform: DAffine2 = self.attribute_cloned_or_default("transform", 0);
metadata.local_transforms.insert(element_id, transform);
// If this raster carries a snapshot of upstream graphic content (e.g. it was produced by Rasterize,
// which destructively merges its inputs into pixels), recurse into that snapshot so the editor can
// surface the original child layers' click targets (the same mechanism Boolean Operation uses).
// The snapshot was captured before Rasterize shifted its input transforms to align with the rasterization
// area, so the children are already in the coordinate space matching `footprint` here — we must NOT
// multiply in `transform` (which is the rasterization area, not a layer-stack transform).
let upstream_nested_layers = self.attribute_cloned_or_default::<Table<Graphic>>("editor:merged_layers", 0);
if !upstream_nested_layers.is_empty() {
upstream_nested_layers.collect_metadata(metadata, footprint, None);
}
}
}

View File

@ -11,7 +11,7 @@ pub trait MergeByDistanceExt {
fn merge_by_distance_spatial(&mut self, transform: DAffine2, distance: f64);
}
impl<Upstream: 'static> MergeByDistanceExt for Vector<Upstream> {
impl MergeByDistanceExt for Vector {
fn merge_by_distance_topological(&mut self, distance: f64) {
// Treat self as an undirected graph
let indices = VectorIndex::build_from(self);
@ -237,7 +237,7 @@ pub struct VectorIndex {
impl VectorIndex {
/// Construct a [`VectorIndex`] by building indexes from the given [`Vector`]. Takes `O(n)` time.
pub fn build_from<Upstream: 'static>(data: &Vector<Upstream>) -> Self {
pub fn build_from(data: &Vector) -> Self {
let point_to_offset = data.point_domain.ids().iter().copied().enumerate().map(|(a, b)| (b, a)).collect::<FxHashMap<_, _>>();
let mut point_to_node = FxHashMap::default();
@ -295,7 +295,7 @@ impl VectorIndex {
/// # Panics
///
/// Will panic if `id` isn't in the data.
pub fn point_position<Upstream: 'static>(&self, id: PointId, data: &Vector<Upstream>) -> DVec2 {
pub fn point_position(&self, id: PointId, data: &Vector) -> DVec2 {
let offset = self.point_to_offset[&id];
data.point_domain.positions()[offset]
}

View File

@ -391,7 +391,7 @@ impl ManipulatorPointId {
/// Attempt to retrieve the manipulator position in layer space (no transformation applied).
#[must_use]
#[track_caller]
pub fn get_position<Upstream: 'static>(&self, vector: &Vector<Upstream>) -> Option<DVec2> {
pub fn get_position(&self, vector: &Vector) -> Option<DVec2> {
match self {
ManipulatorPointId::Anchor(id) => vector.point_domain.position_from_id(*id),
ManipulatorPointId::PrimaryHandle(id) => vector.segment_from_id(*id).and_then(|bezier| bezier.handle_start()),
@ -399,7 +399,7 @@ impl ManipulatorPointId {
}
}
pub fn get_anchor_position<Upstream: 'static>(&self, vector: &Vector<Upstream>) -> Option<DVec2> {
pub fn get_anchor_position(&self, vector: &Vector) -> Option<DVec2> {
match self {
ManipulatorPointId::EndHandle(_) | ManipulatorPointId::PrimaryHandle(_) => self.get_anchor(vector).and_then(|id| vector.point_domain.position_from_id(id)),
_ => self.get_position(vector),
@ -408,7 +408,7 @@ impl ManipulatorPointId {
/// Attempt to get a pair of handles. For an anchor this is the first two handles connected. For a handle it is self and the first opposing handle.
#[must_use]
pub fn get_handle_pair<Upstream: 'static>(self, vector: &Vector<Upstream>) -> Option<[HandleId; 2]> {
pub fn get_handle_pair(self, vector: &Vector) -> Option<[HandleId; 2]> {
match self {
ManipulatorPointId::Anchor(point) => vector.all_connected(point).take(2).collect::<Vec<_>>().try_into().ok(),
ManipulatorPointId::PrimaryHandle(segment) => {
@ -429,7 +429,7 @@ impl ManipulatorPointId {
/// Finds all the connected handles of a point.
/// For an anchor it is all the connected handles.
/// For a handle it is all the handles connected to its corresponding anchor other than the current handle.
pub fn get_all_connected_handles<Upstream: 'static>(self, vector: &Vector<Upstream>) -> Option<Vec<HandleId>> {
pub fn get_all_connected_handles(self, vector: &Vector) -> Option<Vec<HandleId>> {
match self {
ManipulatorPointId::Anchor(point) => {
let connected = vector.all_connected(point).collect::<Vec<_>>();
@ -452,7 +452,7 @@ impl ManipulatorPointId {
/// Attempt to find the closest anchor. If self is already an anchor then it is just self. If it is a start or end handle, then the start or end point is chosen.
#[must_use]
pub fn get_anchor<Upstream: 'static>(self, vector: &Vector<Upstream>) -> Option<PointId> {
pub fn get_anchor(self, vector: &Vector) -> Option<PointId> {
match self {
ManipulatorPointId::Anchor(point) => Some(point),
ManipulatorPointId::PrimaryHandle(segment) => vector.segment_start_from_id(segment),
@ -538,7 +538,7 @@ impl HandleId {
}
/// Calculate the magnitude of the handle from the anchor.
pub fn length<Upstream: 'static>(self, vector: &Vector<Upstream>) -> f64 {
pub fn length(self, vector: &Vector) -> f64 {
let Some(anchor_position) = self.to_manipulator_point().get_anchor_position(vector) else {
// TODO: This was previously an unwrap which was encountered, so this is a temporary way to avoid a crash
return 0.;

View File

@ -846,14 +846,14 @@ struct Faces {
}
#[derive(Debug, Clone, PartialEq)]
pub struct FaceIterator<'a, Upstream> {
vector: &'a Vector<Upstream>,
pub struct FaceIterator<'a> {
vector: &'a Vector,
faces: Faces,
current_face: usize,
}
impl<Upstream> FaceIterator<'_, Upstream> {
fn new<'a>(faces: Faces, vector: &'a Vector<Upstream>) -> FaceIterator<'a, Upstream> {
impl FaceIterator<'_> {
fn new(faces: Faces, vector: &Vector) -> FaceIterator<'_> {
FaceIterator { vector, faces, current_face: 0 }
}
@ -862,7 +862,7 @@ impl<Upstream> FaceIterator<'_, Upstream> {
}
}
impl<Upstream> Iterator for FaceIterator<'_, Upstream> {
impl Iterator for FaceIterator<'_> {
type Item = kurbo::BezPath;
fn next(&mut self) -> Option<Self::Item> {
let start_side = self.faces.face_start.get(self.current_face).copied()?;
@ -916,7 +916,7 @@ impl Faces {
}
}
impl<Upstream> Vector<Upstream> {
impl Vector {
/// Construct a [`kurbo::PathSeg`] by resolving the points from their ids.
fn path_segment_from_index(&self, start: usize, end: usize, handles: BezierHandles) -> PathSeg {
let start = dvec2_to_point(self.point_domain.positions()[start]);
@ -1131,7 +1131,7 @@ impl<Upstream> Vector<Upstream> {
})
}
pub fn build_stroke_path_iter(&self) -> StrokePathIter<'_, Upstream> {
pub fn build_stroke_path_iter(&self) -> StrokePathIter<'_> {
let mut points = vec![StrokePathIterPointMetadata::default(); self.point_domain.ids().len()];
for (segment_index, (&start, &end)) in self.segment_domain.start_point.iter().zip(&self.segment_domain.end_point).enumerate() {
points[start].set(StrokePathIterPointSegmentMetadata::new(segment_index, false));
@ -1239,7 +1239,7 @@ impl<Upstream> Vector<Upstream> {
self.is_branching() && !self.has_regions()
}
pub fn construct_faces(&self) -> FaceIterator<'_, Upstream> {
pub fn construct_faces(&self) -> FaceIterator<'_> {
let mut adjacency: Vec<Vec<FaceSide>> = vec![Vec::new(); self.point_domain.len()];
for (segment_index, (&start, &end)) in self.segment_domain.start_point.iter().zip(&self.segment_domain.end_point).enumerate() {
adjacency[start].push(FaceSide { segment_index, reversed: false });
@ -1361,14 +1361,14 @@ impl StrokePathIterPointMetadata {
}
#[derive(Clone)]
pub struct StrokePathIter<'a, Upstream> {
vector: &'a Vector<Upstream>,
pub struct StrokePathIter<'a> {
vector: &'a Vector,
points: Vec<StrokePathIterPointMetadata>,
skip: usize,
done_one: bool,
}
impl<Upstream> Iterator for StrokePathIter<'_, Upstream> {
impl Iterator for StrokePathIter<'_> {
type Item = (Vec<ManipulatorGroup<PointId>>, bool);
fn next(&mut self) -> Option<Self::Item> {

View File

@ -54,7 +54,7 @@ impl PointModification {
}
/// Create a new modification that will convert an empty [`Vector`] into the target [`Vector`].
pub fn create_from_vector<Upstream>(vector: &Vector<Upstream>) -> Self {
pub fn create_from_vector(vector: &Vector) -> Self {
Self {
add: vector.point_domain.ids().to_vec(),
remove: HashSet::new(),
@ -211,7 +211,7 @@ impl SegmentModification {
}
/// Create a new modification that will convert an empty [`Vector`] into the target [`Vector`].
pub fn create_from_vector<Upstream>(vector: &Vector<Upstream>) -> Self {
pub fn create_from_vector(vector: &Vector) -> Self {
let point_id = |(&segment, &index)| (segment, vector.point_domain.ids()[index]);
Self {
add: vector.segment_domain.ids().to_vec(),
@ -280,7 +280,7 @@ impl RegionModification {
}
/// Create a new modification that will convert an empty [`Vector`] into the target [`Vector`].
pub fn create_from_vector<Upstream>(vector: &Vector<Upstream>) -> Self {
pub fn create_from_vector(vector: &Vector) -> Self {
Self {
add: vector.region_domain.ids().to_vec(),
remove: HashSet::new(),
@ -426,7 +426,7 @@ impl VectorModification {
}
/// Apply this modification to the specified [`Vector`].
pub fn apply<Upstream>(&self, vector: &mut Vector<Upstream>) {
pub fn apply(&self, vector: &mut Vector) {
self.points.apply(&mut vector.point_domain, &mut vector.segment_domain);
self.segments.apply(&mut vector.segment_domain, &vector.point_domain);
self.regions.apply(&mut vector.region_domain);
@ -499,7 +499,7 @@ impl VectorModification {
}
/// Create a new modification that will convert an empty [`Vector`] into the target [`Vector`].
pub fn create_from_vector<Upstream>(vector: &Vector<Upstream>) -> Self {
pub fn create_from_vector(vector: &Vector) -> Self {
Self {
points: PointModification::create_from_vector(vector),
segments: SegmentModification::create_from_vector(vector),
@ -581,7 +581,7 @@ where
deserializer.deserialize_seq(visitor)
}
pub struct AppendBezpath<'a, Upstream: 'static> {
pub struct AppendBezpath<'a> {
first_point: Option<Point>,
last_point: Option<Point>,
first_point_index: Option<usize>,
@ -590,11 +590,11 @@ pub struct AppendBezpath<'a, Upstream: 'static> {
last_segment_id: Option<SegmentId>,
point_id: PointId,
segment_id: SegmentId,
vector: &'a mut Vector<Upstream>,
vector: &'a mut Vector,
}
impl<'a, Upstream> AppendBezpath<'a, Upstream> {
fn new(vector: &'a mut Vector<Upstream>) -> Self {
impl<'a> AppendBezpath<'a> {
fn new(vector: &'a mut Vector) -> Self {
Self {
first_point: None,
last_point: None,
@ -676,7 +676,7 @@ impl<'a, Upstream> AppendBezpath<'a, Upstream> {
self.last_segment_id = None;
}
pub fn append_bezpath(vector: &'a mut Vector<Upstream>, bezpath: BezPath) {
pub fn append_bezpath(vector: &'a mut Vector, bezpath: BezPath) {
let mut this = Self::new(vector);
let mut elements = bezpath.elements().iter().peekable();
@ -726,7 +726,7 @@ pub trait VectorExt {
fn append_bezpath(&mut self, bezpath: BezPath);
}
impl<Upstream: 'static> VectorExt for Vector<Upstream> {
impl VectorExt for Vector {
fn append_bezpath(&mut self, bezpath: BezPath) {
AppendBezpath::append_bezpath(self, bezpath);
}
@ -758,7 +758,7 @@ mod tests {
#[test]
fn modify_new() {
let vector: Vector<()> = Vector::from_subpaths([Subpath::new_ellipse(DVec2::ZERO, DVec2::ONE), Subpath::new_rectangle(DVec2::NEG_ONE, DVec2::ZERO)], false);
let vector: Vector = Vector::from_subpaths([Subpath::new_ellipse(DVec2::ZERO, DVec2::ONE), Subpath::new_rectangle(DVec2::NEG_ONE, DVec2::ZERO)], false);
let modify = VectorModification::create_from_vector(&vector);
@ -780,7 +780,7 @@ mod tests {
false,
),
];
let mut vector: Vector<()> = Vector::from_subpaths(subpaths, false);
let mut vector: Vector = Vector::from_subpaths(subpaths, false);
let mut modify_new = VectorModification::create_from_vector(&vector);
let mut modify_original = VectorModification::default();

View File

@ -16,13 +16,9 @@ use kurbo::{Affine, BezPath, Rect, Shape};
use std::collections::HashMap;
/// Represents vector graphics data, composed of Bézier curves in a path or mesh arrangement.
///
/// Generic over `Upstream` to avoid circular dependency with the Graphic type.
/// - Use `Vector<()>` for basic vectors without upstream tracking
/// - Use `Vector<Option<Table<Graphic>>>` in the graphic crate for vectors with upstream layers
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Vector<Upstream> {
pub struct Vector {
pub style: PathStyle,
/// A list of all manipulator groups (referenced in `subpaths`) that have colinear handles (where they're locked at 180° angles from one another).
@ -32,17 +28,12 @@ pub struct Vector<Upstream> {
pub point_domain: PointDomain,
pub segment_domain: SegmentDomain,
pub region_domain: RegionDomain,
/// Used to store the upstream group/folder of nested layers during destructive Boolean Operations (and other nodes with a similar effect) so that click targets can be preserved for the child layers.
/// Without this, the tools would be working with a collapsed version of the data which has no reference to the original child layers that were booleaned together, resulting in the inner layers not being editable.
#[cfg_attr(feature = "serde", serde(alias = "upstream_group"))]
pub upstream_data: Upstream,
}
unsafe impl<Upstream: 'static> StaticType for Vector<Upstream> {
unsafe impl StaticType for Vector {
type Static = Self;
}
impl<Upstream: Default + 'static> Default for Vector<Upstream> {
impl Default for Vector {
fn default() -> Self {
Self {
style: PathStyle::new(Some(Stroke::new(Some(Color::BLACK), 0.)), super::style::Fill::None),
@ -50,23 +41,21 @@ impl<Upstream: Default + 'static> Default for Vector<Upstream> {
point_domain: PointDomain::new(),
segment_domain: SegmentDomain::new(),
region_domain: RegionDomain::new(),
upstream_data: Upstream::default(),
}
}
}
impl<Upstream> graphene_hash::CacheHash for Vector<Upstream> {
impl graphene_hash::CacheHash for Vector {
fn cache_hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.point_domain.cache_hash(state);
self.segment_domain.cache_hash(state);
self.region_domain.cache_hash(state);
self.style.cache_hash(state);
self.colinear_manipulators.cache_hash(state);
// We don't hash the upstream_data intentionally
}
}
impl<Upstream> Vector<Upstream> {
impl Vector {
/// Add a subpath to this vector path.
pub fn append_subpath(&mut self, subpath: impl Borrow<Subpath<PointId>>, preserve_id: bool) {
let subpath: &Subpath<PointId> = subpath.borrow();
@ -140,28 +129,19 @@ impl<Upstream> Vector<Upstream> {
}
/// Construct some new vector path from a single subpath with an identity transform and black fill.
pub fn from_subpath(subpath: impl Borrow<Subpath<PointId>>) -> Self
where
Upstream: Default + 'static,
{
pub fn from_subpath(subpath: impl Borrow<Subpath<PointId>>) -> Self {
Self::from_subpaths([subpath], false)
}
/// Construct some new vector path from a single [`BezPath`] with an identity transform and black fill.
pub fn from_bezpath(bezpath: BezPath) -> Self
where
Upstream: Default + 'static,
{
pub fn from_bezpath(bezpath: BezPath) -> Self {
let mut vector = Self::default();
vector.append_bezpath(bezpath);
vector
}
/// Construct some new vector path from subpaths with an identity transform and black fill.
pub fn from_subpaths(subpaths: impl IntoIterator<Item = impl Borrow<Subpath<PointId>>>, preserve_id: bool) -> Self
where
Upstream: Default + 'static,
{
pub fn from_subpaths(subpaths: impl IntoIterator<Item = impl Borrow<Subpath<PointId>>>, preserve_id: bool) -> Self {
let mut vector = Self::default();
for subpath in subpaths.into_iter() {
@ -171,10 +151,7 @@ impl<Upstream> Vector<Upstream> {
vector
}
pub fn from_target_types(target_types: impl IntoIterator<Item = impl Borrow<ClickTargetType>>, preserve_id: bool) -> Self
where
Upstream: Default + 'static,
{
pub fn from_target_types(target_types: impl IntoIterator<Item = impl Borrow<ClickTargetType>>, preserve_id: bool) -> Self {
let mut vector = Self::default();
for target_type in target_types.into_iter() {
@ -387,10 +364,7 @@ impl<Upstream> Vector<Upstream> {
}
}
pub fn other_colinear_handle(&self, handle: HandleId) -> Option<HandleId>
where
Upstream: 'static,
{
pub fn other_colinear_handle(&self, handle: HandleId) -> Option<HandleId> {
let pair = self.colinear_manipulators.iter().find(|pair| pair.contains(&handle))?;
let other = pair.iter().copied().find(|&val| val != handle)?;
if handle.to_manipulator_point().get_anchor(self) == other.to_manipulator_point().get_anchor(self) {
@ -471,7 +445,7 @@ impl<Upstream> Vector<Upstream> {
}
}
impl<Upstream> BoundingBox for Vector<Upstream> {
impl BoundingBox for Vector {
fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox {
if !include_stroke {
// Just use the path bounds without stroke
@ -496,7 +470,7 @@ impl<Upstream> BoundingBox for Vector<Upstream> {
}
}
impl<Upstream> RenderComplexity for Vector<Upstream> {
impl RenderComplexity for Vector {
fn render_complexity(&self) -> usize {
self.segment_domain.ids().len()
}
@ -526,7 +500,7 @@ mod tests {
#[test]
fn construct_closed_subpath() {
let circle = Subpath::new_ellipse(DVec2::NEG_ONE, DVec2::ONE);
let vector: Vector<()> = Vector::from_subpath(&circle);
let vector: Vector = Vector::from_subpath(&circle);
assert_eq!(vector.point_domain.ids().len(), 4);
let bezier_paths = vector.segment_iter().map(|(_, bezier, _, _)| bezier).collect::<Vec<_>>();
assert_eq!(bezier_paths.len(), 4);
@ -540,7 +514,7 @@ mod tests {
fn construct_open_subpath() {
let bezier = PathSeg::Cubic(CubicBez::new(Point::ZERO, Point::new(-1., -1.), Point::new(1., 1.), Point::new(1., 0.)));
let subpath = Subpath::from_bezier(bezier);
let vector: Vector<()> = Vector::from_subpath(&subpath);
let vector: Vector = Vector::from_subpath(&subpath);
assert_eq!(vector.point_domain.ids().len(), 2);
let bezier_paths = vector.segment_iter().map(|(_, bezier, _, _)| bezier).collect::<Vec<_>>();
assert_eq!(bezier_paths, vec![bezier]);
@ -555,7 +529,7 @@ mod tests {
let curve = Subpath::from_bezier(curve);
let circle = Subpath::new_ellipse(DVec2::NEG_ONE, DVec2::ONE);
let vector: Vector<()> = Vector::from_subpaths([&curve, &circle], false);
let vector: Vector = Vector::from_subpaths([&curve, &circle], false);
assert_eq!(vector.point_domain.ids().len(), 6);
let bezier_paths = vector.segment_iter().map(|(_, bezier, _, _)| bezier).collect::<Vec<_>>();

View File

@ -276,10 +276,7 @@ async fn brush(
let has_erase_or_restore_strokes = strokes.iter().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)
.with_attribute("alpha_blending", AlphaBlending::default())
.with_attribute("source_node_id", None::<NodeId>);
let mut erase_restore_mask = TableRow::new_from_element(Raster::new_cpu(opaque_image)).with_attribute("transform", background_bounds);
for stroke in strokes {
let mut brush_texture = cache.get_cached_brush(&stroke.style);
@ -313,12 +310,12 @@ async fn brush(
let transform: DAffine2 = actual_image.attribute_cloned_or_default("transform");
let alpha_blending: AlphaBlending = actual_image.attribute_cloned_or_default("alpha_blending");
let source_node_id: Option<NodeId> = actual_image.attribute_cloned_or_default("source_node_id");
let layer: Option<NodeId> = actual_image.attribute_cloned_or_default("editor:layer");
*image.element_mut(0).unwrap() = actual_image.into_element();
image.set_attribute("transform", 0, transform);
image.set_attribute("alpha_blending", 0, alpha_blending);
image.set_attribute("source_node_id", 0, source_node_id);
image.set_attribute("editor:layer", 0, layer);
image
}

View File

@ -64,10 +64,8 @@ impl BrushCacheImpl {
background = std::mem::take(&mut self.blended_image);
// Check if the first non-blended stroke is an extension of the last one.
let mut first_stroke_texture = TableRow::new_from_element(Raster::<CPU>::default())
.with_attribute("transform", glam::DAffine2::ZERO)
.with_attribute("alpha_blending", core_types::AlphaBlending::default())
.with_attribute("source_node_id", None::<core_types::uuid::NodeId>);
// Transform is set to ZERO (not the default IDENTITY) as a sentinel to mark this row as uninitialized.
let mut first_stroke_texture = TableRow::new_from_element(Raster::<CPU>::default()).with_attribute("transform", glam::DAffine2::ZERO);
let mut first_stroke_point_skip = 0;
let strokes = input[num_blended_strokes..].to_vec();
if !strokes.is_empty() && self.prev_input.len() > num_blended_strokes {

View File

@ -27,6 +27,7 @@ async fn context_modification<T>(
Context -> Footprint,
Context -> DVec2,
Context -> Vec<DVec2>,
Context -> Option<NodeId>,
Context -> Vec<NodeId>,
Context -> Vec<f64>,
Context -> Vec<f32>,

View File

@ -177,33 +177,53 @@ where
result_table
}
/// Performs internal editor record-keeping that enables tools to target this network's layer.
/// This node associates the ID of the network's parent layer to every element of output data.
/// This technical detail may be ignored by users, and will be phased out in the future.
/// Returns the NodeId of the user-facing parent layer node that encapsulates this sub-network.
/// 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 async fn source_node_id<T: 'n + Send + Clone>(
_: impl Ctx,
#[implementations(
Table<Artboard>,
Table<Graphic>,
Table<Vector>,
Table<Raster<CPU>>,
Table<Raster<GPU>>,
Table<Color>,
Table<GradientStops>,
)]
content: Table<T>,
node_path: Vec<NodeId>,
) -> Table<T> {
pub fn parent_layer(_: impl Ctx, node_path: Vec<NodeId>) -> Option<NodeId> {
// 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 node (whose network contains this internal node).
let source_node_id = node_path.get(node_path.len().wrapping_sub(2)).copied();
node_path.get(node_path.len().wrapping_sub(2)).copied()
}
let mut content = content;
for source_id in content.iter_attribute_values_mut_or_default::<Option<NodeId>>("source_node_id") {
*source_id = source_node_id;
/// Writes a per-row attribute column on the input table. The value-producing input is evaluated once per row,
/// with the row's element index and the row itself (as a single-row table vararg) passed via context, so the
/// upstream pipeline can return a different value per row that may be derived from the row's own data.
/// If the column already exists, its values are replaced; if not, the column is created.
#[node_macro::node(category("General"))]
async fn write_attribute<T: AnyHash + Clone + Send + Sync + core_types::CacheHash, U: Clone + Send + Sync + Default + std::fmt::Debug + 'static>(
ctx: impl ExtractAll + CloneVarArgs + Ctx,
/// The table whose rows will gain or replace the named attribute column.
#[implementations(
Table<Artboard>, Table<Artboard>, Table<Artboard>, Table<Artboard>, Table<Artboard>, Table<Artboard>, Table<Artboard>,
Table<Graphic>, Table<Graphic>, Table<Graphic>, Table<Graphic>, Table<Graphic>, Table<Graphic>, Table<Graphic>,
Table<Vector>, Table<Vector>, Table<Vector>, Table<Vector>, Table<Vector>, Table<Vector>, Table<Vector>,
Table<Raster<CPU>>, Table<Raster<CPU>>, Table<Raster<CPU>>, Table<Raster<CPU>>, Table<Raster<CPU>>, Table<Raster<CPU>>, Table<Raster<CPU>>,
Table<Raster<GPU>>, Table<Raster<GPU>>, Table<Raster<GPU>>, Table<Raster<GPU>>, Table<Raster<GPU>>, Table<Raster<GPU>>, Table<Raster<GPU>>,
Table<Color>, Table<Color>, Table<Color>, Table<Color>, Table<Color>, Table<Color>, Table<Color>,
Table<GradientStops>, Table<GradientStops>, Table<GradientStops>, Table<GradientStops>, Table<GradientStops>, Table<GradientStops>, Table<GradientStops>,
)]
mut content: Table<T>,
/// The attribute name (column key) to write or replace.
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 -> DVec2, Context -> DAffine2, Context -> Option<NodeId>,
Context -> f64, Context -> u32, Context -> bool, Context -> String, Context -> DVec2, Context -> DAffine2, Context -> Option<NodeId>,
Context -> f64, Context -> u32, Context -> bool, Context -> String, Context -> DVec2, Context -> DAffine2, Context -> Option<NodeId>,
Context -> f64, Context -> u32, Context -> bool, Context -> String, Context -> DVec2, Context -> DAffine2, Context -> Option<NodeId>,
Context -> f64, Context -> u32, Context -> bool, Context -> String, Context -> DVec2, Context -> DAffine2, Context -> Option<NodeId>,
Context -> f64, Context -> u32, Context -> bool, Context -> String, Context -> DVec2, Context -> DAffine2, Context -> Option<NodeId>,
Context -> f64, Context -> u32, Context -> bool, Context -> String, Context -> DVec2, Context -> DAffine2, Context -> Option<NodeId>,
)]
value: impl Node<'n, Context<'static>, Output = U>,
) -> Table<T> {
for index in 0..content.len() {
let row = content.clone_row(index).expect("index is within bounds");
let owned_ctx = OwnedContextImpl::from(ctx.clone()).with_vararg(Box::new(Table::new_from_row(row))).with_index(index);
let v = value.eval(owned_ctx.into_context()).await;
content.set_attribute(&name, index, v);
}
content
}
@ -239,11 +259,11 @@ pub async fn legacy_layer_extend<T: 'n + Send + Clone>(
) -> 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 source_node_id = nested_node_path.get(nested_node_path.len().wrapping_sub(2)).copied();
let layer = nested_node_path.get(nested_node_path.len().wrapping_sub(2)).copied();
let mut base = base;
for mut row in new.into_iter() {
row.set_attribute("source_node_id", source_node_id);
row.set_attribute("editor:layer", layer);
base.push(row);
}

View File

@ -18,6 +18,8 @@ pub use graphene_canvas_utils as canvas_utils;
#[cfg(target_family = "wasm")]
use graphic_types::Graphic;
#[cfg(target_family = "wasm")]
use graphic_types::IntoGraphicTable;
#[cfg(target_family = "wasm")]
use graphic_types::Vector;
use graphic_types::raster_types::Image;
use graphic_types::raster_types::{CPU, Raster};
@ -170,7 +172,7 @@ async fn create_canvas(_: impl Ctx) -> 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 + 'n>(
async fn rasterize<T: WasmNotSend + Clone + 'n>(
_: impl Ctx,
#[implementations(
Table<Vector>,
@ -184,7 +186,7 @@ async fn rasterize<T: WasmNotSend + 'n>(
mut canvas: CanvasHandle,
) -> Table<Raster<CPU>>
where
Table<T>: Render,
Table<T>: Render + Clone + graphic_types::IntoGraphicTable,
{
use core_types::table::TableRow;
use glam::{DAffine2, DVec2};
@ -194,6 +196,10 @@ where
return Table::new();
}
// Snapshot the input as a Table<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_table = data.clone().into_graphic_table();
let mut render = SvgRender::new();
let aabb = Bbox::from_transform(footprint.transform).to_axis_aligned_bbox();
let size = aabb.size();
@ -229,5 +235,9 @@ where
let rasterized = context.get_image_data(0., 0., resolution.x as f64, resolution.y as f64).unwrap();
let image = Image::from_image_data(&rasterized.data().0, resolution.x as u32, resolution.y as u32);
Table::new_from_row(TableRow::new_from_element(Raster::new_cpu(image)).with_attribute("transform", footprint.transform))
Table::new_from_row(
TableRow::new_from_element(Raster::new_cpu(image))
.with_attribute("transform", footprint.transform)
.with_attribute("editor:merged_layers", upstream_graphic_table),
)
}

View File

@ -46,7 +46,10 @@ async fn boolean_operation<I: graphic_types::IntoGraphicTable + 'n + Send + Clon
let result_vector = result_vector_table.element_mut(0).unwrap();
Vector::transform(result_vector, transform);
result_vector.style.set_stroke_transform(DAffine2::IDENTITY);
result_vector.upstream_data = Some(content.clone());
// Snapshot the input layers as the `editor:merged_layers` row attribute so the renderer can recurse into them
// for editor click-target preservation.
result_vector_table.set_attribute("editor:merged_layers", 0, content.clone());
// Clean up the boolean operation result by merging duplicated points
let merge_transform: DAffine2 = result_vector_table.attribute_cloned_or_default("transform", 0);
@ -127,7 +130,6 @@ fn boolean_operation_on_vector_table(vector: &Table<Vector>, boolean_operation:
let copy_from = vector.element(index).unwrap();
let element = Vector {
style: copy_from.style.clone(),
upstream_data: copy_from.upstream_data.clone(),
..Default::default()
};
TableRow::from_parts(element, attributes)
@ -177,7 +179,7 @@ fn flatten_vector(graphic_table: &Table<Graphic>) -> Table<Vector> {
}
Graphic::RasterCPU(image) => {
let parent_transform: DAffine2 = graphic_table.attribute_cloned_or_default("transform", index);
let make_row = |transform, source_node_id, alpha_blending| {
let make_row = |transform, layer, alpha_blending| {
let mut subpath = Subpath::new_rectangle(DVec2::ZERO, DVec2::ONE);
subpath.apply_transform(transform);
@ -186,24 +188,24 @@ fn flatten_vector(graphic_table: &Table<Graphic>) -> Table<Vector> {
TableRow::new_from_element(element)
.with_attribute("alpha_blending", alpha_blending)
.with_attribute("source_node_id", source_node_id)
.with_attribute("editor:layer", layer)
};
// Apply the parent graphic's transform to each raster element, preserving each row's source_node_id
// Apply the parent graphic's transform to each raster element, preserving each row's layer
// and alpha_blending so the boolean op downstream can route clicks (and inherit blending state)
// back to the originating raster layer
(0..image.len())
.map(|i| {
let row_transform: DAffine2 = image.attribute_cloned_or_default("transform", i);
let source_node_id: Option<NodeId> = image.attribute_cloned_or_default("source_node_id", i);
let layer: Option<NodeId> = image.attribute_cloned_or_default("editor:layer", i);
let alpha_blending: AlphaBlending = image.attribute_cloned_or_default("alpha_blending", i);
make_row(parent_transform * row_transform, source_node_id, alpha_blending)
make_row(parent_transform * row_transform, layer, alpha_blending)
})
.collect::<Vec<_>>()
}
Graphic::RasterGPU(image) => {
let parent_transform: DAffine2 = graphic_table.attribute_cloned_or_default("transform", index);
let make_row = |transform, source_node_id, alpha_blending| {
let make_row = |transform, layer, alpha_blending| {
let mut subpath = Subpath::new_rectangle(DVec2::ZERO, DVec2::ONE);
subpath.apply_transform(transform);
@ -212,18 +214,18 @@ fn flatten_vector(graphic_table: &Table<Graphic>) -> Table<Vector> {
TableRow::new_from_element(element)
.with_attribute("alpha_blending", alpha_blending)
.with_attribute("source_node_id", source_node_id)
.with_attribute("editor:layer", layer)
};
// Apply the parent graphic's transform to each raster element, preserving each row's source_node_id
// Apply the parent graphic's transform to each raster element, preserving each row's layer
// and alpha_blending so the boolean op downstream can route clicks (and inherit blending state)
// back to the originating raster layer
(0..image.len())
.map(|i| {
let row_transform: DAffine2 = image.attribute_cloned_or_default("transform", i);
let source_node_id: Option<NodeId> = image.attribute_cloned_or_default("source_node_id", i);
let layer: Option<NodeId> = image.attribute_cloned_or_default("editor:layer", i);
let alpha_blending: AlphaBlending = image.attribute_cloned_or_default("alpha_blending", i);
make_row(parent_transform * row_transform, source_node_id, alpha_blending)
make_row(parent_transform * row_transform, layer, alpha_blending)
})
.collect::<Vec<_>>()
}

View File

@ -335,6 +335,8 @@ pub fn noise_pattern(
return Table::new();
}
let transform = DAffine2::from_translation(offset) * DAffine2::from_scale(size);
let footprint_scale = footprint.scale();
let width = (size.x * footprint_scale.x) as u32;
let height = (size.y * footprint_scale.y) as u32;
@ -376,12 +378,7 @@ pub fn noise_pattern(
}
}
return Table::new_from_row(
TableRow::new_from_element(Raster::new_cpu(image))
.with_attribute("transform", DAffine2::from_translation(offset) * DAffine2::from_scale(size))
.with_attribute("alpha_blending", AlphaBlending::default())
.with_attribute("source_node_id", None::<core_types::uuid::NodeId>),
);
return Table::new_from_row(TableRow::new_from_element(Raster::new_cpu(image)).with_attribute("transform", transform));
}
};
noise.set_noise_type(Some(noise_type));
@ -439,12 +436,7 @@ pub fn noise_pattern(
}
}
Table::new_from_row(
TableRow::new_from_element(Raster::new_cpu(image))
.with_attribute("transform", DAffine2::from_translation(offset) * DAffine2::from_scale(size))
.with_attribute("alpha_blending", AlphaBlending::default())
.with_attribute("source_node_id", None::<core_types::uuid::NodeId>),
)
Table::new_from_row(TableRow::new_from_element(Raster::new_cpu(image)).with_attribute("transform", transform))
}
#[node_macro::node(category("Raster: Pattern"))]
@ -489,9 +481,7 @@ pub fn mandelbrot(ctx: impl ExtractFootprint + Send) -> Table<Raster<CPU>> {
data,
..Default::default()
}))
.with_attribute("transform", DAffine2::from_translation(offset) * DAffine2::from_scale(size))
.with_attribute("alpha_blending", AlphaBlending::default())
.with_attribute("source_node_id", None::<core_types::uuid::NodeId>),
.with_attribute("transform", DAffine2::from_translation(offset) * DAffine2::from_scale(size)),
)
}

View File

@ -1,4 +1,3 @@
use core_types::AlphaBlending;
use core_types::table::{Table, TableRow};
use glam::{DAffine2, DVec2};
use parley::GlyphRun;
@ -10,16 +9,16 @@ use skrifa::{MetadataProvider, OutlineGlyph};
use vector_types::subpath::{ManipulatorGroup, Subpath};
use vector_types::vector::{PointId, Vector};
pub struct PathBuilder<Upstream> {
pub struct PathBuilder {
current_subpath: Subpath<PointId>,
origin: DVec2,
glyph_subpaths: Vec<Subpath<PointId>>,
pub vector_table: Table<Vector<Upstream>>,
pub vector_table: Table<Vector>,
scale: f64,
id: PointId,
}
impl<Upstream: Default + 'static> PathBuilder<Upstream> {
impl PathBuilder {
pub fn new(per_glyph_instances: bool, scale: f64) -> Self {
Self {
current_subpath: Subpath::new(Vec::new(), false),
@ -52,12 +51,8 @@ impl<Upstream: Default + 'static> PathBuilder<Upstream> {
}
if per_glyph_instances {
self.vector_table.push(
TableRow::new_from_element(Vector::from_subpaths(core::mem::take(&mut self.glyph_subpaths), false))
.with_attribute("transform", DAffine2::from_translation(glyph_offset))
.with_attribute("alpha_blending", AlphaBlending::default())
.with_attribute("source_node_id", None::<core_types::uuid::NodeId>),
);
self.vector_table
.push(TableRow::new_from_element(Vector::from_subpaths(core::mem::take(&mut self.glyph_subpaths), false)).with_attribute("transform", DAffine2::from_translation(glyph_offset)));
} else {
for subpath in self.glyph_subpaths.drain(..) {
// Unwrapping here is ok because `self.vector_table` is initialized with a single `Vector` table element
@ -120,7 +115,7 @@ impl<Upstream: Default + 'static> PathBuilder<Upstream> {
}
}
pub fn finalize(mut self) -> Table<Vector<Upstream>> {
pub fn finalize(mut self) -> Table<Vector> {
if self.vector_table.is_empty() {
self.vector_table = Table::new_from_element(Vector::default());
}
@ -128,7 +123,7 @@ impl<Upstream: Default + 'static> PathBuilder<Upstream> {
}
}
impl<Upstream: Default + 'static> OutlinePen for PathBuilder<Upstream> {
impl OutlinePen for PathBuilder {
fn move_to(&mut self, x: f32, y: f32) {
if !self.current_subpath.is_empty() {
self.glyph_subpaths.push(std::mem::replace(&mut self.current_subpath, Subpath::new(Vec::new(), false)));

View File

@ -87,7 +87,7 @@ impl TextContext {
}
/// Convert text to vector paths using the specified font and typesetting configuration
pub fn to_path<Upstream: Default + 'static>(&mut self, text: &str, font: &Font, font_cache: &FontCache, typesetting: TypesettingConfig, per_glyph_instances: bool) -> Table<Vector<Upstream>> {
pub fn to_path(&mut self, text: &str, font: &Font, font_cache: &FontCache, typesetting: TypesettingConfig, per_glyph_instances: bool) -> Table<Vector> {
let Some(layout) = self.layout_text(text, font, font_cache, typesetting) else {
return Table::new_from_element(Vector::default());
};

View File

@ -6,7 +6,7 @@ use parley::fontique::Blob;
use std::sync::Arc;
use vector_types::Vector;
pub fn to_path<Upstream: Default + 'static>(text: &str, font: &Font, font_cache: &FontCache, typesetting: TypesettingConfig, per_glyph_instances: bool) -> Table<Vector<Upstream>> {
pub fn to_path(text: &str, font: &Font, font_cache: &FontCache, typesetting: TypesettingConfig, per_glyph_instances: bool) -> Table<Vector> {
TextContext::with_thread_local(|ctx| ctx.to_path(text, font, font_cache, typesetting, per_glyph_instances))
}

View File

@ -17,8 +17,8 @@ async fn path_modify(_ctx: impl Ctx, mut vector: Table<Vector>, modification: Bo
// Update the source node id
let this_node_path = node_path.iter().rev().nth(1).copied();
let existing: Option<NodeId> = vector.attribute_cloned_or_default("source_node_id", 0);
vector.set_attribute("source_node_id", 0, existing.or(this_node_path));
let existing: Option<NodeId> = vector.attribute_cloned_or_default("editor:layer", 0);
vector.set_attribute("editor:layer", 0, existing.or(this_node_path));
if vector.len() > 1 {
warn!("The path modify ran on {} vector rows. Only the first can be modified.", vector.len());

View File

@ -354,8 +354,6 @@ async fn round_corners(
let attributes = source.clone_row_attributes(index);
let source = source.element(index).unwrap();
let upstream_nested_layers = source.upstream_data.clone();
// Flip the roundness to help with user intuition
let roundness = 1. - roundness;
// Convert 0-100 to 0-0.5
@ -439,8 +437,6 @@ async fn round_corners(
result.append_bezpath(rounded_subpath);
}
result.upstream_data = upstream_nested_layers;
TableRow::from_parts(result, attributes)
})
.collect()
@ -1300,7 +1296,7 @@ pub async fn flatten_path<T: IntoGraphicTable + 'n + Send>(_: impl Ctx, #[implem
// Concatenate every vector element's subpaths into the single output compound path
for index in 0..flattened.len() {
let Some(element) = flattened.element(index) else { continue };
let node_id: Option<NodeId> = flattened.attribute_cloned_or_default("source_node_id", index);
let node_id: Option<NodeId> = flattened.attribute_cloned_or_default("editor:layer", index);
let node_id = node_id.map(|node_id| node_id.0).unwrap_or_default();
let mut hasher = DefaultHasher::new();
@ -1317,13 +1313,13 @@ pub async fn flatten_path<T: IntoGraphicTable + 'n + Send>(_: impl Ctx, #[implem
// Preserve a reference to the original upstream graphic table so the renderer can recurse into it
// when collecting metadata, exposing the original child layers' click targets to editor tools.
// This is the same mechanism Boolean Operation uses to keep its inputs editable after the merge.
output.upstream_data = Some(graphic_table);
output_table.set_attribute("editor:merged_layers", 0, graphic_table);
// Adopt the last input row's source_node_id so the editor can also bucket clicks under a contributing child layer
// Adopt the last input row's layer so the editor can also bucket clicks under a contributing child layer
if !flattened.is_empty() {
let primary = flattened.len() - 1;
let source_node_id: Option<NodeId> = flattened.attribute_cloned_or_default("source_node_id", primary);
output_table.set_attribute("source_node_id", 0, source_node_id);
let layer: Option<NodeId> = flattened.attribute_cloned_or_default("editor:layer", primary);
output_table.set_attribute("editor:layer", 0, layer);
}
output_table
@ -1351,7 +1347,6 @@ async fn sample_polyline(
region_domain: Default::default(),
colinear_manipulators: Default::default(),
style: std::mem::take(&mut row.element_mut().style),
upstream_data: std::mem::take(&mut row.element_mut().upstream_data),
};
// Transfer the stroke transform from the input vector content to the result.
result.style.set_stroke_transform(row.attribute_cloned_or_default("transform"));
@ -1441,7 +1436,6 @@ async fn simplify(
let mut result = Vector {
style: std::mem::take(&mut row.element_mut().style),
upstream_data: std::mem::take(&mut row.element_mut().upstream_data),
..Default::default()
};
@ -1538,7 +1532,6 @@ async fn decimate(
let mut result = Vector {
style: std::mem::take(&mut row.element_mut().style),
upstream_data: std::mem::take(&mut row.element_mut().upstream_data),
..Default::default()
};
@ -2382,13 +2375,13 @@ async fn morph<I: IntoGraphicTable + 'n + Send + Clone>(
trs * skew
};
// Pre-compensate upstream_data transforms so that when collect_metadata applies
// Pre-compensate merged_layers transforms so that when collect_metadata applies
// the row transform (which will be group_transform * lerped_transform after the
// pipeline's Transform node runs), the lerped_transform cancels out and children
// get the correct footprint: parent * group_transform * child_transform.
// Only pre-compensate if the lerped transform is invertible (non-zero determinant).
// A zero determinant can occur when interpolated scale passes through zero (e.g., flipped axes),
// in which case we skip pre-compensation to avoid propagating NaN through upstream_data transforms.
// in which case we skip pre-compensation to avoid propagating NaN through merged_layers transforms.
if lerped_transform.matrix2.determinant().abs() > f64::EPSILON {
let lerped_inverse = lerped_transform.inverse();
for transform in graphic_table_content.iter_attribute_values_mut_or_default::<DAffine2>("transform") {
@ -2404,21 +2397,15 @@ async fn morph<I: IntoGraphicTable + 'n + Send + Clone>(
let mut attributes = content.clone_row_attributes(endpoint_index);
attributes.insert("transform", lerped_transform);
attributes.insert("editor:merged_layers", graphic_table_content);
return Table::new_from_row(TableRow::from_parts(
Vector {
upstream_data: Some(graphic_table_content),
..endpoint_element.clone()
},
attributes,
));
return Table::new_from_row(TableRow::from_parts(endpoint_element.clone(), attributes));
}
let mut vector = Vector {
upstream_data: Some(graphic_table_content),
style: source_element.style.lerp(&target_element.style, time),
..Default::default()
};
vector.style = source_element.style.lerp(&target_element.style, time);
// Work directly with manipulator groups, bypassing the BezPath intermediate representation.
// This avoids the full Vector → BezPath → interpolate → BezPath → Vector roundtrip each frame.
@ -2566,13 +2553,14 @@ async fn morph<I: IntoGraphicTable + 'n + Send + Clone>(
// The result is a synthesis of source and target, so adopt whichever endpoint the result is closer to as
// the click-target identity (so the editor can route clicks back to one of the contributing layers)
let primary_index = if time < 0.5 { source_index } else { target_index };
let source_node_id: Option<NodeId> = content.attribute_cloned_or_default("source_node_id", primary_index);
let layer: Option<NodeId> = content.attribute_cloned_or_default("editor:layer", primary_index);
Table::new_from_row(
TableRow::new_from_element(vector)
.with_attribute("transform", lerped_transform)
.with_attribute("alpha_blending", vector_alpha_blending)
.with_attribute("source_node_id", source_node_id),
.with_attribute("editor:layer", layer)
.with_attribute("editor:merged_layers", graphic_table_content),
)
}
@ -3047,7 +3035,6 @@ async fn centroid(ctx: impl Ctx + CloneVarArgs + ExtractAll, content: impl Node<
#[cfg(test)]
mod test {
use super::*;
use core_types::AlphaBlending;
use core_types::Node;
use kurbo::{CubicBez, Ellipse, Point, Rect};
use std::future::Future;
@ -3073,10 +3060,7 @@ mod test {
fn create_vector_row(bezpath: BezPath, transform: DAffine2) -> TableRow<Vector> {
let mut row = Vector::default();
row.append_bezpath(bezpath);
TableRow::new_from_element(row)
.with_attribute("transform", transform)
.with_attribute("alpha_blending", AlphaBlending::default())
.with_attribute("source_node_id", None::<core_types::uuid::NodeId>)
TableRow::new_from_element(row).with_attribute("transform", transform)
}
#[tokio::test]