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:28:31 -07:00
parent 76938eb69a
commit afc2c9178e
27 changed files with 312 additions and 376 deletions
@@ -275,6 +275,8 @@ impl<T: TableRowLayout> TableRowLayout for Table<T> {
Some(format_alpha_blending(value)) Some(format_alpha_blending(value))
} else if let Some(&value) = ty.downcast_ref::<Option<NodeId>>() { } else if let Some(&value) = ty.downcast_ref::<Option<NodeId>>() {
Some(value.map_or_else(|| "-".to_string(), |id| id.to_string())) 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 { } else {
None None
} }
@@ -458,17 +460,6 @@ impl TableRowLayout for Vector {
TextLabel::new("Colinear Handle IDs").narrow(true).widget_instance(), TextLabel::new("Colinear Handle IDs").narrow(true).widget_instance(),
TextLabel::new(colinear).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 => { VectorTableTab::Points => {
table_rows.push(column_headings(&["", "position"])); table_rows.push(column_headings(&["", "position"]));
@@ -219,7 +219,7 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
node_template: NodeTemplate { node_template: NodeTemplate {
document_node: DocumentNode { document_node: DocumentNode {
implementation: DocumentNodeImplementation::Network(NodeNetwork { implementation: DocumentNodeImplementation::Network(NodeNetwork {
exports: vec![NodeInput::node(NodeId(4), 0)], exports: vec![NodeInput::node(NodeId(5), 0)],
nodes: [ nodes: [
// Primary (bottom) input type coercion // Primary (bottom) input type coercion
DocumentNode { DocumentNode {
@@ -233,22 +233,33 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
implementation: DocumentNodeImplementation::ProtoNode(graphic::wrap_graphic::IDENTIFIER), implementation: DocumentNodeImplementation::ProtoNode(graphic::wrap_graphic::IDENTIFIER),
..Default::default() ..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 { DocumentNode {
inputs: vec![NodeInput::node(NodeId(1), 0), NodeInput::Reflection(graph_craft::document::DocumentNodeMetadata::DocumentNodePath)], inputs: vec![NodeInput::Reflection(graph_craft::document::DocumentNodeMetadata::DocumentNodePath)],
implementation: DocumentNodeImplementation::ProtoNode(graphic::source_node_id::IDENTIFIER), 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() ..Default::default()
}, },
// The monitor node is used to display a thumbnail in the UI // The monitor node is used to display a thumbnail in the UI
DocumentNode { DocumentNode {
inputs: vec![NodeInput::node(NodeId(2), 0)], inputs: vec![NodeInput::node(NodeId(3), 0)],
implementation: DocumentNodeImplementation::ProtoNode(memo::monitor::IDENTIFIER), implementation: DocumentNodeImplementation::ProtoNode(memo::monitor::IDENTIFIER),
skip_deduplication: true, skip_deduplication: true,
..Default::default() ..Default::default()
}, },
DocumentNode { DocumentNode {
call_argument: generic!(T), 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), implementation: DocumentNodeImplementation::ProtoNode(graphic::extend::IDENTIFIER),
..Default::default() ..Default::default()
}, },
@@ -272,6 +283,7 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
network_metadata: Some(NodeNetworkMetadata { network_metadata: Some(NodeNetworkMetadata {
persistent_metadata: NodeNetworkPersistentMetadata { persistent_metadata: NodeNetworkPersistentMetadata {
node_metadata: [ node_metadata: [
// 0: to_graphic
DocumentNodeMetadata { DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata { persistent_metadata: DocumentNodePersistentMetadata {
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(-21, -3)), node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(-21, -3)),
@@ -279,6 +291,7 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
}, },
..Default::default() ..Default::default()
}, },
// 1: wrap_graphic
DocumentNodeMetadata { DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata { persistent_metadata: DocumentNodePersistentMetadata {
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(-21, -1)), node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(-21, -1)),
@@ -286,6 +299,15 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
}, },
..Default::default() ..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 { DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata { persistent_metadata: DocumentNodePersistentMetadata {
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(-14, -1)), node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(-14, -1)),
@@ -293,6 +315,7 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
}, },
..Default::default() ..Default::default()
}, },
// 4: monitor
DocumentNodeMetadata { DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata { persistent_metadata: DocumentNodePersistentMetadata {
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(-7, -1)), node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(-7, -1)),
@@ -300,6 +323,7 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
}, },
..Default::default() ..Default::default()
}, },
// 5: extend
DocumentNodeMetadata { DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata { persistent_metadata: DocumentNodePersistentMetadata {
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, -3)), node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, -3)),
@@ -328,7 +352,7 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
node_template: NodeTemplate { node_template: NodeTemplate {
document_node: DocumentNode { document_node: DocumentNode {
implementation: DocumentNodeImplementation::Network(NodeNetwork { implementation: DocumentNodeImplementation::Network(NodeNetwork {
exports: vec![NodeInput::node(NodeId(3), 0)], exports: vec![NodeInput::node(NodeId(4), 0)],
nodes: [ 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 // 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 { DocumentNode {
@@ -344,16 +368,27 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
], ],
..Default::default() ..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 { DocumentNode {
inputs: vec![NodeInput::node(NodeId(0), 0), NodeInput::Reflection(graph_craft::document::DocumentNodeMetadata::DocumentNodePath)], inputs: vec![NodeInput::Reflection(graph_craft::document::DocumentNodeMetadata::DocumentNodePath)],
implementation: DocumentNodeImplementation::ProtoNode(graphic::source_node_id::IDENTIFIER), 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() ..Default::default()
}, },
// The monitor node is used to display a thumbnail in the UI. // The monitor node is used to display a thumbnail in the UI.
// TODO: Check if thumbnail is reversed // TODO: Check if thumbnail is reversed
DocumentNode { DocumentNode {
inputs: vec![NodeInput::node(NodeId(1), 0)], inputs: vec![NodeInput::node(NodeId(2), 0)],
implementation: DocumentNodeImplementation::ProtoNode(memo::monitor::IDENTIFIER), implementation: DocumentNodeImplementation::ProtoNode(memo::monitor::IDENTIFIER),
call_argument: generic!(T), call_argument: generic!(T),
skip_deduplication: true, skip_deduplication: true,
@@ -362,7 +397,7 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
DocumentNode { DocumentNode {
inputs: vec![ inputs: vec![
NodeInput::import(graphene_std::Type::Fn(Box::new(concrete!(Context)), Box::new(concrete!(Table<Artboard>))), 0), 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), implementation: DocumentNodeImplementation::ProtoNode(graphic::extend::IDENTIFIER),
..Default::default() ..Default::default()
@@ -418,6 +453,7 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
network_metadata: Some(NodeNetworkMetadata { network_metadata: Some(NodeNetworkMetadata {
persistent_metadata: NodeNetworkPersistentMetadata { persistent_metadata: NodeNetworkPersistentMetadata {
node_metadata: [ node_metadata: [
// 0: create_artboard
DocumentNodeMetadata { DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata { persistent_metadata: DocumentNodePersistentMetadata {
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(-21, -3)), node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(-21, -3)),
@@ -425,6 +461,15 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
}, },
..Default::default() ..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 { DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata { persistent_metadata: DocumentNodePersistentMetadata {
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(-14, -3)), node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(-14, -3)),
@@ -432,6 +477,7 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
}, },
..Default::default() ..Default::default()
}, },
// 3: monitor
DocumentNodeMetadata { DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata { persistent_metadata: DocumentNodePersistentMetadata {
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(-7, -3)), node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(-7, -3)),
@@ -439,6 +485,7 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
}, },
..Default::default() ..Default::default()
}, },
// 4: extend
DocumentNodeMetadata { DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata { persistent_metadata: DocumentNodePersistentMetadata {
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, -4)), node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, -4)),
@@ -169,10 +169,6 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[
"graphene_core::graphic::LegacyLayerExtendNode", "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 { NodeReplacement {
node: graphene_std::graphic::to_graphic::IDENTIFIER, node: graphene_std::graphic::to_graphic::IDENTIFIER,
aliases: &[ aliases: &[
@@ -1024,6 +1020,16 @@ pub fn document_migration_reset_node_definition(document_serialized_content: &st
return true; 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 false
} }
@@ -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::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 => 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<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<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 => 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 => [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 => 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<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<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 => [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 => 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::Vec2]),
+1 -4
View File
@@ -366,10 +366,7 @@ pub fn simplify_identifier_name(ty: &str) -> String {
} }
pub fn make_type_user_readable(ty: &str) -> String { pub fn make_type_user_readable(ty: &str) -> String {
ty.replace("Option<Arc<OwnedContextImpl>>", "Context") ty.replace("Option<Arc<OwnedContextImpl>>", "Context").replace("Raster<CPU>", "Raster").replace("Raster<GPU>", "Raster")
.replace("Vector<Option<Table<Graphic>>>", "Vector")
.replace("Raster<CPU>", "Raster")
.replace("Raster<GPU>", "Raster")
} }
impl std::fmt::Debug for Type { impl std::fmt::Debug for Type {
@@ -113,30 +113,11 @@ pub fn migrate_artboard<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Re
alpha_blending: Vec<AlphaBlending>, 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)? { Ok(match ArtboardFormat::deserialize(deserializer)? {
ArtboardFormat::ArtboardGroup(artboard_group) => { ArtboardFormat::ArtboardGroup(artboard_group) => artboard_group.artboards.into_iter().map(|(artboard, _)| TableRow::new_from_element(artboard)).collect(),
let mut table = Table::new(); ArtboardFormat::OldArtboardTable(old_table) => old_table.element.into_iter().map(TableRow::new_from_element).collect(),
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::ArtboardTable(artboard_table) => artboard_table, ArtboardFormat::ArtboardTable(artboard_table) => artboard_table,
}) })
} }
@@ -12,7 +12,7 @@ use raster_types::{CPU, GPU, Raster};
use vector_types::GradientStops; use vector_types::GradientStops;
// use vector_types::Vector; // 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. /// 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)] #[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>>) { 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() { 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_transform: DAffine2 = current_graphic_row.attribute_cloned_or_default("transform");
let current_alpha_blending: AlphaBlending = current_graphic_row.attribute_cloned_or_default("alpha_blending"); 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("transform", current_transform * row_transform);
attributes.insert("alpha_blending", compose_alpha_blending(current_alpha_blending, row_alpha_blending)); 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)); 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), 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)? { Ok(match GraphicFormat::deserialize(deserializer)? {
GraphicFormat::OldGraphicGroup(old) => { GraphicFormat::OldGraphicGroup(old) => old.elements.into_iter().map(|(graphic, _)| TableRow::new_from_element(graphic)).collect(),
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::OlderTableOldGraphicGroup(old) => old GraphicFormat::OlderTableOldGraphicGroup(old) => old
.element .element
.into_iter() .into_iter()
.flat_map(|element| { .flat_map(|element| element.elements.into_iter().map(|(graphic, _)| TableRow::new_from_element(graphic)))
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)
})
})
.collect(), .collect(),
GraphicFormat::OldTableOldGraphicGroup(old) => old GraphicFormat::OldTableOldGraphicGroup(old) => old
.element .element
.into_iter() .into_iter()
.flat_map(|element| { .flat_map(|element| element.elements.into_iter().map(|(graphic, _)| TableRow::new_from_element(graphic)))
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)
})
})
.collect(), .collect(),
GraphicFormat::OldTableGraphicGroup(old) => old GraphicFormat::OldTableGraphicGroup(old) => old
.element .element
.into_iter() .into_iter()
.flat_map(|element| { .flat_map(|element| element.elements.into_iter().map(|(graphic, _)| TableRow::new_from_element(graphic)))
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)
})
})
.collect(), .collect(),
GraphicFormat::Table(value) => { GraphicFormat::Table(value) => {
// Try to deserialize as either table format // Try to deserialize as either table format
if let Ok(old_table) = serde_json::from_value::<Table<GraphicGroup>>(value.clone()) { if let Ok(old_table) = serde_json::from_value::<Table<GraphicGroup>>(value.clone()) {
let mut graphic_table = Table::new(); let mut graphic_table = Table::new();
for index in 0..old_table.len() { for index in 0..old_table.len() {
let row_transform: DAffine2 = old_table.attribute_cloned_or_default("transform", index); for (graphic, _) in &old_table.element(index).unwrap().elements {
let row_alpha_blending: AlphaBlending = old_table.attribute_cloned_or_default("alpha_blending", index); graphic_table.push(TableRow::new_from_element(graphic.clone()));
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),
);
} }
} }
graphic_table graphic_table
+11 -34
View File
@@ -72,40 +72,17 @@ pub mod migrations {
Ok(match VectorFormat::deserialize(deserializer)? { Ok(match VectorFormat::deserialize(deserializer)? {
VectorFormat::Vector(vector) => Table::new_from_element(vector), VectorFormat::Vector(vector) => Table::new_from_element(vector),
VectorFormat::OldVectorData(old) => { // Attributes (transform, alpha_blending, editor:layer) are not serialized, so migration only needs
let mut vector_table = Table::new_from_element(Vector { // to recover the elements. Per-row attribute values are populated at runtime by the node graph.
style: old.style, VectorFormat::OldVectorData(old) => Table::new_from_element(Vector {
colinear_manipulators: old.colinear_manipulators, style: old.style,
point_domain: old.point_domain, colinear_manipulators: old.colinear_manipulators,
segment_domain: old.segment_domain, point_domain: old.point_domain,
region_domain: old.region_domain, segment_domain: old.segment_domain,
upstream_data: old.upstream_graphic_group, region_domain: old.region_domain,
}); }),
vector_table.set_attribute("transform", 0, old.transform); VectorFormat::OlderVectorTable(older_table) => older_table.element.into_iter().map(TableRow::new_from_element).collect(),
vector_table.set_attribute("alpha_blending", 0, old.alpha_blending); VectorFormat::OldVectorTable(old_table) => old_table.element.into_iter().map(TableRow::new_from_element).collect(),
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(),
VectorFormat::VectorTable(vector_table) => vector_table, VectorFormat::VectorTable(vector_table) => vector_table,
}) })
} }
+9 -33
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())) 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> { fn old_table_to_new_table<T>(old_table: OldTable<T>) -> Table<T> {
old_table old_table.element.into_iter().map(TableRow::new_from_element).collect()
.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()
} }
fn older_table_to_new_table<T>(old_table: OlderTable<T>) -> Table<T> { fn older_table_to_new_table<T>(old_table: OlderTable<T>) -> Table<T> {
old_table old_table.element.into_iter().map(TableRow::new_from_element).collect()
.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()
} }
fn from_image_frame_table(image_frame: Table<ImageFrame<Color>>) -> Table<Raster<CPU>> { 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>>), 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)? { Ok(match FormatVersions::deserialize(deserializer)? {
FormatVersions::Image(image) => TableRow::new_from_element(Raster::new_cpu(image)) FormatVersions::Image(image) => TableRow::new_from_element(Raster::new_cpu(image)),
.with_attribute("transform", DAffine2::IDENTITY) FormatVersions::OldImageFrame(old) => TableRow::new_from_element(Raster::new_cpu(old.image)),
.with_attribute("alpha_blending", AlphaBlending::default()) FormatVersions::ImageFrameTable(image_frame) => TableRow::new_from_element(Raster::new_cpu(image_frame.element(0).unwrap().image.clone())),
.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::RasterTable(image_frame_table) => image_frame_table.into_iter().next().unwrap_or_default(), FormatVersions::RasterTable(image_frame_table) => image_frame_table.into_iter().next().unwrap_or_default(),
FormatVersions::RasterTableRow(image_table_row) => image_table_row, FormatVersions::RasterTableRow(image_table_row) => image_table_row,
}) })
+47 -21
View File
@@ -412,10 +412,10 @@ impl Render for Graphic {
metadata.upstream_footprints.insert(element_id, footprint); metadata.upstream_footprints.insert(element_id, footprint);
// TODO: Find a way to handle more than the first row // TODO: Find a way to handle more than the first row
if !table.is_empty() { 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); 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); 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>) { fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, _element_id: Option<NodeId>) {
for index in 0..self.len() { for index in 0..self.len() {
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);
self.element(index).unwrap().collect_metadata(metadata, footprint, source_node_id); 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>) { fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option<NodeId>) {
for index in 0..self.len() { for index in 0..self.len() {
let row_transform: DAffine2 = self.attribute_cloned_or_default("transform", index); 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 element = self.element(index).unwrap();
let mut footprint = footprint; let mut footprint = footprint;
footprint.transform *= row_transform; 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)); element.collect_metadata(metadata, footprint, Some(element_id));
} else { } 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); 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>) { 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"); let (elements, layers) = self.element_and_attribute_slices_mut::<Option<NodeId>>("editor:layer");
for (element, source_node_id) in elements.iter_mut().zip(source_node_ids.iter()) { for (element, layer) in elements.iter_mut().zip(layers.iter()) {
element.new_ids_from_hash(*source_node_id); element.new_ids_from_hash(*layer);
} }
} }
} }
@@ -938,8 +938,7 @@ impl Render for Table<Vector> {
let vector_row = Table::new_from_row( let vector_row = Table::new_from_row(
TableRow::new_from_element(cloned_vector) TableRow::new_from_element(cloned_vector)
.with_attribute("transform", multiplied_transform) .with_attribute("transform", multiplied_transform)
.with_attribute("alpha_blending", alpha_blending) .with_attribute("alpha_blending", alpha_blending),
.with_attribute("source_node_id", None::<NodeId>),
); );
(id, mask_type, vector_row) (id, mask_type, vector_row)
@@ -1256,8 +1255,7 @@ impl Render for Table<Vector> {
let vector_table = Table::new_from_row( let vector_table = Table::new_from_row(
TableRow::new_from_element(cloned_element) TableRow::new_from_element(cloned_element)
.with_attribute("transform", row_transform) .with_attribute("transform", row_transform)
.with_attribute("alpha_blending", alpha_blending) .with_attribute("alpha_blending", alpha_blending),
.with_attribute("source_node_id", None::<NodeId>),
); );
let bounds = element.bounding_box_with_transform(multiplied_transform).unwrap_or(layer_bounds); 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() { for index in 0..self.len() {
let Some(vector) = self.element(index) else { continue }; let Some(vector) = self.element(index) else { continue };
let transform: DAffine2 = self.attribute_cloned_or_default("transform", index); 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) { if let Some(element_id) = caller_element_id.or(layer) {
// When recovering element_id from the row's source_node_id (because the caller // 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 // passed None), also store the transform metadata that Graphic::collect_metadata
// normally provides but skipped due to the None element_id. // normally provides but skipped due to the None element_id.
if caller_element_id.is_none() { if caller_element_id.is_none() {
@@ -1365,7 +1363,7 @@ impl Render for Table<Vector> {
.stroke_bezier_paths() .stroke_bezier_paths()
.map(fill) .map(fill)
.map(|subpath| ClickTarget::new_with_subpath(subpath, stroke_width).into()) .map(|subpath| ClickTarget::new_with_subpath(subpath, stroke_width).into())
.chain(single_anchors_targets.into_iter()) .chain(single_anchors_targets)
.collect::<Vec<_>>(); .collect::<Vec<_>>();
metadata.click_targets.entry(element_id).or_insert(click_targets); 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())); 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; let mut upstream_footprint = footprint;
upstream_footprint.transform *= transform; upstream_footprint.transform *= transform;
upstream_nested_layers.collect_metadata(metadata, upstream_footprint, None); 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); metadata.upstream_footprints.insert(element_id, footprint);
// TODO: Find a way to handle more than one row of the raster table // TODO: Find a way to handle more than one row of the raster table
if !self.is_empty() { 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); metadata.upstream_footprints.insert(element_id, footprint);
// TODO: Find a way to handle more than one row of the raster table // TODO: Find a way to handle more than one row of the raster table
if !self.is_empty() { 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);
}
} }
} }
@@ -11,7 +11,7 @@ pub trait MergeByDistanceExt {
fn merge_by_distance_spatial(&mut self, transform: DAffine2, distance: f64); 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) { fn merge_by_distance_topological(&mut self, distance: f64) {
// Treat self as an undirected graph // Treat self as an undirected graph
let indices = VectorIndex::build_from(self); let indices = VectorIndex::build_from(self);
@@ -237,7 +237,7 @@ pub struct VectorIndex {
impl VectorIndex { impl VectorIndex {
/// Construct a [`VectorIndex`] by building indexes from the given [`Vector`]. Takes `O(n)` time. /// 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 point_to_offset = data.point_domain.ids().iter().copied().enumerate().map(|(a, b)| (b, a)).collect::<FxHashMap<_, _>>();
let mut point_to_node = FxHashMap::default(); let mut point_to_node = FxHashMap::default();
@@ -295,7 +295,7 @@ impl VectorIndex {
/// # Panics /// # Panics
/// ///
/// Will panic if `id` isn't in the data. /// 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]; let offset = self.point_to_offset[&id];
data.point_domain.positions()[offset] data.point_domain.positions()[offset]
} }
@@ -391,7 +391,7 @@ impl ManipulatorPointId {
/// Attempt to retrieve the manipulator position in layer space (no transformation applied). /// Attempt to retrieve the manipulator position in layer space (no transformation applied).
#[must_use] #[must_use]
#[track_caller] #[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 { match self {
ManipulatorPointId::Anchor(id) => vector.point_domain.position_from_id(*id), ManipulatorPointId::Anchor(id) => vector.point_domain.position_from_id(*id),
ManipulatorPointId::PrimaryHandle(id) => vector.segment_from_id(*id).and_then(|bezier| bezier.handle_start()), 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 { match self {
ManipulatorPointId::EndHandle(_) | ManipulatorPointId::PrimaryHandle(_) => self.get_anchor(vector).and_then(|id| vector.point_domain.position_from_id(id)), ManipulatorPointId::EndHandle(_) | ManipulatorPointId::PrimaryHandle(_) => self.get_anchor(vector).and_then(|id| vector.point_domain.position_from_id(id)),
_ => self.get_position(vector), _ => 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. /// 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] #[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 { match self {
ManipulatorPointId::Anchor(point) => vector.all_connected(point).take(2).collect::<Vec<_>>().try_into().ok(), ManipulatorPointId::Anchor(point) => vector.all_connected(point).take(2).collect::<Vec<_>>().try_into().ok(),
ManipulatorPointId::PrimaryHandle(segment) => { ManipulatorPointId::PrimaryHandle(segment) => {
@@ -429,7 +429,7 @@ impl ManipulatorPointId {
/// Finds all the connected handles of a point. /// Finds all the connected handles of a point.
/// For an anchor it is all the connected handles. /// 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. /// 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 { match self {
ManipulatorPointId::Anchor(point) => { ManipulatorPointId::Anchor(point) => {
let connected = vector.all_connected(point).collect::<Vec<_>>(); 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. /// 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] #[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 { match self {
ManipulatorPointId::Anchor(point) => Some(point), ManipulatorPointId::Anchor(point) => Some(point),
ManipulatorPointId::PrimaryHandle(segment) => vector.segment_start_from_id(segment), ManipulatorPointId::PrimaryHandle(segment) => vector.segment_start_from_id(segment),
@@ -538,7 +538,7 @@ impl HandleId {
} }
/// Calculate the magnitude of the handle from the anchor. /// 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 { 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 // TODO: This was previously an unwrap which was encountered, so this is a temporary way to avoid a crash
return 0.; return 0.;
@@ -846,14 +846,14 @@ struct Faces {
} }
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub struct FaceIterator<'a, Upstream> { pub struct FaceIterator<'a> {
vector: &'a Vector<Upstream>, vector: &'a Vector,
faces: Faces, faces: Faces,
current_face: usize, current_face: usize,
} }
impl<Upstream> FaceIterator<'_, Upstream> { impl FaceIterator<'_> {
fn new<'a>(faces: Faces, vector: &'a Vector<Upstream>) -> FaceIterator<'a, Upstream> { fn new(faces: Faces, vector: &Vector) -> FaceIterator<'_> {
FaceIterator { vector, faces, current_face: 0 } 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; type Item = kurbo::BezPath;
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
let start_side = self.faces.face_start.get(self.current_face).copied()?; 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. /// Construct a [`kurbo::PathSeg`] by resolving the points from their ids.
fn path_segment_from_index(&self, start: usize, end: usize, handles: BezierHandles) -> PathSeg { fn path_segment_from_index(&self, start: usize, end: usize, handles: BezierHandles) -> PathSeg {
let start = dvec2_to_point(self.point_domain.positions()[start]); 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()]; 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() { 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)); points[start].set(StrokePathIterPointSegmentMetadata::new(segment_index, false));
@@ -1239,7 +1239,7 @@ impl<Upstream> Vector<Upstream> {
self.is_branching() && !self.has_regions() 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()]; 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() { 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 }); adjacency[start].push(FaceSide { segment_index, reversed: false });
@@ -1361,14 +1361,14 @@ impl StrokePathIterPointMetadata {
} }
#[derive(Clone)] #[derive(Clone)]
pub struct StrokePathIter<'a, Upstream> { pub struct StrokePathIter<'a> {
vector: &'a Vector<Upstream>, vector: &'a Vector,
points: Vec<StrokePathIterPointMetadata>, points: Vec<StrokePathIterPointMetadata>,
skip: usize, skip: usize,
done_one: bool, done_one: bool,
} }
impl<Upstream> Iterator for StrokePathIter<'_, Upstream> { impl Iterator for StrokePathIter<'_> {
type Item = (Vec<ManipulatorGroup<PointId>>, bool); type Item = (Vec<ManipulatorGroup<PointId>>, bool);
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
@@ -54,7 +54,7 @@ impl PointModification {
} }
/// Create a new modification that will convert an empty [`Vector`] into the target [`Vector`]. /// 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 { Self {
add: vector.point_domain.ids().to_vec(), add: vector.point_domain.ids().to_vec(),
remove: HashSet::new(), remove: HashSet::new(),
@@ -211,7 +211,7 @@ impl SegmentModification {
} }
/// Create a new modification that will convert an empty [`Vector`] into the target [`Vector`]. /// 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]); let point_id = |(&segment, &index)| (segment, vector.point_domain.ids()[index]);
Self { Self {
add: vector.segment_domain.ids().to_vec(), 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`]. /// 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 { Self {
add: vector.region_domain.ids().to_vec(), add: vector.region_domain.ids().to_vec(),
remove: HashSet::new(), remove: HashSet::new(),
@@ -426,7 +426,7 @@ impl VectorModification {
} }
/// Apply this modification to the specified [`Vector`]. /// 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.points.apply(&mut vector.point_domain, &mut vector.segment_domain);
self.segments.apply(&mut vector.segment_domain, &vector.point_domain); self.segments.apply(&mut vector.segment_domain, &vector.point_domain);
self.regions.apply(&mut vector.region_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`]. /// 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 { Self {
points: PointModification::create_from_vector(vector), points: PointModification::create_from_vector(vector),
segments: SegmentModification::create_from_vector(vector), segments: SegmentModification::create_from_vector(vector),
@@ -581,7 +581,7 @@ where
deserializer.deserialize_seq(visitor) deserializer.deserialize_seq(visitor)
} }
pub struct AppendBezpath<'a, Upstream: 'static> { pub struct AppendBezpath<'a> {
first_point: Option<Point>, first_point: Option<Point>,
last_point: Option<Point>, last_point: Option<Point>,
first_point_index: Option<usize>, first_point_index: Option<usize>,
@@ -590,11 +590,11 @@ pub struct AppendBezpath<'a, Upstream: 'static> {
last_segment_id: Option<SegmentId>, last_segment_id: Option<SegmentId>,
point_id: PointId, point_id: PointId,
segment_id: SegmentId, segment_id: SegmentId,
vector: &'a mut Vector<Upstream>, vector: &'a mut Vector,
} }
impl<'a, Upstream> AppendBezpath<'a, Upstream> { impl<'a> AppendBezpath<'a> {
fn new(vector: &'a mut Vector<Upstream>) -> Self { fn new(vector: &'a mut Vector) -> Self {
Self { Self {
first_point: None, first_point: None,
last_point: None, last_point: None,
@@ -676,7 +676,7 @@ impl<'a, Upstream> AppendBezpath<'a, Upstream> {
self.last_segment_id = None; 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 this = Self::new(vector);
let mut elements = bezpath.elements().iter().peekable(); let mut elements = bezpath.elements().iter().peekable();
@@ -726,7 +726,7 @@ pub trait VectorExt {
fn append_bezpath(&mut self, bezpath: BezPath); 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) { fn append_bezpath(&mut self, bezpath: BezPath) {
AppendBezpath::append_bezpath(self, bezpath); AppendBezpath::append_bezpath(self, bezpath);
} }
@@ -758,7 +758,7 @@ mod tests {
#[test] #[test]
fn modify_new() { 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); let modify = VectorModification::create_from_vector(&vector);
@@ -780,7 +780,7 @@ mod tests {
false, 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_new = VectorModification::create_from_vector(&vector);
let mut modify_original = VectorModification::default(); let mut modify_original = VectorModification::default();
@@ -16,13 +16,9 @@ use kurbo::{Affine, BezPath, Rect, Shape};
use std::collections::HashMap; use std::collections::HashMap;
/// Represents vector graphics data, composed of Bézier curves in a path or mesh arrangement. /// 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)] #[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Vector<Upstream> { pub struct Vector {
pub style: PathStyle, 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). /// 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 point_domain: PointDomain,
pub segment_domain: SegmentDomain, pub segment_domain: SegmentDomain,
pub region_domain: RegionDomain, 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; type Static = Self;
} }
impl<Upstream: Default + 'static> Default for Vector<Upstream> { impl Default for Vector {
fn default() -> Self { fn default() -> Self {
Self { Self {
style: PathStyle::new(Some(Stroke::new(Some(Color::BLACK), 0.)), super::style::Fill::None), 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(), point_domain: PointDomain::new(),
segment_domain: SegmentDomain::new(), segment_domain: SegmentDomain::new(),
region_domain: RegionDomain::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) { fn cache_hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.point_domain.cache_hash(state); self.point_domain.cache_hash(state);
self.segment_domain.cache_hash(state); self.segment_domain.cache_hash(state);
self.region_domain.cache_hash(state); self.region_domain.cache_hash(state);
self.style.cache_hash(state); self.style.cache_hash(state);
self.colinear_manipulators.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. /// Add a subpath to this vector path.
pub fn append_subpath(&mut self, subpath: impl Borrow<Subpath<PointId>>, preserve_id: bool) { pub fn append_subpath(&mut self, subpath: impl Borrow<Subpath<PointId>>, preserve_id: bool) {
let subpath: &Subpath<PointId> = subpath.borrow(); 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. /// 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 pub fn from_subpath(subpath: impl Borrow<Subpath<PointId>>) -> Self {
where
Upstream: Default + 'static,
{
Self::from_subpaths([subpath], false) Self::from_subpaths([subpath], false)
} }
/// Construct some new vector path from a single [`BezPath`] with an identity transform and black fill. /// Construct some new vector path from a single [`BezPath`] with an identity transform and black fill.
pub fn from_bezpath(bezpath: BezPath) -> Self pub fn from_bezpath(bezpath: BezPath) -> Self {
where
Upstream: Default + 'static,
{
let mut vector = Self::default(); let mut vector = Self::default();
vector.append_bezpath(bezpath); vector.append_bezpath(bezpath);
vector vector
} }
/// Construct some new vector path from subpaths with an identity transform and black fill. /// 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 pub fn from_subpaths(subpaths: impl IntoIterator<Item = impl Borrow<Subpath<PointId>>>, preserve_id: bool) -> Self {
where
Upstream: Default + 'static,
{
let mut vector = Self::default(); let mut vector = Self::default();
for subpath in subpaths.into_iter() { for subpath in subpaths.into_iter() {
@@ -171,10 +151,7 @@ impl<Upstream> Vector<Upstream> {
vector vector
} }
pub fn from_target_types(target_types: impl IntoIterator<Item = impl Borrow<ClickTargetType>>, preserve_id: bool) -> Self pub fn from_target_types(target_types: impl IntoIterator<Item = impl Borrow<ClickTargetType>>, preserve_id: bool) -> Self {
where
Upstream: Default + 'static,
{
let mut vector = Self::default(); let mut vector = Self::default();
for target_type in target_types.into_iter() { 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> pub fn other_colinear_handle(&self, handle: HandleId) -> Option<HandleId> {
where
Upstream: 'static,
{
let pair = self.colinear_manipulators.iter().find(|pair| pair.contains(&handle))?; let pair = self.colinear_manipulators.iter().find(|pair| pair.contains(&handle))?;
let other = pair.iter().copied().find(|&val| val != 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) { 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 { fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox {
if !include_stroke { if !include_stroke {
// Just use the path bounds without 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 { fn render_complexity(&self) -> usize {
self.segment_domain.ids().len() self.segment_domain.ids().len()
} }
@@ -526,7 +500,7 @@ mod tests {
#[test] #[test]
fn construct_closed_subpath() { fn construct_closed_subpath() {
let circle = Subpath::new_ellipse(DVec2::NEG_ONE, DVec2::ONE); 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); assert_eq!(vector.point_domain.ids().len(), 4);
let bezier_paths = vector.segment_iter().map(|(_, bezier, _, _)| bezier).collect::<Vec<_>>(); let bezier_paths = vector.segment_iter().map(|(_, bezier, _, _)| bezier).collect::<Vec<_>>();
assert_eq!(bezier_paths.len(), 4); assert_eq!(bezier_paths.len(), 4);
@@ -540,7 +514,7 @@ mod tests {
fn construct_open_subpath() { 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 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 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); assert_eq!(vector.point_domain.ids().len(), 2);
let bezier_paths = vector.segment_iter().map(|(_, bezier, _, _)| bezier).collect::<Vec<_>>(); let bezier_paths = vector.segment_iter().map(|(_, bezier, _, _)| bezier).collect::<Vec<_>>();
assert_eq!(bezier_paths, vec![bezier]); assert_eq!(bezier_paths, vec![bezier]);
@@ -555,7 +529,7 @@ mod tests {
let curve = Subpath::from_bezier(curve); let curve = Subpath::from_bezier(curve);
let circle = Subpath::new_ellipse(DVec2::NEG_ONE, DVec2::ONE); 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); assert_eq!(vector.point_domain.ids().len(), 6);
let bezier_paths = vector.segment_iter().map(|(_, bezier, _, _)| bezier).collect::<Vec<_>>(); let bezier_paths = vector.segment_iter().map(|(_, bezier, _, _)| bezier).collect::<Vec<_>>();
+3 -6
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)); 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 { if has_erase_or_restore_strokes {
let opaque_image = Image::new(bbox.size().x as u32, bbox.size().y as u32, Color::WHITE); 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)) let mut erase_restore_mask = TableRow::new_from_element(Raster::new_cpu(opaque_image)).with_attribute("transform", background_bounds);
.with_attribute("transform", background_bounds)
.with_attribute("alpha_blending", AlphaBlending::default())
.with_attribute("source_node_id", None::<NodeId>);
for stroke in strokes { for stroke in strokes {
let mut brush_texture = cache.get_cached_brush(&stroke.style); 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 transform: DAffine2 = actual_image.attribute_cloned_or_default("transform");
let alpha_blending: AlphaBlending = actual_image.attribute_cloned_or_default("alpha_blending"); 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.element_mut(0).unwrap() = actual_image.into_element();
image.set_attribute("transform", 0, transform); image.set_attribute("transform", 0, transform);
image.set_attribute("alpha_blending", 0, alpha_blending); 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 image
} }
+2 -4
View File
@@ -64,10 +64,8 @@ impl BrushCacheImpl {
background = std::mem::take(&mut self.blended_image); background = std::mem::take(&mut self.blended_image);
// Check if the first non-blended stroke is an extension of the last one. // 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()) // Transform is set to ZERO (not the default IDENTITY) as a sentinel to mark this row as uninitialized.
.with_attribute("transform", glam::DAffine2::ZERO) 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>);
let mut first_stroke_point_skip = 0; let mut first_stroke_point_skip = 0;
let strokes = input[num_blended_strokes..].to_vec(); let strokes = input[num_blended_strokes..].to_vec();
if !strokes.is_empty() && self.prev_input.len() > num_blended_strokes { if !strokes.is_empty() && self.prev_input.len() > num_blended_strokes {
@@ -27,6 +27,7 @@ async fn context_modification<T>(
Context -> Footprint, Context -> Footprint,
Context -> DVec2, Context -> DVec2,
Context -> Vec<DVec2>, Context -> Vec<DVec2>,
Context -> Option<NodeId>,
Context -> Vec<NodeId>, Context -> Vec<NodeId>,
Context -> Vec<f64>, Context -> Vec<f64>,
Context -> Vec<f32>, Context -> Vec<f32>,
+45 -25
View File
@@ -177,33 +177,53 @@ where
result_table result_table
} }
/// Performs internal editor record-keeping that enables tools to target this network's layer. /// Returns the NodeId of the user-facing parent layer node that encapsulates this sub-network.
/// This node associates the ID of the network's parent layer to every element of output data. /// Used as the value source for stamping the `editor:layer` attribute on each row of a layer's output,
/// This technical detail may be ignored by users, and will be phased out in the future. /// which lets editor tools (e.g. selection, click target routing) trace data back to its owning layer.
#[node_macro::node(category(""))] #[node_macro::node(category(""))]
pub async fn source_node_id<T: 'n + Send + Clone>( pub fn parent_layer(_: impl Ctx, node_path: Vec<NodeId>) -> Option<NodeId> {
_: 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> {
// Get the penultimate element of the node path, or None if the path is too short // 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). node_path.get(node_path.len().wrapping_sub(2)).copied()
let source_node_id = node_path.get(node_path.len().wrapping_sub(2)).copied(); }
let mut content = content; /// Writes a per-row attribute column on the input table. The value-producing input is evaluated once per row,
for source_id in content.iter_attribute_values_mut_or_default::<Option<NodeId>>("source_node_id") { /// with the row's element index and the row itself (as a single-row table vararg) passed via context, so the
*source_id = source_node_id; /// 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 content
} }
@@ -239,11 +259,11 @@ pub async fn legacy_layer_extend<T: 'n + Send + Clone>(
) -> Table<T> { ) -> Table<T> {
// Get the penultimate element of the node path, or None if the path is too short // 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). // 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; let mut base = base;
for mut row in new.into_iter() { 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); base.push(row);
} }
@@ -18,6 +18,8 @@ pub use graphene_canvas_utils as canvas_utils;
#[cfg(target_family = "wasm")] #[cfg(target_family = "wasm")]
use graphic_types::Graphic; use graphic_types::Graphic;
#[cfg(target_family = "wasm")] #[cfg(target_family = "wasm")]
use graphic_types::IntoGraphicTable;
#[cfg(target_family = "wasm")]
use graphic_types::Vector; use graphic_types::Vector;
use graphic_types::raster_types::Image; use graphic_types::raster_types::Image;
use graphic_types::raster_types::{CPU, Raster}; 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*. /// Renders a view of the input graphic within an area defined by the *Footprint*.
#[cfg(target_family = "wasm")] #[cfg(target_family = "wasm")]
#[node_macro::node(category(""))] #[node_macro::node(category(""))]
async fn rasterize<T: WasmNotSend + 'n>( async fn rasterize<T: WasmNotSend + Clone + 'n>(
_: impl Ctx, _: impl Ctx,
#[implementations( #[implementations(
Table<Vector>, Table<Vector>,
@@ -184,7 +186,7 @@ async fn rasterize<T: WasmNotSend + 'n>(
mut canvas: CanvasHandle, mut canvas: CanvasHandle,
) -> Table<Raster<CPU>> ) -> Table<Raster<CPU>>
where where
Table<T>: Render, Table<T>: Render + Clone + graphic_types::IntoGraphicTable,
{ {
use core_types::table::TableRow; use core_types::table::TableRow;
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
@@ -194,6 +196,10 @@ where
return Table::new(); 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 mut render = SvgRender::new();
let aabb = Bbox::from_transform(footprint.transform).to_axis_aligned_bbox(); let aabb = Bbox::from_transform(footprint.transform).to_axis_aligned_bbox();
let size = aabb.size(); 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 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); 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),
)
} }
+14 -12
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(); let result_vector = result_vector_table.element_mut(0).unwrap();
Vector::transform(result_vector, transform); Vector::transform(result_vector, transform);
result_vector.style.set_stroke_transform(DAffine2::IDENTITY); 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 // Clean up the boolean operation result by merging duplicated points
let merge_transform: DAffine2 = result_vector_table.attribute_cloned_or_default("transform", 0); 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 copy_from = vector.element(index).unwrap();
let element = Vector { let element = Vector {
style: copy_from.style.clone(), style: copy_from.style.clone(),
upstream_data: copy_from.upstream_data.clone(),
..Default::default() ..Default::default()
}; };
TableRow::from_parts(element, attributes) TableRow::from_parts(element, attributes)
@@ -177,7 +179,7 @@ fn flatten_vector(graphic_table: &Table<Graphic>) -> Table<Vector> {
} }
Graphic::RasterCPU(image) => { Graphic::RasterCPU(image) => {
let parent_transform: DAffine2 = graphic_table.attribute_cloned_or_default("transform", index); 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); let mut subpath = Subpath::new_rectangle(DVec2::ZERO, DVec2::ONE);
subpath.apply_transform(transform); subpath.apply_transform(transform);
@@ -186,24 +188,24 @@ fn flatten_vector(graphic_table: &Table<Graphic>) -> Table<Vector> {
TableRow::new_from_element(element) TableRow::new_from_element(element)
.with_attribute("alpha_blending", alpha_blending) .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) // and alpha_blending so the boolean op downstream can route clicks (and inherit blending state)
// back to the originating raster layer // back to the originating raster layer
(0..image.len()) (0..image.len())
.map(|i| { .map(|i| {
let row_transform: DAffine2 = image.attribute_cloned_or_default("transform", 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); 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<_>>() .collect::<Vec<_>>()
} }
Graphic::RasterGPU(image) => { Graphic::RasterGPU(image) => {
let parent_transform: DAffine2 = graphic_table.attribute_cloned_or_default("transform", index); 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); let mut subpath = Subpath::new_rectangle(DVec2::ZERO, DVec2::ONE);
subpath.apply_transform(transform); subpath.apply_transform(transform);
@@ -212,18 +214,18 @@ fn flatten_vector(graphic_table: &Table<Graphic>) -> Table<Vector> {
TableRow::new_from_element(element) TableRow::new_from_element(element)
.with_attribute("alpha_blending", alpha_blending) .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) // and alpha_blending so the boolean op downstream can route clicks (and inherit blending state)
// back to the originating raster layer // back to the originating raster layer
(0..image.len()) (0..image.len())
.map(|i| { .map(|i| {
let row_transform: DAffine2 = image.attribute_cloned_or_default("transform", 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); 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<_>>() .collect::<Vec<_>>()
} }
+5 -15
View File
@@ -335,6 +335,8 @@ pub fn noise_pattern(
return Table::new(); return Table::new();
} }
let transform = DAffine2::from_translation(offset) * DAffine2::from_scale(size);
let footprint_scale = footprint.scale(); let footprint_scale = footprint.scale();
let width = (size.x * footprint_scale.x) as u32; let width = (size.x * footprint_scale.x) as u32;
let height = (size.y * footprint_scale.y) as u32; let height = (size.y * footprint_scale.y) as u32;
@@ -376,12 +378,7 @@ pub fn noise_pattern(
} }
} }
return Table::new_from_row( return Table::new_from_row(TableRow::new_from_element(Raster::new_cpu(image)).with_attribute("transform", transform));
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>),
);
} }
}; };
noise.set_noise_type(Some(noise_type)); noise.set_noise_type(Some(noise_type));
@@ -439,12 +436,7 @@ pub fn noise_pattern(
} }
} }
Table::new_from_row( Table::new_from_row(TableRow::new_from_element(Raster::new_cpu(image)).with_attribute("transform", transform))
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>),
)
} }
#[node_macro::node(category("Raster: Pattern"))] #[node_macro::node(category("Raster: Pattern"))]
@@ -489,9 +481,7 @@ pub fn mandelbrot(ctx: impl ExtractFootprint + Send) -> Table<Raster<CPU>> {
data, data,
..Default::default() ..Default::default()
})) }))
.with_attribute("transform", DAffine2::from_translation(offset) * DAffine2::from_scale(size)) .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>),
) )
} }
+7 -12
View File
@@ -1,4 +1,3 @@
use core_types::AlphaBlending;
use core_types::table::{Table, TableRow}; use core_types::table::{Table, TableRow};
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
use parley::GlyphRun; use parley::GlyphRun;
@@ -10,16 +9,16 @@ use skrifa::{MetadataProvider, OutlineGlyph};
use vector_types::subpath::{ManipulatorGroup, Subpath}; use vector_types::subpath::{ManipulatorGroup, Subpath};
use vector_types::vector::{PointId, Vector}; use vector_types::vector::{PointId, Vector};
pub struct PathBuilder<Upstream> { pub struct PathBuilder {
current_subpath: Subpath<PointId>, current_subpath: Subpath<PointId>,
origin: DVec2, origin: DVec2,
glyph_subpaths: Vec<Subpath<PointId>>, glyph_subpaths: Vec<Subpath<PointId>>,
pub vector_table: Table<Vector<Upstream>>, pub vector_table: Table<Vector>,
scale: f64, scale: f64,
id: PointId, id: PointId,
} }
impl<Upstream: Default + 'static> PathBuilder<Upstream> { impl PathBuilder {
pub fn new(per_glyph_instances: bool, scale: f64) -> Self { pub fn new(per_glyph_instances: bool, scale: f64) -> Self {
Self { Self {
current_subpath: Subpath::new(Vec::new(), false), current_subpath: Subpath::new(Vec::new(), false),
@@ -52,12 +51,8 @@ impl<Upstream: Default + 'static> PathBuilder<Upstream> {
} }
if per_glyph_instances { if per_glyph_instances {
self.vector_table.push( self.vector_table
TableRow::new_from_element(Vector::from_subpaths(core::mem::take(&mut self.glyph_subpaths), false)) .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("transform", DAffine2::from_translation(glyph_offset))
.with_attribute("alpha_blending", AlphaBlending::default())
.with_attribute("source_node_id", None::<core_types::uuid::NodeId>),
);
} else { } else {
for subpath in self.glyph_subpaths.drain(..) { for subpath in self.glyph_subpaths.drain(..) {
// Unwrapping here is ok because `self.vector_table` is initialized with a single `Vector` table element // 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() { if self.vector_table.is_empty() {
self.vector_table = Table::new_from_element(Vector::default()); 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) { fn move_to(&mut self, x: f32, y: f32) {
if !self.current_subpath.is_empty() { if !self.current_subpath.is_empty() {
self.glyph_subpaths.push(std::mem::replace(&mut self.current_subpath, Subpath::new(Vec::new(), false))); self.glyph_subpaths.push(std::mem::replace(&mut self.current_subpath, Subpath::new(Vec::new(), false)));
+1 -1
View File
@@ -87,7 +87,7 @@ impl TextContext {
} }
/// Convert text to vector paths using the specified font and typesetting configuration /// 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 { let Some(layout) = self.layout_text(text, font, font_cache, typesetting) else {
return Table::new_from_element(Vector::default()); return Table::new_from_element(Vector::default());
}; };
+1 -1
View File
@@ -6,7 +6,7 @@ use parley::fontique::Blob;
use std::sync::Arc; use std::sync::Arc;
use vector_types::Vector; 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)) TextContext::with_thread_local(|ctx| ctx.to_path(text, font, font_cache, typesetting, per_glyph_instances))
} }
@@ -17,8 +17,8 @@ async fn path_modify(_ctx: impl Ctx, mut vector: Table<Vector>, modification: Bo
// Update the source node id // Update the source node id
let this_node_path = node_path.iter().rev().nth(1).copied(); let this_node_path = node_path.iter().rev().nth(1).copied();
let existing: Option<NodeId> = vector.attribute_cloned_or_default("source_node_id", 0); let existing: Option<NodeId> = vector.attribute_cloned_or_default("editor:layer", 0);
vector.set_attribute("source_node_id", 0, existing.or(this_node_path)); vector.set_attribute("editor:layer", 0, existing.or(this_node_path));
if vector.len() > 1 { if vector.len() > 1 {
warn!("The path modify ran on {} vector rows. Only the first can be modified.", vector.len()); warn!("The path modify ran on {} vector rows. Only the first can be modified.", vector.len());
+14 -30
View File
@@ -354,8 +354,6 @@ async fn round_corners(
let attributes = source.clone_row_attributes(index); let attributes = source.clone_row_attributes(index);
let source = source.element(index).unwrap(); let source = source.element(index).unwrap();
let upstream_nested_layers = source.upstream_data.clone();
// Flip the roundness to help with user intuition // Flip the roundness to help with user intuition
let roundness = 1. - roundness; let roundness = 1. - roundness;
// Convert 0-100 to 0-0.5 // Convert 0-100 to 0-0.5
@@ -439,8 +437,6 @@ async fn round_corners(
result.append_bezpath(rounded_subpath); result.append_bezpath(rounded_subpath);
} }
result.upstream_data = upstream_nested_layers;
TableRow::from_parts(result, attributes) TableRow::from_parts(result, attributes)
}) })
.collect() .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 // Concatenate every vector element's subpaths into the single output compound path
for index in 0..flattened.len() { for index in 0..flattened.len() {
let Some(element) = flattened.element(index) else { continue }; 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 node_id = node_id.map(|node_id| node_id.0).unwrap_or_default();
let mut hasher = DefaultHasher::new(); 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 // 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. // 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. // 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() { if !flattened.is_empty() {
let primary = flattened.len() - 1; let primary = flattened.len() - 1;
let source_node_id: Option<NodeId> = flattened.attribute_cloned_or_default("source_node_id", primary); let layer: Option<NodeId> = flattened.attribute_cloned_or_default("editor:layer", primary);
output_table.set_attribute("source_node_id", 0, source_node_id); output_table.set_attribute("editor:layer", 0, layer);
} }
output_table output_table
@@ -1351,7 +1347,6 @@ async fn sample_polyline(
region_domain: Default::default(), region_domain: Default::default(),
colinear_manipulators: Default::default(), colinear_manipulators: Default::default(),
style: std::mem::take(&mut row.element_mut().style), 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. // Transfer the stroke transform from the input vector content to the result.
result.style.set_stroke_transform(row.attribute_cloned_or_default("transform")); result.style.set_stroke_transform(row.attribute_cloned_or_default("transform"));
@@ -1441,7 +1436,6 @@ async fn simplify(
let mut result = Vector { let mut result = Vector {
style: std::mem::take(&mut row.element_mut().style), style: std::mem::take(&mut row.element_mut().style),
upstream_data: std::mem::take(&mut row.element_mut().upstream_data),
..Default::default() ..Default::default()
}; };
@@ -1538,7 +1532,6 @@ async fn decimate(
let mut result = Vector { let mut result = Vector {
style: std::mem::take(&mut row.element_mut().style), style: std::mem::take(&mut row.element_mut().style),
upstream_data: std::mem::take(&mut row.element_mut().upstream_data),
..Default::default() ..Default::default()
}; };
@@ -2382,13 +2375,13 @@ async fn morph<I: IntoGraphicTable + 'n + Send + Clone>(
trs * skew 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 // the row transform (which will be group_transform * lerped_transform after the
// pipeline's Transform node runs), the lerped_transform cancels out and children // pipeline's Transform node runs), the lerped_transform cancels out and children
// get the correct footprint: parent * group_transform * child_transform. // get the correct footprint: parent * group_transform * child_transform.
// Only pre-compensate if the lerped transform is invertible (non-zero determinant). // 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), // 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 { if lerped_transform.matrix2.determinant().abs() > f64::EPSILON {
let lerped_inverse = lerped_transform.inverse(); let lerped_inverse = lerped_transform.inverse();
for transform in graphic_table_content.iter_attribute_values_mut_or_default::<DAffine2>("transform") { 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); let mut attributes = content.clone_row_attributes(endpoint_index);
attributes.insert("transform", lerped_transform); attributes.insert("transform", lerped_transform);
attributes.insert("editor:merged_layers", graphic_table_content);
return Table::new_from_row(TableRow::from_parts( return Table::new_from_row(TableRow::from_parts(endpoint_element.clone(), attributes));
Vector {
upstream_data: Some(graphic_table_content),
..endpoint_element.clone()
},
attributes,
));
} }
let mut vector = Vector { let mut vector = Vector {
upstream_data: Some(graphic_table_content), style: source_element.style.lerp(&target_element.style, time),
..Default::default() ..Default::default()
}; };
vector.style = source_element.style.lerp(&target_element.style, time);
// Work directly with manipulator groups, bypassing the BezPath intermediate representation. // Work directly with manipulator groups, bypassing the BezPath intermediate representation.
// This avoids the full Vector → BezPath → interpolate → BezPath → Vector roundtrip each frame. // 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 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) // 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 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( Table::new_from_row(
TableRow::new_from_element(vector) TableRow::new_from_element(vector)
.with_attribute("transform", lerped_transform) .with_attribute("transform", lerped_transform)
.with_attribute("alpha_blending", vector_alpha_blending) .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)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;
use core_types::AlphaBlending;
use core_types::Node; use core_types::Node;
use kurbo::{CubicBez, Ellipse, Point, Rect}; use kurbo::{CubicBez, Ellipse, Point, Rect};
use std::future::Future; use std::future::Future;
@@ -3073,10 +3060,7 @@ mod test {
fn create_vector_row(bezpath: BezPath, transform: DAffine2) -> TableRow<Vector> { fn create_vector_row(bezpath: BezPath, transform: DAffine2) -> TableRow<Vector> {
let mut row = Vector::default(); let mut row = Vector::default();
row.append_bezpath(bezpath); row.append_bezpath(bezpath);
TableRow::new_from_element(row) TableRow::new_from_element(row).with_attribute("transform", transform)
.with_attribute("transform", transform)
.with_attribute("alpha_blending", AlphaBlending::default())
.with_attribute("source_node_id", None::<core_types::uuid::NodeId>)
} }
#[tokio::test] #[tokio::test]