mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-18 18:38:05 +08:00
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:
@@ -276,10 +276,7 @@ async fn brush(
|
||||
let has_erase_or_restore_strokes = strokes.iter().any(|s| matches!(s.style.blend_mode, BlendMode::Erase | BlendMode::Restore));
|
||||
if has_erase_or_restore_strokes {
|
||||
let opaque_image = Image::new(bbox.size().x as u32, bbox.size().y as u32, Color::WHITE);
|
||||
let mut erase_restore_mask = TableRow::new_from_element(Raster::new_cpu(opaque_image))
|
||||
.with_attribute("transform", background_bounds)
|
||||
.with_attribute("alpha_blending", AlphaBlending::default())
|
||||
.with_attribute("source_node_id", None::<NodeId>);
|
||||
let mut erase_restore_mask = TableRow::new_from_element(Raster::new_cpu(opaque_image)).with_attribute("transform", background_bounds);
|
||||
|
||||
for stroke in strokes {
|
||||
let mut brush_texture = cache.get_cached_brush(&stroke.style);
|
||||
@@ -313,12 +310,12 @@ async fn brush(
|
||||
|
||||
let transform: DAffine2 = actual_image.attribute_cloned_or_default("transform");
|
||||
let alpha_blending: AlphaBlending = actual_image.attribute_cloned_or_default("alpha_blending");
|
||||
let source_node_id: Option<NodeId> = actual_image.attribute_cloned_or_default("source_node_id");
|
||||
let layer: Option<NodeId> = actual_image.attribute_cloned_or_default("editor:layer");
|
||||
|
||||
*image.element_mut(0).unwrap() = actual_image.into_element();
|
||||
image.set_attribute("transform", 0, transform);
|
||||
image.set_attribute("alpha_blending", 0, alpha_blending);
|
||||
image.set_attribute("source_node_id", 0, source_node_id);
|
||||
image.set_attribute("editor:layer", 0, layer);
|
||||
|
||||
image
|
||||
}
|
||||
|
||||
@@ -64,10 +64,8 @@ impl BrushCacheImpl {
|
||||
background = std::mem::take(&mut self.blended_image);
|
||||
|
||||
// Check if the first non-blended stroke is an extension of the last one.
|
||||
let mut first_stroke_texture = TableRow::new_from_element(Raster::<CPU>::default())
|
||||
.with_attribute("transform", glam::DAffine2::ZERO)
|
||||
.with_attribute("alpha_blending", core_types::AlphaBlending::default())
|
||||
.with_attribute("source_node_id", None::<core_types::uuid::NodeId>);
|
||||
// Transform is set to ZERO (not the default IDENTITY) as a sentinel to mark this row as uninitialized.
|
||||
let mut first_stroke_texture = TableRow::new_from_element(Raster::<CPU>::default()).with_attribute("transform", glam::DAffine2::ZERO);
|
||||
let mut first_stroke_point_skip = 0;
|
||||
let strokes = input[num_blended_strokes..].to_vec();
|
||||
if !strokes.is_empty() && self.prev_input.len() > num_blended_strokes {
|
||||
|
||||
@@ -27,6 +27,7 @@ async fn context_modification<T>(
|
||||
Context -> Footprint,
|
||||
Context -> DVec2,
|
||||
Context -> Vec<DVec2>,
|
||||
Context -> Option<NodeId>,
|
||||
Context -> Vec<NodeId>,
|
||||
Context -> Vec<f64>,
|
||||
Context -> Vec<f32>,
|
||||
|
||||
@@ -177,33 +177,53 @@ where
|
||||
result_table
|
||||
}
|
||||
|
||||
/// Performs internal editor record-keeping that enables tools to target this network's layer.
|
||||
/// This node associates the ID of the network's parent layer to every element of output data.
|
||||
/// This technical detail may be ignored by users, and will be phased out in the future.
|
||||
/// Returns the NodeId of the user-facing parent layer node that encapsulates this sub-network.
|
||||
/// Used as the value source for stamping the `editor:layer` attribute on each row of a layer's output,
|
||||
/// which lets editor tools (e.g. selection, click target routing) trace data back to its owning layer.
|
||||
#[node_macro::node(category(""))]
|
||||
pub async fn source_node_id<T: 'n + Send + Clone>(
|
||||
_: impl Ctx,
|
||||
#[implementations(
|
||||
Table<Artboard>,
|
||||
Table<Graphic>,
|
||||
Table<Vector>,
|
||||
Table<Raster<CPU>>,
|
||||
Table<Raster<GPU>>,
|
||||
Table<Color>,
|
||||
Table<GradientStops>,
|
||||
)]
|
||||
content: Table<T>,
|
||||
node_path: Vec<NodeId>,
|
||||
) -> Table<T> {
|
||||
pub fn parent_layer(_: impl Ctx, node_path: Vec<NodeId>) -> Option<NodeId> {
|
||||
// Get the penultimate element of the node path, or None if the path is too short
|
||||
// This is used to get the ID of the user-facing parent layer node (whose network contains this internal node).
|
||||
let source_node_id = node_path.get(node_path.len().wrapping_sub(2)).copied();
|
||||
node_path.get(node_path.len().wrapping_sub(2)).copied()
|
||||
}
|
||||
|
||||
let mut content = content;
|
||||
for source_id in content.iter_attribute_values_mut_or_default::<Option<NodeId>>("source_node_id") {
|
||||
*source_id = source_node_id;
|
||||
/// Writes a per-row attribute column on the input table. The value-producing input is evaluated once per row,
|
||||
/// with the row's element index and the row itself (as a single-row table vararg) passed via context, so the
|
||||
/// upstream pipeline can return a different value per row that may be derived from the row's own data.
|
||||
/// If the column already exists, its values are replaced; if not, the column is created.
|
||||
#[node_macro::node(category("General"))]
|
||||
async fn write_attribute<T: AnyHash + Clone + Send + Sync + core_types::CacheHash, U: Clone + Send + Sync + Default + std::fmt::Debug + 'static>(
|
||||
ctx: impl ExtractAll + CloneVarArgs + Ctx,
|
||||
/// The table whose rows will gain or replace the named attribute column.
|
||||
#[implementations(
|
||||
Table<Artboard>, Table<Artboard>, Table<Artboard>, Table<Artboard>, Table<Artboard>, Table<Artboard>, Table<Artboard>,
|
||||
Table<Graphic>, Table<Graphic>, Table<Graphic>, Table<Graphic>, Table<Graphic>, Table<Graphic>, Table<Graphic>,
|
||||
Table<Vector>, Table<Vector>, Table<Vector>, Table<Vector>, Table<Vector>, Table<Vector>, Table<Vector>,
|
||||
Table<Raster<CPU>>, Table<Raster<CPU>>, Table<Raster<CPU>>, Table<Raster<CPU>>, Table<Raster<CPU>>, Table<Raster<CPU>>, Table<Raster<CPU>>,
|
||||
Table<Raster<GPU>>, Table<Raster<GPU>>, Table<Raster<GPU>>, Table<Raster<GPU>>, Table<Raster<GPU>>, Table<Raster<GPU>>, Table<Raster<GPU>>,
|
||||
Table<Color>, Table<Color>, Table<Color>, Table<Color>, Table<Color>, Table<Color>, Table<Color>,
|
||||
Table<GradientStops>, Table<GradientStops>, Table<GradientStops>, Table<GradientStops>, Table<GradientStops>, Table<GradientStops>, Table<GradientStops>,
|
||||
)]
|
||||
mut content: Table<T>,
|
||||
/// The attribute name (column key) to write or replace.
|
||||
name: String,
|
||||
/// The node that produces the per-row value. Called once per row with the row index in context.
|
||||
#[implementations(
|
||||
Context -> f64, Context -> u32, Context -> bool, Context -> String, Context -> DVec2, Context -> DAffine2, Context -> Option<NodeId>,
|
||||
Context -> f64, Context -> u32, Context -> bool, Context -> String, Context -> DVec2, Context -> DAffine2, Context -> Option<NodeId>,
|
||||
Context -> f64, Context -> u32, Context -> bool, Context -> String, Context -> DVec2, Context -> DAffine2, Context -> Option<NodeId>,
|
||||
Context -> f64, Context -> u32, Context -> bool, Context -> String, Context -> DVec2, Context -> DAffine2, Context -> Option<NodeId>,
|
||||
Context -> f64, Context -> u32, Context -> bool, Context -> String, Context -> DVec2, Context -> DAffine2, Context -> Option<NodeId>,
|
||||
Context -> f64, Context -> u32, Context -> bool, Context -> String, Context -> DVec2, Context -> DAffine2, Context -> Option<NodeId>,
|
||||
Context -> f64, Context -> u32, Context -> bool, Context -> String, Context -> DVec2, Context -> DAffine2, Context -> Option<NodeId>,
|
||||
)]
|
||||
value: impl Node<'n, Context<'static>, Output = U>,
|
||||
) -> Table<T> {
|
||||
for index in 0..content.len() {
|
||||
let row = content.clone_row(index).expect("index is within bounds");
|
||||
let owned_ctx = OwnedContextImpl::from(ctx.clone()).with_vararg(Box::new(Table::new_from_row(row))).with_index(index);
|
||||
let v = value.eval(owned_ctx.into_context()).await;
|
||||
content.set_attribute(&name, index, v);
|
||||
}
|
||||
|
||||
content
|
||||
}
|
||||
|
||||
@@ -239,11 +259,11 @@ pub async fn legacy_layer_extend<T: 'n + Send + Clone>(
|
||||
) -> Table<T> {
|
||||
// Get the penultimate element of the node path, or None if the path is too short
|
||||
// This is used to get the ID of the user-facing parent layer-style node (which encapsulates this internal node).
|
||||
let source_node_id = nested_node_path.get(nested_node_path.len().wrapping_sub(2)).copied();
|
||||
let layer = nested_node_path.get(nested_node_path.len().wrapping_sub(2)).copied();
|
||||
|
||||
let mut base = base;
|
||||
for mut row in new.into_iter() {
|
||||
row.set_attribute("source_node_id", source_node_id);
|
||||
row.set_attribute("editor:layer", layer);
|
||||
base.push(row);
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@ pub use graphene_canvas_utils as canvas_utils;
|
||||
#[cfg(target_family = "wasm")]
|
||||
use graphic_types::Graphic;
|
||||
#[cfg(target_family = "wasm")]
|
||||
use graphic_types::IntoGraphicTable;
|
||||
#[cfg(target_family = "wasm")]
|
||||
use graphic_types::Vector;
|
||||
use graphic_types::raster_types::Image;
|
||||
use graphic_types::raster_types::{CPU, Raster};
|
||||
@@ -170,7 +172,7 @@ async fn create_canvas(_: impl Ctx) -> CanvasHandle {
|
||||
/// Renders a view of the input graphic within an area defined by the *Footprint*.
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[node_macro::node(category(""))]
|
||||
async fn rasterize<T: WasmNotSend + 'n>(
|
||||
async fn rasterize<T: WasmNotSend + Clone + 'n>(
|
||||
_: impl Ctx,
|
||||
#[implementations(
|
||||
Table<Vector>,
|
||||
@@ -184,7 +186,7 @@ async fn rasterize<T: WasmNotSend + 'n>(
|
||||
mut canvas: CanvasHandle,
|
||||
) -> Table<Raster<CPU>>
|
||||
where
|
||||
Table<T>: Render,
|
||||
Table<T>: Render + Clone + graphic_types::IntoGraphicTable,
|
||||
{
|
||||
use core_types::table::TableRow;
|
||||
use glam::{DAffine2, DVec2};
|
||||
@@ -194,6 +196,10 @@ where
|
||||
return Table::new();
|
||||
}
|
||||
|
||||
// Snapshot the input as a Table<Graphic> so the renderer can recurse into the original child layers
|
||||
// when collecting metadata, exposing their click targets to editor tools (same mechanism as Boolean Operation).
|
||||
let upstream_graphic_table = data.clone().into_graphic_table();
|
||||
|
||||
let mut render = SvgRender::new();
|
||||
let aabb = Bbox::from_transform(footprint.transform).to_axis_aligned_bbox();
|
||||
let size = aabb.size();
|
||||
@@ -229,5 +235,9 @@ where
|
||||
let rasterized = context.get_image_data(0., 0., resolution.x as f64, resolution.y as f64).unwrap();
|
||||
|
||||
let image = Image::from_image_data(&rasterized.data().0, resolution.x as u32, resolution.y as u32);
|
||||
Table::new_from_row(TableRow::new_from_element(Raster::new_cpu(image)).with_attribute("transform", footprint.transform))
|
||||
Table::new_from_row(
|
||||
TableRow::new_from_element(Raster::new_cpu(image))
|
||||
.with_attribute("transform", footprint.transform)
|
||||
.with_attribute("editor:merged_layers", upstream_graphic_table),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -46,7 +46,10 @@ async fn boolean_operation<I: graphic_types::IntoGraphicTable + 'n + Send + Clon
|
||||
let result_vector = result_vector_table.element_mut(0).unwrap();
|
||||
Vector::transform(result_vector, transform);
|
||||
result_vector.style.set_stroke_transform(DAffine2::IDENTITY);
|
||||
result_vector.upstream_data = Some(content.clone());
|
||||
|
||||
// Snapshot the input layers as the `editor:merged_layers` row attribute so the renderer can recurse into them
|
||||
// for editor click-target preservation.
|
||||
result_vector_table.set_attribute("editor:merged_layers", 0, content.clone());
|
||||
|
||||
// Clean up the boolean operation result by merging duplicated points
|
||||
let merge_transform: DAffine2 = result_vector_table.attribute_cloned_or_default("transform", 0);
|
||||
@@ -127,7 +130,6 @@ fn boolean_operation_on_vector_table(vector: &Table<Vector>, boolean_operation:
|
||||
let copy_from = vector.element(index).unwrap();
|
||||
let element = Vector {
|
||||
style: copy_from.style.clone(),
|
||||
upstream_data: copy_from.upstream_data.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
TableRow::from_parts(element, attributes)
|
||||
@@ -177,7 +179,7 @@ fn flatten_vector(graphic_table: &Table<Graphic>) -> Table<Vector> {
|
||||
}
|
||||
Graphic::RasterCPU(image) => {
|
||||
let parent_transform: DAffine2 = graphic_table.attribute_cloned_or_default("transform", index);
|
||||
let make_row = |transform, source_node_id, alpha_blending| {
|
||||
let make_row = |transform, layer, alpha_blending| {
|
||||
let mut subpath = Subpath::new_rectangle(DVec2::ZERO, DVec2::ONE);
|
||||
subpath.apply_transform(transform);
|
||||
|
||||
@@ -186,24 +188,24 @@ fn flatten_vector(graphic_table: &Table<Graphic>) -> Table<Vector> {
|
||||
|
||||
TableRow::new_from_element(element)
|
||||
.with_attribute("alpha_blending", alpha_blending)
|
||||
.with_attribute("source_node_id", source_node_id)
|
||||
.with_attribute("editor:layer", layer)
|
||||
};
|
||||
|
||||
// Apply the parent graphic's transform to each raster element, preserving each row's source_node_id
|
||||
// Apply the parent graphic's transform to each raster element, preserving each row's layer
|
||||
// and alpha_blending so the boolean op downstream can route clicks (and inherit blending state)
|
||||
// back to the originating raster layer
|
||||
(0..image.len())
|
||||
.map(|i| {
|
||||
let row_transform: DAffine2 = image.attribute_cloned_or_default("transform", i);
|
||||
let source_node_id: Option<NodeId> = image.attribute_cloned_or_default("source_node_id", i);
|
||||
let layer: Option<NodeId> = image.attribute_cloned_or_default("editor:layer", i);
|
||||
let alpha_blending: AlphaBlending = image.attribute_cloned_or_default("alpha_blending", i);
|
||||
make_row(parent_transform * row_transform, source_node_id, alpha_blending)
|
||||
make_row(parent_transform * row_transform, layer, alpha_blending)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
Graphic::RasterGPU(image) => {
|
||||
let parent_transform: DAffine2 = graphic_table.attribute_cloned_or_default("transform", index);
|
||||
let make_row = |transform, source_node_id, alpha_blending| {
|
||||
let make_row = |transform, layer, alpha_blending| {
|
||||
let mut subpath = Subpath::new_rectangle(DVec2::ZERO, DVec2::ONE);
|
||||
subpath.apply_transform(transform);
|
||||
|
||||
@@ -212,18 +214,18 @@ fn flatten_vector(graphic_table: &Table<Graphic>) -> Table<Vector> {
|
||||
|
||||
TableRow::new_from_element(element)
|
||||
.with_attribute("alpha_blending", alpha_blending)
|
||||
.with_attribute("source_node_id", source_node_id)
|
||||
.with_attribute("editor:layer", layer)
|
||||
};
|
||||
|
||||
// Apply the parent graphic's transform to each raster element, preserving each row's source_node_id
|
||||
// Apply the parent graphic's transform to each raster element, preserving each row's layer
|
||||
// and alpha_blending so the boolean op downstream can route clicks (and inherit blending state)
|
||||
// back to the originating raster layer
|
||||
(0..image.len())
|
||||
.map(|i| {
|
||||
let row_transform: DAffine2 = image.attribute_cloned_or_default("transform", i);
|
||||
let source_node_id: Option<NodeId> = image.attribute_cloned_or_default("source_node_id", i);
|
||||
let layer: Option<NodeId> = image.attribute_cloned_or_default("editor:layer", i);
|
||||
let alpha_blending: AlphaBlending = image.attribute_cloned_or_default("alpha_blending", i);
|
||||
make_row(parent_transform * row_transform, source_node_id, alpha_blending)
|
||||
make_row(parent_transform * row_transform, layer, alpha_blending)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
@@ -335,6 +335,8 @@ pub fn noise_pattern(
|
||||
return Table::new();
|
||||
}
|
||||
|
||||
let transform = DAffine2::from_translation(offset) * DAffine2::from_scale(size);
|
||||
|
||||
let footprint_scale = footprint.scale();
|
||||
let width = (size.x * footprint_scale.x) as u32;
|
||||
let height = (size.y * footprint_scale.y) as u32;
|
||||
@@ -376,12 +378,7 @@ pub fn noise_pattern(
|
||||
}
|
||||
}
|
||||
|
||||
return Table::new_from_row(
|
||||
TableRow::new_from_element(Raster::new_cpu(image))
|
||||
.with_attribute("transform", DAffine2::from_translation(offset) * DAffine2::from_scale(size))
|
||||
.with_attribute("alpha_blending", AlphaBlending::default())
|
||||
.with_attribute("source_node_id", None::<core_types::uuid::NodeId>),
|
||||
);
|
||||
return Table::new_from_row(TableRow::new_from_element(Raster::new_cpu(image)).with_attribute("transform", transform));
|
||||
}
|
||||
};
|
||||
noise.set_noise_type(Some(noise_type));
|
||||
@@ -439,12 +436,7 @@ pub fn noise_pattern(
|
||||
}
|
||||
}
|
||||
|
||||
Table::new_from_row(
|
||||
TableRow::new_from_element(Raster::new_cpu(image))
|
||||
.with_attribute("transform", DAffine2::from_translation(offset) * DAffine2::from_scale(size))
|
||||
.with_attribute("alpha_blending", AlphaBlending::default())
|
||||
.with_attribute("source_node_id", None::<core_types::uuid::NodeId>),
|
||||
)
|
||||
Table::new_from_row(TableRow::new_from_element(Raster::new_cpu(image)).with_attribute("transform", transform))
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Raster: Pattern"))]
|
||||
@@ -489,9 +481,7 @@ pub fn mandelbrot(ctx: impl ExtractFootprint + Send) -> Table<Raster<CPU>> {
|
||||
data,
|
||||
..Default::default()
|
||||
}))
|
||||
.with_attribute("transform", DAffine2::from_translation(offset) * DAffine2::from_scale(size))
|
||||
.with_attribute("alpha_blending", AlphaBlending::default())
|
||||
.with_attribute("source_node_id", None::<core_types::uuid::NodeId>),
|
||||
.with_attribute("transform", DAffine2::from_translation(offset) * DAffine2::from_scale(size)),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use core_types::AlphaBlending;
|
||||
use core_types::table::{Table, TableRow};
|
||||
use glam::{DAffine2, DVec2};
|
||||
use parley::GlyphRun;
|
||||
@@ -10,16 +9,16 @@ use skrifa::{MetadataProvider, OutlineGlyph};
|
||||
use vector_types::subpath::{ManipulatorGroup, Subpath};
|
||||
use vector_types::vector::{PointId, Vector};
|
||||
|
||||
pub struct PathBuilder<Upstream> {
|
||||
pub struct PathBuilder {
|
||||
current_subpath: Subpath<PointId>,
|
||||
origin: DVec2,
|
||||
glyph_subpaths: Vec<Subpath<PointId>>,
|
||||
pub vector_table: Table<Vector<Upstream>>,
|
||||
pub vector_table: Table<Vector>,
|
||||
scale: f64,
|
||||
id: PointId,
|
||||
}
|
||||
|
||||
impl<Upstream: Default + 'static> PathBuilder<Upstream> {
|
||||
impl PathBuilder {
|
||||
pub fn new(per_glyph_instances: bool, scale: f64) -> Self {
|
||||
Self {
|
||||
current_subpath: Subpath::new(Vec::new(), false),
|
||||
@@ -52,12 +51,8 @@ impl<Upstream: Default + 'static> PathBuilder<Upstream> {
|
||||
}
|
||||
|
||||
if per_glyph_instances {
|
||||
self.vector_table.push(
|
||||
TableRow::new_from_element(Vector::from_subpaths(core::mem::take(&mut self.glyph_subpaths), false))
|
||||
.with_attribute("transform", DAffine2::from_translation(glyph_offset))
|
||||
.with_attribute("alpha_blending", AlphaBlending::default())
|
||||
.with_attribute("source_node_id", None::<core_types::uuid::NodeId>),
|
||||
);
|
||||
self.vector_table
|
||||
.push(TableRow::new_from_element(Vector::from_subpaths(core::mem::take(&mut self.glyph_subpaths), false)).with_attribute("transform", DAffine2::from_translation(glyph_offset)));
|
||||
} else {
|
||||
for subpath in self.glyph_subpaths.drain(..) {
|
||||
// Unwrapping here is ok because `self.vector_table` is initialized with a single `Vector` table element
|
||||
@@ -120,7 +115,7 @@ impl<Upstream: Default + 'static> PathBuilder<Upstream> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn finalize(mut self) -> Table<Vector<Upstream>> {
|
||||
pub fn finalize(mut self) -> Table<Vector> {
|
||||
if self.vector_table.is_empty() {
|
||||
self.vector_table = Table::new_from_element(Vector::default());
|
||||
}
|
||||
@@ -128,7 +123,7 @@ impl<Upstream: Default + 'static> PathBuilder<Upstream> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<Upstream: Default + 'static> OutlinePen for PathBuilder<Upstream> {
|
||||
impl OutlinePen for PathBuilder {
|
||||
fn move_to(&mut self, x: f32, y: f32) {
|
||||
if !self.current_subpath.is_empty() {
|
||||
self.glyph_subpaths.push(std::mem::replace(&mut self.current_subpath, Subpath::new(Vec::new(), false)));
|
||||
|
||||
@@ -87,7 +87,7 @@ impl TextContext {
|
||||
}
|
||||
|
||||
/// Convert text to vector paths using the specified font and typesetting configuration
|
||||
pub fn to_path<Upstream: Default + 'static>(&mut self, text: &str, font: &Font, font_cache: &FontCache, typesetting: TypesettingConfig, per_glyph_instances: bool) -> Table<Vector<Upstream>> {
|
||||
pub fn to_path(&mut self, text: &str, font: &Font, font_cache: &FontCache, typesetting: TypesettingConfig, per_glyph_instances: bool) -> Table<Vector> {
|
||||
let Some(layout) = self.layout_text(text, font, font_cache, typesetting) else {
|
||||
return Table::new_from_element(Vector::default());
|
||||
};
|
||||
|
||||
@@ -6,7 +6,7 @@ use parley::fontique::Blob;
|
||||
use std::sync::Arc;
|
||||
use vector_types::Vector;
|
||||
|
||||
pub fn to_path<Upstream: Default + 'static>(text: &str, font: &Font, font_cache: &FontCache, typesetting: TypesettingConfig, per_glyph_instances: bool) -> Table<Vector<Upstream>> {
|
||||
pub fn to_path(text: &str, font: &Font, font_cache: &FontCache, typesetting: TypesettingConfig, per_glyph_instances: bool) -> Table<Vector> {
|
||||
TextContext::with_thread_local(|ctx| ctx.to_path(text, font, font_cache, typesetting, per_glyph_instances))
|
||||
}
|
||||
|
||||
|
||||
@@ -17,8 +17,8 @@ async fn path_modify(_ctx: impl Ctx, mut vector: Table<Vector>, modification: Bo
|
||||
|
||||
// Update the source node id
|
||||
let this_node_path = node_path.iter().rev().nth(1).copied();
|
||||
let existing: Option<NodeId> = vector.attribute_cloned_or_default("source_node_id", 0);
|
||||
vector.set_attribute("source_node_id", 0, existing.or(this_node_path));
|
||||
let existing: Option<NodeId> = vector.attribute_cloned_or_default("editor:layer", 0);
|
||||
vector.set_attribute("editor:layer", 0, existing.or(this_node_path));
|
||||
|
||||
if vector.len() > 1 {
|
||||
warn!("The path modify ran on {} vector rows. Only the first can be modified.", vector.len());
|
||||
|
||||
@@ -354,8 +354,6 @@ async fn round_corners(
|
||||
let attributes = source.clone_row_attributes(index);
|
||||
let source = source.element(index).unwrap();
|
||||
|
||||
let upstream_nested_layers = source.upstream_data.clone();
|
||||
|
||||
// Flip the roundness to help with user intuition
|
||||
let roundness = 1. - roundness;
|
||||
// Convert 0-100 to 0-0.5
|
||||
@@ -439,8 +437,6 @@ async fn round_corners(
|
||||
result.append_bezpath(rounded_subpath);
|
||||
}
|
||||
|
||||
result.upstream_data = upstream_nested_layers;
|
||||
|
||||
TableRow::from_parts(result, attributes)
|
||||
})
|
||||
.collect()
|
||||
@@ -1300,7 +1296,7 @@ pub async fn flatten_path<T: IntoGraphicTable + 'n + Send>(_: impl Ctx, #[implem
|
||||
// Concatenate every vector element's subpaths into the single output compound path
|
||||
for index in 0..flattened.len() {
|
||||
let Some(element) = flattened.element(index) else { continue };
|
||||
let node_id: Option<NodeId> = flattened.attribute_cloned_or_default("source_node_id", index);
|
||||
let node_id: Option<NodeId> = flattened.attribute_cloned_or_default("editor:layer", index);
|
||||
let node_id = node_id.map(|node_id| node_id.0).unwrap_or_default();
|
||||
|
||||
let mut hasher = DefaultHasher::new();
|
||||
@@ -1317,13 +1313,13 @@ pub async fn flatten_path<T: IntoGraphicTable + 'n + Send>(_: impl Ctx, #[implem
|
||||
// Preserve a reference to the original upstream graphic table so the renderer can recurse into it
|
||||
// when collecting metadata, exposing the original child layers' click targets to editor tools.
|
||||
// This is the same mechanism Boolean Operation uses to keep its inputs editable after the merge.
|
||||
output.upstream_data = Some(graphic_table);
|
||||
output_table.set_attribute("editor:merged_layers", 0, graphic_table);
|
||||
|
||||
// Adopt the last input row's source_node_id so the editor can also bucket clicks under a contributing child layer
|
||||
// Adopt the last input row's layer so the editor can also bucket clicks under a contributing child layer
|
||||
if !flattened.is_empty() {
|
||||
let primary = flattened.len() - 1;
|
||||
let source_node_id: Option<NodeId> = flattened.attribute_cloned_or_default("source_node_id", primary);
|
||||
output_table.set_attribute("source_node_id", 0, source_node_id);
|
||||
let layer: Option<NodeId> = flattened.attribute_cloned_or_default("editor:layer", primary);
|
||||
output_table.set_attribute("editor:layer", 0, layer);
|
||||
}
|
||||
|
||||
output_table
|
||||
@@ -1351,7 +1347,6 @@ async fn sample_polyline(
|
||||
region_domain: Default::default(),
|
||||
colinear_manipulators: Default::default(),
|
||||
style: std::mem::take(&mut row.element_mut().style),
|
||||
upstream_data: std::mem::take(&mut row.element_mut().upstream_data),
|
||||
};
|
||||
// Transfer the stroke transform from the input vector content to the result.
|
||||
result.style.set_stroke_transform(row.attribute_cloned_or_default("transform"));
|
||||
@@ -1441,7 +1436,6 @@ async fn simplify(
|
||||
|
||||
let mut result = Vector {
|
||||
style: std::mem::take(&mut row.element_mut().style),
|
||||
upstream_data: std::mem::take(&mut row.element_mut().upstream_data),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
@@ -1538,7 +1532,6 @@ async fn decimate(
|
||||
|
||||
let mut result = Vector {
|
||||
style: std::mem::take(&mut row.element_mut().style),
|
||||
upstream_data: std::mem::take(&mut row.element_mut().upstream_data),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
@@ -2382,13 +2375,13 @@ async fn morph<I: IntoGraphicTable + 'n + Send + Clone>(
|
||||
trs * skew
|
||||
};
|
||||
|
||||
// Pre-compensate upstream_data transforms so that when collect_metadata applies
|
||||
// Pre-compensate merged_layers transforms so that when collect_metadata applies
|
||||
// the row transform (which will be group_transform * lerped_transform after the
|
||||
// pipeline's Transform node runs), the lerped_transform cancels out and children
|
||||
// get the correct footprint: parent * group_transform * child_transform.
|
||||
// Only pre-compensate if the lerped transform is invertible (non-zero determinant).
|
||||
// A zero determinant can occur when interpolated scale passes through zero (e.g., flipped axes),
|
||||
// in which case we skip pre-compensation to avoid propagating NaN through upstream_data transforms.
|
||||
// in which case we skip pre-compensation to avoid propagating NaN through merged_layers transforms.
|
||||
if lerped_transform.matrix2.determinant().abs() > f64::EPSILON {
|
||||
let lerped_inverse = lerped_transform.inverse();
|
||||
for transform in graphic_table_content.iter_attribute_values_mut_or_default::<DAffine2>("transform") {
|
||||
@@ -2404,21 +2397,15 @@ async fn morph<I: IntoGraphicTable + 'n + Send + Clone>(
|
||||
|
||||
let mut attributes = content.clone_row_attributes(endpoint_index);
|
||||
attributes.insert("transform", lerped_transform);
|
||||
attributes.insert("editor:merged_layers", graphic_table_content);
|
||||
|
||||
return Table::new_from_row(TableRow::from_parts(
|
||||
Vector {
|
||||
upstream_data: Some(graphic_table_content),
|
||||
..endpoint_element.clone()
|
||||
},
|
||||
attributes,
|
||||
));
|
||||
return Table::new_from_row(TableRow::from_parts(endpoint_element.clone(), attributes));
|
||||
}
|
||||
|
||||
let mut vector = Vector {
|
||||
upstream_data: Some(graphic_table_content),
|
||||
style: source_element.style.lerp(&target_element.style, time),
|
||||
..Default::default()
|
||||
};
|
||||
vector.style = source_element.style.lerp(&target_element.style, time);
|
||||
|
||||
// Work directly with manipulator groups, bypassing the BezPath intermediate representation.
|
||||
// This avoids the full Vector → BezPath → interpolate → BezPath → Vector roundtrip each frame.
|
||||
@@ -2566,13 +2553,14 @@ async fn morph<I: IntoGraphicTable + 'n + Send + Clone>(
|
||||
// The result is a synthesis of source and target, so adopt whichever endpoint the result is closer to as
|
||||
// the click-target identity (so the editor can route clicks back to one of the contributing layers)
|
||||
let primary_index = if time < 0.5 { source_index } else { target_index };
|
||||
let source_node_id: Option<NodeId> = content.attribute_cloned_or_default("source_node_id", primary_index);
|
||||
let layer: Option<NodeId> = content.attribute_cloned_or_default("editor:layer", primary_index);
|
||||
|
||||
Table::new_from_row(
|
||||
TableRow::new_from_element(vector)
|
||||
.with_attribute("transform", lerped_transform)
|
||||
.with_attribute("alpha_blending", vector_alpha_blending)
|
||||
.with_attribute("source_node_id", source_node_id),
|
||||
.with_attribute("editor:layer", layer)
|
||||
.with_attribute("editor:merged_layers", graphic_table_content),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3047,7 +3035,6 @@ async fn centroid(ctx: impl Ctx + CloneVarArgs + ExtractAll, content: impl Node<
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use core_types::AlphaBlending;
|
||||
use core_types::Node;
|
||||
use kurbo::{CubicBez, Ellipse, Point, Rect};
|
||||
use std::future::Future;
|
||||
@@ -3073,10 +3060,7 @@ mod test {
|
||||
fn create_vector_row(bezpath: BezPath, transform: DAffine2) -> TableRow<Vector> {
|
||||
let mut row = Vector::default();
|
||||
row.append_bezpath(bezpath);
|
||||
TableRow::new_from_element(row)
|
||||
.with_attribute("transform", transform)
|
||||
.with_attribute("alpha_blending", AlphaBlending::default())
|
||||
.with_attribute("source_node_id", None::<core_types::uuid::NodeId>)
|
||||
TableRow::new_from_element(row).with_attribute("transform", transform)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
Reference in New Issue
Block a user