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

* Fix click target propagation with the Rasterize node

* Add the 'Write Attribute' node

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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