Implement dynamic table attributes to generalize the graphic-specific Table type (#4050)

* Feature-gate serde derives behind cfg_attr in all runtime node graph type crates

* Refactor Table to move its hard-coded fields into an attributes field

* Encapsulate TableRow/TableRowRef/TableRowMut attribute fields behind accessor methods

* Remove TaggedValue::GraphicUnused

* Refactor Table<T> to use dynamic attributes instead fixed names

* Fix code review soundness concerns

* Add todo work

* Replace row-oriented Table<T> API with column-oriented access

* Fix attribute propagation bugs

---------
This commit is contained in:
Keavon Chambers
2026-04-28 02:10:24 -07:00
parent 324b9e664c
commit 76938eb69a
75 changed files with 2330 additions and 1349 deletions

View File

@@ -1,3 +1,4 @@
use core_types::AlphaBlending;
use core_types::registry::types::Percentage;
use core_types::table::Table;
use core_types::{BlendMode, Color, Ctx};
@@ -17,36 +18,36 @@ impl MultiplyAlpha for Color {
}
impl MultiplyAlpha for Table<Vector> {
fn multiply_alpha(&mut self, factor: f64) {
for row in self.iter_mut() {
row.alpha_blending.opacity *= factor as f32;
for a in self.iter_attribute_values_mut_or_default::<AlphaBlending>("alpha_blending") {
a.opacity *= factor as f32;
}
}
}
impl MultiplyAlpha for Table<Graphic> {
fn multiply_alpha(&mut self, factor: f64) {
for row in self.iter_mut() {
row.alpha_blending.opacity *= factor as f32;
for a in self.iter_attribute_values_mut_or_default::<AlphaBlending>("alpha_blending") {
a.opacity *= factor as f32;
}
}
}
impl MultiplyAlpha for Table<Raster<CPU>> {
fn multiply_alpha(&mut self, factor: f64) {
for row in self.iter_mut() {
row.alpha_blending.opacity *= factor as f32;
for a in self.iter_attribute_values_mut_or_default::<AlphaBlending>("alpha_blending") {
a.opacity *= factor as f32;
}
}
}
impl MultiplyAlpha for Table<Color> {
fn multiply_alpha(&mut self, factor: f64) {
for row in self.iter_mut() {
row.alpha_blending.opacity *= factor as f32;
for a in self.iter_attribute_values_mut_or_default::<AlphaBlending>("alpha_blending") {
a.opacity *= factor as f32;
}
}
}
impl MultiplyAlpha for Table<GradientStops> {
fn multiply_alpha(&mut self, factor: f64) {
for row in self.iter_mut() {
row.alpha_blending.opacity *= factor as f32;
for a in self.iter_attribute_values_mut_or_default::<AlphaBlending>("alpha_blending") {
a.opacity *= factor as f32;
}
}
}
@@ -61,36 +62,36 @@ impl MultiplyFill for Color {
}
impl MultiplyFill for Table<Vector> {
fn multiply_fill(&mut self, factor: f64) {
for row in self.iter_mut() {
row.alpha_blending.fill *= factor as f32;
for a in self.iter_attribute_values_mut_or_default::<AlphaBlending>("alpha_blending") {
a.fill *= factor as f32;
}
}
}
impl MultiplyFill for Table<Graphic> {
fn multiply_fill(&mut self, factor: f64) {
for row in self.iter_mut() {
row.alpha_blending.fill *= factor as f32;
for a in self.iter_attribute_values_mut_or_default::<AlphaBlending>("alpha_blending") {
a.fill *= factor as f32;
}
}
}
impl MultiplyFill for Table<Raster<CPU>> {
fn multiply_fill(&mut self, factor: f64) {
for row in self.iter_mut() {
row.alpha_blending.fill *= factor as f32;
for a in self.iter_attribute_values_mut_or_default::<AlphaBlending>("alpha_blending") {
a.fill *= factor as f32;
}
}
}
impl MultiplyFill for Table<Color> {
fn multiply_fill(&mut self, factor: f64) {
for row in self.iter_mut() {
row.alpha_blending.fill *= factor as f32;
for a in self.iter_attribute_values_mut_or_default::<AlphaBlending>("alpha_blending") {
a.fill *= factor as f32;
}
}
}
impl MultiplyFill for Table<GradientStops> {
fn multiply_fill(&mut self, factor: f64) {
for row in self.iter_mut() {
row.alpha_blending.fill *= factor as f32;
for a in self.iter_attribute_values_mut_or_default::<AlphaBlending>("alpha_blending") {
a.fill *= factor as f32;
}
}
}
@@ -101,36 +102,36 @@ trait SetBlendMode {
impl SetBlendMode for Table<Vector> {
fn set_blend_mode(&mut self, blend_mode: BlendMode) {
for row in self.iter_mut() {
row.alpha_blending.blend_mode = blend_mode;
for a in self.iter_attribute_values_mut_or_default::<AlphaBlending>("alpha_blending") {
a.blend_mode = blend_mode;
}
}
}
impl SetBlendMode for Table<Graphic> {
fn set_blend_mode(&mut self, blend_mode: BlendMode) {
for row in self.iter_mut() {
row.alpha_blending.blend_mode = blend_mode;
for a in self.iter_attribute_values_mut_or_default::<AlphaBlending>("alpha_blending") {
a.blend_mode = blend_mode;
}
}
}
impl SetBlendMode for Table<Raster<CPU>> {
fn set_blend_mode(&mut self, blend_mode: BlendMode) {
for row in self.iter_mut() {
row.alpha_blending.blend_mode = blend_mode;
for a in self.iter_attribute_values_mut_or_default::<AlphaBlending>("alpha_blending") {
a.blend_mode = blend_mode;
}
}
}
impl SetBlendMode for Table<Color> {
fn set_blend_mode(&mut self, blend_mode: BlendMode) {
for row in self.iter_mut() {
row.alpha_blending.blend_mode = blend_mode;
for a in self.iter_attribute_values_mut_or_default::<AlphaBlending>("alpha_blending") {
a.blend_mode = blend_mode;
}
}
}
impl SetBlendMode for Table<GradientStops> {
fn set_blend_mode(&mut self, blend_mode: BlendMode) {
for row in self.iter_mut() {
row.alpha_blending.blend_mode = blend_mode;
for a in self.iter_attribute_values_mut_or_default::<AlphaBlending>("alpha_blending") {
a.blend_mode = blend_mode;
}
}
}
@@ -141,36 +142,36 @@ trait SetClip {
impl SetClip for Table<Vector> {
fn set_clip(&mut self, clip: bool) {
for row in self.iter_mut() {
row.alpha_blending.clip = clip;
for a in self.iter_attribute_values_mut_or_default::<AlphaBlending>("alpha_blending") {
a.clip = clip;
}
}
}
impl SetClip for Table<Graphic> {
fn set_clip(&mut self, clip: bool) {
for row in self.iter_mut() {
row.alpha_blending.clip = clip;
for a in self.iter_attribute_values_mut_or_default::<AlphaBlending>("alpha_blending") {
a.clip = clip;
}
}
}
impl SetClip for Table<Raster<CPU>> {
fn set_clip(&mut self, clip: bool) {
for row in self.iter_mut() {
row.alpha_blending.clip = clip;
for a in self.iter_attribute_values_mut_or_default::<AlphaBlending>("alpha_blending") {
a.clip = clip;
}
}
}
impl SetClip for Table<Color> {
fn set_clip(&mut self, clip: bool) {
for row in self.iter_mut() {
row.alpha_blending.clip = clip;
for a in self.iter_attribute_values_mut_or_default::<AlphaBlending>("alpha_blending") {
a.clip = clip;
}
}
}
impl SetClip for Table<GradientStops> {
fn set_clip(&mut self, clip: bool) {
for row in self.iter_mut() {
row.alpha_blending.clip = clip;
for a in self.iter_attribute_values_mut_or_default::<AlphaBlending>("alpha_blending") {
a.clip = clip;
}
}
}

View File

@@ -8,7 +8,7 @@ license = "MIT OR Apache-2.0"
[features]
default = ["serde"]
serde = ["dep:serde"]
serde = ["dep:serde", "core-types/serde", "raster-types/serde", "raster-nodes/serde"]
[dependencies]
# Local dependencies

View File

@@ -8,8 +8,9 @@ use core_types::math::bbox::{AxisAlignedBbox, Bbox};
use core_types::registry::FutureWrapperNode;
use core_types::table::{Table, TableRow};
use core_types::transform::Transform;
use core_types::uuid::NodeId;
use core_types::value::ClonedNode;
use core_types::{Ctx, Node};
use core_types::{AlphaBlending, Ctx, Node};
use glam::{DAffine2, DVec2};
use raster_nodes::blending_nodes::blend_colors;
use raster_nodes::std_nodes::{empty_image, extend_image_to_bounds};
@@ -89,14 +90,15 @@ where
return target;
}
for table_row in target.iter_mut() {
let target_width = table_row.element.width;
let target_height = table_row.element.height;
let (elements, transforms) = target.element_and_attribute_slices_mut::<DAffine2>("transform");
for (element, transform_attribute) in elements.iter_mut().zip(transforms.iter()) {
let target_width = element.width;
let target_height = element.height;
let target_size = DVec2::new(target_width as f64, target_height as f64);
let texture_size = DVec2::new(texture.width as f64, texture.height as f64);
let document_to_target = DAffine2::from_translation(-texture_size / 2.) * DAffine2::from_scale(target_size) * table_row.transform.inverse();
let document_to_target = DAffine2::from_translation(-texture_size / 2.) * DAffine2::from_scale(target_size) * transform_attribute.inverse();
for position in &positions {
let start = document_to_target.transform_point2(*position).round();
@@ -116,12 +118,12 @@ where
let max_y = (blit_area_offset.y + blit_area_dimensions.y).saturating_sub(1);
let max_x = (blit_area_offset.x + blit_area_dimensions.x).saturating_sub(1);
assert!(texture_index(max_x, max_y) < texture.data.len());
assert!(target_index(max_x, max_y) < table_row.element.data.len());
assert!(target_index(max_x, max_y) < element.data.len());
for y in blit_area_offset.y..blit_area_offset.y + blit_area_dimensions.y {
for x in blit_area_offset.x..blit_area_offset.x + blit_area_dimensions.x {
let src_pixel = texture.data[texture_index(x, y)];
let dst_pixel = &mut table_row.element.data_mut().data[target_index(x + clamp_start.x, y + clamp_start.y)];
let dst_pixel = &mut element.data_mut().data[target_index(x + clamp_start.x, y + clamp_start.y)];
*dst_pixel = blend_mode.eval((src_pixel, *dst_pixel));
}
}
@@ -137,7 +139,7 @@ pub async fn create_brush_texture(brush_style: &BrushStyle) -> Raster<CPU> {
let blank_texture = empty_image((), transform, Table::new_from_element(Color::TRANSPARENT)).into_iter().next().unwrap_or_default();
let image = blend_stamp_closure(stamp, blank_texture, |a, b| blend_colors(a, b, BlendMode::Normal, 1.));
image.element
image.into_element()
}
pub fn blend_with_mode(background: TableRow<Raster<CPU>>, foreground: TableRow<Raster<CPU>>, blend_mode: BlendMode, opacity: f64) -> TableRow<Raster<CPU>> {
@@ -198,7 +200,7 @@ async fn brush(
image.push(TableRow::default());
}
// TODO: Find a way to handle more than one row
let table_row = image.iter().next().expect("Expected the one row we just pushed").into_cloned();
let table_row = image.clone_row(0).expect("Expected the one row we just pushed");
let bounds = Table::new_from_row(table_row.clone()).bounding_box(DAffine2::IDENTITY, false);
let [start, end] = if let RenderBoundingBox::Rectangle(rect) = bounds { rect } else { [DVec2::ZERO, DVec2::ZERO] };
@@ -274,11 +276,10 @@ 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 {
element: Raster::new_cpu(opaque_image),
transform: background_bounds,
..Default::default()
};
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>);
for stroke in strokes {
let mut brush_texture = cache.get_cached_brush(&stroke.style);
@@ -310,24 +311,29 @@ async fn brush(
actual_image = blend_image_closure(erase_restore_mask, actual_image, |a, b| blend_params.eval((a, b)));
}
let first_row = image.iter_mut().next().unwrap();
*first_row.element = actual_image.element;
*first_row.transform = actual_image.transform;
*first_row.alpha_blending = actual_image.alpha_blending;
*first_row.source_node_id = actual_image.source_node_id;
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");
*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
}
pub fn blend_image_closure(foreground: TableRow<Raster<CPU>>, mut background: TableRow<Raster<CPU>>, map_fn: impl Fn(Color, Color) -> Color) -> TableRow<Raster<CPU>> {
let foreground_size = DVec2::new(foreground.element.width as f64, foreground.element.height as f64);
let background_size = DVec2::new(background.element.width as f64, background.element.height as f64);
let foreground_size = DVec2::new(foreground.element().width as f64, foreground.element().height as f64);
let background_size = DVec2::new(background.element().width as f64, background.element().height as f64);
// Transforms a point from the background image to the foreground image
let background_to_foreground = DAffine2::from_scale(foreground_size) * foreground.transform.inverse() * background.transform * DAffine2::from_scale(1. / background_size);
let foreground_transform: DAffine2 = foreground.attribute_cloned_or_default("transform");
let background_transform: DAffine2 = background.attribute_cloned_or_default("transform");
let background_to_foreground = DAffine2::from_scale(foreground_size) * foreground_transform.inverse() * background_transform * DAffine2::from_scale(1. / background_size);
// Footprint of the foreground image (0, 0)..(1, 1) in the background image space
let background_aabb = Bbox::unit().affine_transform(background.transform.inverse() * foreground.transform).to_axis_aligned_bbox();
let background_aabb = Bbox::unit().affine_transform(background_transform.inverse() * foreground_transform).to_axis_aligned_bbox();
// Clamp the foreground image to the background image
let start = (background_aabb.start * background_size).max(DVec2::ZERO).as_uvec2();
@@ -338,8 +344,10 @@ pub fn blend_image_closure(foreground: TableRow<Raster<CPU>>, mut background: Ta
let background_point = DVec2::new(x as f64, y as f64);
let foreground_point = background_to_foreground.transform_point2(background_point);
let source_pixel = foreground.element.sample(foreground_point);
let Some(destination_pixel) = background.element.data_mut().get_pixel_mut(x, y) else { continue };
let source_pixel = foreground.element().sample(foreground_point);
let Some(destination_pixel) = background.element_mut().data_mut().get_pixel_mut(x, y) else {
continue;
};
*destination_pixel = map_fn(source_pixel, *destination_pixel);
}
@@ -349,13 +357,14 @@ pub fn blend_image_closure(foreground: TableRow<Raster<CPU>>, mut background: Ta
}
pub fn blend_stamp_closure(foreground: BrushStampGenerator<Color>, mut background: TableRow<Raster<CPU>>, map_fn: impl Fn(Color, Color) -> Color) -> TableRow<Raster<CPU>> {
let background_size = DVec2::new(background.element.width as f64, background.element.height as f64);
let background_size = DVec2::new(background.element().width as f64, background.element().height as f64);
// Transforms a point from the background image to the foreground image
let background_to_foreground = background.transform * DAffine2::from_scale(1. / background_size);
let background_transform: DAffine2 = background.attribute_cloned_or_default("transform");
let background_to_foreground = background_transform * DAffine2::from_scale(1. / background_size);
// Footprint of the foreground image (0, 0)..(1, 1) in the background image space
let background_aabb = Bbox::unit().affine_transform(background.transform.inverse() * foreground.transform).to_axis_aligned_bbox();
let background_aabb = Bbox::unit().affine_transform(background_transform.inverse() * foreground.transform()).to_axis_aligned_bbox();
// Clamp the foreground image to the background image
let start = (background_aabb.start * background_size).max(DVec2::ZERO).as_uvec2();
@@ -368,7 +377,9 @@ pub fn blend_stamp_closure(foreground: BrushStampGenerator<Color>, mut backgroun
let foreground_point = background_to_foreground.transform_point2(background_point);
let Some(source_pixel) = foreground.sample(foreground_point, area) else { continue };
let Some(destination_pixel) = background.element.data_mut().get_pixel_mut(x, y) else { continue };
let Some(destination_pixel) = background.element_mut().data_mut().get_pixel_mut(x, y) else {
continue;
};
*destination_pixel = map_fn(source_pixel, *destination_pixel);
}
@@ -411,6 +422,6 @@ mod test {
BrushCache::default(),
)
.await;
assert_eq!(image.iter().next().unwrap().element.width, 20);
assert_eq!(image.element(0).unwrap().width, 20);
}
}

View File

@@ -14,24 +14,25 @@ use std::sync::{Arc, Mutex};
// TODO: This is a temporary hack, be sure to not reuse this when the brush system is replaced/rewritten.
static NEXT_BRUSH_CACHE_IMPL_ID: AtomicU64 = AtomicU64::new(0);
#[derive(Clone, Debug, DynAny, serde::Serialize, serde::Deserialize)]
#[derive(Clone, Debug, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
struct BrushCacheImpl {
#[serde(default = "new_unique_id")]
#[cfg_attr(feature = "serde", serde(default = "new_unique_id"))]
unique_id: u64,
// The full previous input that was cached.
#[serde(default)]
#[cfg_attr(feature = "serde", serde(default))]
prev_input: Vec<BrushStroke>,
// The strokes that have been fully processed and blended into the background.
#[serde(default, deserialize_with = "raster_types::image::migrate_image_frame_row")]
#[cfg_attr(feature = "serde", serde(default, deserialize_with = "raster_types::image::migrate_image_frame_row"))]
background: TableRow<Raster<CPU>>,
#[serde(default, deserialize_with = "raster_types::image::migrate_image_frame_row")]
#[cfg_attr(feature = "serde", serde(default, deserialize_with = "raster_types::image::migrate_image_frame_row"))]
blended_image: TableRow<Raster<CPU>>,
#[serde(default, deserialize_with = "raster_types::image::migrate_image_frame_row")]
#[cfg_attr(feature = "serde", serde(default, deserialize_with = "raster_types::image::migrate_image_frame_row"))]
last_stroke_texture: TableRow<Raster<CPU>>,
// A cache for brush textures.
#[serde(skip)]
#[cfg_attr(feature = "serde", serde(skip))]
brush_texture_cache: HashMap<CacheHashWrapper<BrushStyle>, Raster<CPU>>,
}
@@ -63,11 +64,10 @@ 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 {
element: Raster::<CPU>::default(),
transform: glam::DAffine2::ZERO,
..Default::default()
};
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 strokes = input[num_blended_strokes..].to_vec();
if !strokes.is_empty() && self.prev_input.len() > num_blended_strokes {
@@ -135,7 +135,8 @@ pub struct BrushPlan {
pub first_stroke_point_skip: usize,
}
#[derive(Debug, Default, DynAny, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Default, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct BrushCache(Arc<Mutex<BrushCacheImpl>>);
// A bit of a cursed implementation to work around the current node system.

View File

@@ -5,7 +5,8 @@ use core_types::math::bbox::AxisAlignedBbox;
use dyn_any::DynAny;
use glam::DVec2;
/// The style of a brush.
#[derive(Clone, Debug, CacheHash, DynAny, serde::Serialize, serde::Deserialize)]
#[derive(Clone, Debug, CacheHash, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct BrushStyle {
pub color: Color,
pub diameter: f64,
@@ -42,13 +43,15 @@ impl PartialEq for BrushStyle {
}
/// A single sample of brush parameters across the brush stroke.
#[derive(Clone, Debug, PartialEq, core_types::CacheHash, DynAny, serde::Serialize, serde::Deserialize)]
#[derive(Clone, Debug, PartialEq, core_types::CacheHash, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct BrushInputSample {
pub position: DVec2,
}
/// The parameters for a single stroke brush.
#[derive(Clone, Debug, PartialEq, core_types::CacheHash, Default, DynAny, serde::Serialize, serde::Deserialize)]
#[derive(Clone, Debug, PartialEq, core_types::CacheHash, Default, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct BrushStroke {
pub style: BrushStyle,
pub trace: Vec<BrushInputSample>,

View File

@@ -8,6 +8,7 @@ license = "MIT OR Apache-2.0"
[features]
default = ["serde"]
serde = ["dep:serde", "core-types/serde", "raster-types/serde", "graphic-types/serde"]
wasm = [
"core-types/wasm",
"raster-types/wasm",

View File

@@ -9,7 +9,8 @@ use raster_types::{CPU, GPU, Raster};
const DAY: f64 = 1000. * 3600. * 24.;
#[derive(Debug, Clone, Copy, PartialEq, Eq, dyn_any::DynAny, Default, Hash, CacheHash, node_macro::ChoiceType, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, dyn_any::DynAny, Default, Hash, CacheHash, node_macro::ChoiceType)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum RealTimeMode {
#[label("UTC")]
Utc,

View File

@@ -15,7 +15,8 @@ fn extract_xy<T: Into<DVec2>>(_: impl Ctx, #[implementations(DVec2, IVec2, UVec2
/// The X or Y component of a vec2.
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, CacheHash, DynAny, node_macro::ChoiceType, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, CacheHash, DynAny, node_macro::ChoiceType)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[widget(Radio)]
pub enum XY {
#[default]

View File

@@ -50,8 +50,7 @@ pub async fn create_artboard<T: IntoGraphicTable + 'n>(
let dimensions = dimensions.abs();
let background: Option<Color> = background.into();
let background = background.unwrap_or(Color::WHITE);
let background = background.element(0).copied().unwrap_or(Color::WHITE);
Table::new_from_element(Artboard {
content,

View File

@@ -169,7 +169,8 @@ where
// Create and add mirrored instance
for mut row in content.into_iter() {
row.transform = reflected_transform * row.transform;
let current_transform: DAffine2 = row.attribute_cloned_or_default("transform");
row.set_attribute("transform", reflected_transform * current_transform);
result_table.push(row);
}
@@ -199,8 +200,8 @@ pub async fn source_node_id<T: 'n + Send + Clone>(
let source_node_id = node_path.get(node_path.len().wrapping_sub(2)).copied();
let mut content = content;
for row in content.iter_mut() {
*row.source_node_id = source_node_id;
for source_id in content.iter_attribute_values_mut_or_default::<Option<NodeId>>("source_node_id") {
*source_id = source_node_id;
}
content
@@ -241,8 +242,9 @@ pub async fn legacy_layer_extend<T: 'n + Send + Clone>(
let source_node_id = nested_node_path.get(nested_node_path.len().wrapping_sub(2)).copied();
let mut base = base;
for row in new.into_iter() {
base.push(TableRow { source_node_id, ..row });
for mut row in new.into_iter() {
row.set_attribute("source_node_id", source_node_id);
base.push(row);
}
base
@@ -290,9 +292,10 @@ pub async fn to_graphic<T: IntoGraphicTable + 'n>(
pub async fn flatten_graphic(_: impl Ctx, content: Table<Graphic>, fully_flatten: bool) -> Table<Graphic> {
// TODO: Avoid mutable reference, instead return a new Table<Graphic>?
fn flatten_table(output_graphic_table: &mut Table<Graphic>, current_graphic_table: Table<Graphic>, fully_flatten: bool, recursion_depth: usize) {
for current_row in current_graphic_table.iter() {
let current_element = current_row.element.clone();
let reference = *current_row.source_node_id;
for index in 0..current_graphic_table.len() {
let Some(current_element) = current_graphic_table.element(index) else { continue };
let current_element = current_element.clone();
let current_transform: DAffine2 = current_graphic_table.attribute_cloned_or_default("transform", index);
let recurse = fully_flatten || recursion_depth == 0;
@@ -300,20 +303,16 @@ pub async fn flatten_graphic(_: impl Ctx, content: Table<Graphic>, fully_flatten
// If we're allowed to recurse, flatten any graphics we encounter
Graphic::Graphic(mut current_element) if recurse => {
// Apply the parent graphic's transform to all child elements
for graphic in current_element.iter_mut() {
*graphic.transform = *current_row.transform * *graphic.transform;
for graphic_transform in current_element.iter_attribute_values_mut_or_default::<DAffine2>("transform") {
*graphic_transform = current_transform * *graphic_transform;
}
flatten_table(output_graphic_table, current_element, fully_flatten, recursion_depth + 1);
}
// Push any leaf Graphic elements we encounter, which can be either Graphic table elements beyond the recursion depth, or table elements other than Graphic tables
_ => {
output_graphic_table.push(TableRow {
element: current_element,
transform: *current_row.transform,
alpha_blending: *current_row.alpha_blending,
source_node_id: reference,
});
let attributes = current_graphic_table.clone_row_attributes(index);
output_graphic_table.push(TableRow::from_parts(current_element, attributes));
}
}
}
@@ -370,20 +369,17 @@ fn colors_to_gradient<T: IntoGraphicTable + 'n + Send + Clone>(_: impl Ctx, #[im
]));
}
if let (Some(color), None) = {
let mut colors_iter = colors.iter();
(colors_iter.next(), colors_iter.next())
} {
if let (1, Some(&single_color)) = (total_colors, colors.element(0)) {
return Table::new_from_element(GradientStops::new(vec![
GradientStop {
position: 0.,
midpoint: 0.5,
color: *color.element,
color: single_color,
},
GradientStop {
position: 1.,
midpoint: 0.5,
color: *color.element,
color: single_color,
},
]));
}
@@ -391,7 +387,7 @@ fn colors_to_gradient<T: IntoGraphicTable + 'n + Send + Clone>(_: impl Ctx, #[im
let colors = colors.into_iter().enumerate().map(|(index, row)| GradientStop {
position: index as f64 / (total_colors - 1) as f64,
midpoint: 0.5,
color: row.element,
color: row.into_element(),
});
Table::new_from_element(GradientStops::new(colors))
}

View File

@@ -119,8 +119,8 @@ fn string_to_bytes(_: impl Ctx, string: String) -> Vec<u8> {
/// Converts extracted raw RGBA pixel data from an input image. Each pixel becomes 4 sequential bytes. Useful for transmission over HTTP or writing to files.
#[node_macro::node(category("Web Request"), name("Image to Bytes"))]
fn image_to_bytes(_: impl Ctx, image: Table<Raster<CPU>>) -> Vec<u8> {
let Some(image) = image.iter().next() else { return vec![] };
image.element.data.iter().flat_map(|color| color.to_rgba8_srgb().into_iter()).collect::<Vec<u8>>()
let Some(image) = image.element(0) else { return vec![] };
image.data.iter().flat_map(|color| color.to_rgba8_srgb().into_iter()).collect::<Vec<u8>>()
}
/// Loads binary from URLs and local asset paths. Returns a transparent placeholder if the resource fails to load, allowing rendering to continue.
@@ -187,6 +187,7 @@ where
Table<T>: Render,
{
use core_types::table::TableRow;
use glam::{DAffine2, DVec2};
if footprint.transform.matrix2.determinant() == 0. {
log::trace!("Invalid footprint received for rasterization");
@@ -203,11 +204,11 @@ where
..Default::default()
};
for row in data.iter_mut() {
*row.transform = glam::DAffine2::from_translation(-aabb.start) * *row.transform;
for transform in data.iter_attribute_values_mut_or_default::<DAffine2>("transform") {
*transform = DAffine2::from_translation(-aabb.start) * *transform;
}
data.render_svg(&mut render, &render_params);
render.format_svg(glam::DVec2::ZERO, size);
render.format_svg(DVec2::ZERO, size);
let svg_string = render.svg.to_svg_string();
canvas.set_resolution(resolution);
@@ -228,9 +229,5 @@ 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 {
element: Raster::new_cpu(image),
transform: footprint.transform,
..Default::default()
})
Table::new_from_row(TableRow::new_from_element(Raster::new_cpu(image)).with_attribute("transform", footprint.transform))
}

View File

@@ -866,10 +866,10 @@ fn gradient_value(_: impl Ctx, _primary: (), gradient: Table<GradientStops>) ->
/// Gets the color at the specified position along the gradient, given a position from 0 (left) to 1 (right).
#[node_macro::node(category("Color"))]
fn sample_gradient(_: impl Ctx, _primary: (), gradient: Table<GradientStops>, position: Fraction) -> Table<Color> {
let Some(row) = gradient.get(0) else { return Table::new() };
let Some(gradient) = gradient.element(0) else { return Table::new() };
let position = position.clamp(0., 1.);
let color = row.element.evaluate(position);
let color = gradient.evaluate(position);
Table::new_from_element(color)
}

View File

@@ -1,5 +1,6 @@
use core_types::table::{Table, TableRow, TableRowRef};
use core_types::{Color, Ctx};
use core_types::table::{Table, TableRow};
use core_types::uuid::NodeId;
use core_types::{AlphaBlending, Color, Ctx};
use glam::{DAffine2, DVec2};
use graphic_types::vector_types::subpath::{ManipulatorGroup, Subpath};
use graphic_types::vector_types::vector::PointId;
@@ -34,19 +35,22 @@ async fn boolean_operation<I: graphic_types::IntoGraphicTable + 'n + Send + Clon
let content = content.into_graphic_table();
// The first index is the bottom of the stack
let mut result_vector_table = boolean_operation_on_vector_table(flatten_vector(&content).iter(), operation);
let flattened = flatten_vector(&content);
let mut result_vector_table = boolean_operation_on_vector_table(&flattened, operation);
// Replace the transformation matrix with a mutation of the vector points themselves
if let Some(result_vector) = result_vector_table.iter_mut().next() {
let transform = *result_vector.transform;
*result_vector.transform = DAffine2::IDENTITY;
if result_vector_table.element_mut(0).is_some() {
let transform: DAffine2 = result_vector_table.attribute_cloned_or_default("transform", 0);
result_vector_table.set_attribute("transform", 0, DAffine2::IDENTITY);
Vector::transform(result_vector.element, transform);
result_vector.element.style.set_stroke_transform(DAffine2::IDENTITY);
result_vector.element.upstream_data = Some(content.clone());
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());
// Clean up the boolean operation result by merging duplicated points
result_vector.element.merge_by_distance_spatial(*result_vector.transform, 0.0001);
let merge_transform: DAffine2 = result_vector_table.attribute_cloned_or_default("transform", 0);
result_vector_table.element_mut(0).unwrap().merge_by_distance_spatial(merge_transform, 0.0001);
}
result_vector_table
@@ -106,26 +110,34 @@ impl WindingNumber {
}
}
fn boolean_operation_on_vector_table<'a>(vector: impl DoubleEndedIterator<Item = TableRowRef<'a, Vector>> + Clone, boolean_operation: BooleanOperation) -> Table<Vector> {
fn boolean_operation_on_vector_table(vector: &Table<Vector>, boolean_operation: BooleanOperation) -> Table<Vector> {
const EPSILON: f64 = 1e-5;
let mut table = Table::new();
let mut paths = Vec::new();
let mut row = TableRow::<Vector>::default();
let copy_from = if matches!(boolean_operation, BooleanOperation::SubtractFront) {
vector.clone().next()
let copy_from_index = if matches!(boolean_operation, BooleanOperation::SubtractFront) {
if !vector.is_empty() { Some(0) } else { None }
} else {
vector.clone().next_back()
if !vector.is_empty() { Some(vector.len() - 1) } else { None }
};
let mut row = if let Some(index) = copy_from_index {
let mut attributes = vector.clone_row_attributes(index);
// The boolean op bakes input transforms into the output geometry, so the result row carries no transform of its own
attributes.insert("transform", DAffine2::IDENTITY);
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)
} else {
TableRow::<Vector>::default()
};
if let Some(copy_from) = copy_from {
row.alpha_blending = *copy_from.alpha_blending;
row.source_node_id = *copy_from.source_node_id;
row.element.style = copy_from.element.style.clone();
row.element.upstream_data = copy_from.element.upstream_data.clone();
}
for element in vector {
paths.push(to_bez_path(element.element, *element.transform));
for index in 0..vector.len() {
let element = vector.element(index).unwrap();
paths.push(to_bez_path(element, vector.attribute_cloned_or_default("transform", index)));
}
let top = match Topology::<WindingNumber>::from_paths(paths.iter().enumerate().map(|(idx, path)| (path, (idx, paths.len()))), EPSILON) {
@@ -139,7 +151,7 @@ fn boolean_operation_on_vector_table<'a>(vector: impl DoubleEndedIterator<Item =
let contours = top.contours(|winding| winding.is_inside(boolean_operation));
for subpath in from_bez_paths(contours.contours().map(|c| &c.path)) {
row.element.append_subpath(subpath, false);
row.element_mut().append_subpath(subpath, false);
}
table.push(row);
@@ -147,95 +159,107 @@ fn boolean_operation_on_vector_table<'a>(vector: impl DoubleEndedIterator<Item =
}
fn flatten_vector(graphic_table: &Table<Graphic>) -> Table<Vector> {
graphic_table
.iter()
.flat_map(|element| {
match element.element.clone() {
(0..graphic_table.len())
.flat_map(|index| {
let graphic = graphic_table.element(index).unwrap();
match graphic.clone() {
Graphic::Vector(vector) => {
// Apply the parent graphic's transform to each element of the vector table
let parent_transform: DAffine2 = graphic_table.attribute_cloned_or_default("transform", index);
vector
.into_iter()
.map(|mut sub_vector| {
sub_vector.transform = *element.transform * sub_vector.transform;
let current_transform: DAffine2 = sub_vector.attribute_cloned_or_default("transform");
*sub_vector.attribute_mut_or_insert_default("transform") = parent_transform * current_transform;
sub_vector
})
.collect::<Vec<_>>()
}
Graphic::RasterCPU(image) => {
let make_row = |transform| {
// Convert the image frame into a rectangular subpath with the image's transform
let parent_transform: DAffine2 = graphic_table.attribute_cloned_or_default("transform", index);
let make_row = |transform, source_node_id, alpha_blending| {
let mut subpath = Subpath::new_rectangle(DVec2::ZERO, DVec2::ONE);
subpath.apply_transform(transform);
// Create a vector table row from the rectangular subpath, with a default black fill
let mut element = Vector::from_subpath(subpath);
element.style.set_fill(Fill::Solid(Color::BLACK));
TableRow { element, ..Default::default() }
TableRow::new_from_element(element)
.with_attribute("alpha_blending", alpha_blending)
.with_attribute("source_node_id", source_node_id)
};
// Apply the parent graphic's transform to each raster element
image.iter().map(|row| make_row(*element.transform * *row.transform)).collect::<Vec<_>>()
// Apply the parent graphic's transform to each raster element, preserving each row's source_node_id
// 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 alpha_blending: AlphaBlending = image.attribute_cloned_or_default("alpha_blending", i);
make_row(parent_transform * row_transform, source_node_id, alpha_blending)
})
.collect::<Vec<_>>()
}
Graphic::RasterGPU(image) => {
let make_row = |transform| {
// Convert the image frame into a rectangular subpath with the image's transform
let parent_transform: DAffine2 = graphic_table.attribute_cloned_or_default("transform", index);
let make_row = |transform, source_node_id, alpha_blending| {
let mut subpath = Subpath::new_rectangle(DVec2::ZERO, DVec2::ONE);
subpath.apply_transform(transform);
// Create a vector table row from the rectangular subpath, with a default black fill
let mut element = Vector::from_subpath(subpath);
element.style.set_fill(Fill::Solid(Color::BLACK));
TableRow { element, ..Default::default() }
TableRow::new_from_element(element)
.with_attribute("alpha_blending", alpha_blending)
.with_attribute("source_node_id", source_node_id)
};
// Apply the parent graphic's transform to each raster element
image.iter().map(|row| make_row(*element.transform * *row.transform)).collect::<Vec<_>>()
// Apply the parent graphic's transform to each raster element, preserving each row's source_node_id
// 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 alpha_blending: AlphaBlending = image.attribute_cloned_or_default("alpha_blending", i);
make_row(parent_transform * row_transform, source_node_id, alpha_blending)
})
.collect::<Vec<_>>()
}
Graphic::Graphic(mut graphic) => {
let parent_transform: DAffine2 = graphic_table.attribute_cloned_or_default("transform", index);
// Apply the parent graphic's transform to each element of inner table
for sub_element in graphic.iter_mut() {
*sub_element.transform = *element.transform * *sub_element.transform;
for transform in graphic.iter_attribute_values_mut_or_default::<DAffine2>("transform") {
*transform = parent_transform * *transform;
}
// Recursively flatten the inner table into the output vector table
let unioned = boolean_operation_on_vector_table(flatten_vector(&graphic).iter(), BooleanOperation::Union);
let flattened = flatten_vector(&graphic);
let unioned = boolean_operation_on_vector_table(&flattened, BooleanOperation::Union);
unioned.into_iter().collect::<Vec<_>>()
}
Graphic::Color(color) => color
.into_iter()
.map(|row| {
let (color, attributes) = row.into_parts();
let mut element = Vector::default();
element.style.set_fill(Fill::Solid(row.element));
element.style.set_fill(Fill::Solid(color));
element.style.set_stroke_transform(DAffine2::IDENTITY);
TableRow {
element,
transform: row.transform,
alpha_blending: row.alpha_blending,
source_node_id: row.source_node_id,
}
TableRow::from_parts(element, attributes)
})
.collect::<Vec<_>>(),
Graphic::Gradient(gradient) => gradient
.into_iter()
.map(|row| {
let (stops, attributes) = row.into_parts();
let mut element = Vector::default();
element.style.set_fill(Fill::Gradient(graphic_types::vector_types::gradient::Gradient {
stops: row.element,
..Default::default()
}));
element.style.set_fill(Fill::Gradient(graphic_types::vector_types::gradient::Gradient { stops, ..Default::default() }));
element.style.set_stroke_transform(DAffine2::IDENTITY);
TableRow {
element,
transform: row.transform,
alpha_blending: row.alpha_blending,
source_node_id: row.source_node_id,
}
TableRow::from_parts(element, attributes)
})
.collect::<Vec<_>>(),
}

View File

@@ -11,8 +11,10 @@ workspace = true
[features]
default = ["std"]
serde = ["dep:serde", "core-types?/serde", "raster-types?/serde", "vector-types?/serde"]
shader-nodes = ["std", "dep:raster-nodes-shaders", "dep:wgpu-executor"]
std = [
"serde",
"dep:core-types",
"dep:dyn-any",
"dep:graphene-hash",
@@ -23,7 +25,6 @@ std = [
"dep:rand",
"dep:rand_chacha",
"dep:fastnoise-lite",
"dep:serde",
"dep:kurbo",
]
wasm = [

View File

@@ -18,8 +18,8 @@ mod adjust_std {
impl Adjust<Color> for Table<Raster<CPU>> {
fn adjust(&mut self, map_fn: impl Fn(&Color) -> Color) {
for row in self.iter_mut() {
for color in row.element.data_mut().data.iter_mut() {
for element in self.iter_element_values_mut() {
for color in element.data_mut().data.iter_mut() {
*color = map_fn(color);
}
}
@@ -27,15 +27,15 @@ mod adjust_std {
}
impl Adjust<Color> for Table<Color> {
fn adjust(&mut self, map_fn: impl Fn(&Color) -> Color) {
for row in self.iter_mut() {
*row.element = map_fn(row.element);
for element in self.iter_element_values_mut() {
*element = map_fn(element);
}
}
}
impl Adjust<Color> for Table<GradientStops> {
fn adjust(&mut self, map_fn: impl Fn(&Color) -> Color) {
for row in self.iter_mut() {
row.element.adjust(&map_fn);
for element in self.iter_element_values_mut() {
element.adjust(&map_fn);
}
}
}

View File

@@ -34,7 +34,8 @@ use vector_types::GradientStops;
// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=Color%20Lookup%20(Photoshop%20CS6
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[cfg_attr(feature = "std", derive(dyn_any::DynAny, serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "std", derive(dyn_any::DynAny))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash, node_macro::ChoiceType, bytemuck::NoUninit, BufferStruct, FromPrimitive, IntoPrimitive)]
#[widget(Dropdown)]
#[repr(u32)]
@@ -565,7 +566,8 @@ fn vibrance<T: Adjust<Color>>(
#[repr(u32)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[cfg_attr(feature = "std", derive(dyn_any::DynAny, serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "std", derive(dyn_any::DynAny))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, node_macro::ChoiceType, BufferStruct, FromPrimitive, IntoPrimitive)]
#[widget(Radio)]
pub enum RedGreenBlue {
@@ -576,7 +578,8 @@ pub enum RedGreenBlue {
}
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[cfg_attr(feature = "std", derive(dyn_any::DynAny, serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "std", derive(dyn_any::DynAny))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, node_macro::ChoiceType, bytemuck::NoUninit, BufferStruct, FromPrimitive, IntoPrimitive)]
#[widget(Radio)]
#[repr(u32)]
@@ -590,7 +593,8 @@ pub enum RedGreenBlueAlpha {
/// Style of noise pattern.
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[cfg_attr(feature = "std", derive(dyn_any::DynAny, serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "std", derive(dyn_any::DynAny))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, node_macro::ChoiceType)]
#[widget(Dropdown)]
pub enum NoiseType {
@@ -608,7 +612,8 @@ pub enum NoiseType {
/// Style of layered levels of the noise pattern.
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[cfg_attr(feature = "std", derive(dyn_any::DynAny, serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "std", derive(dyn_any::DynAny))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, node_macro::ChoiceType)]
pub enum FractalType {
#[default]
@@ -625,7 +630,8 @@ pub enum FractalType {
/// Distance function used by the cellular noise.
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[cfg_attr(feature = "std", derive(dyn_any::DynAny, serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "std", derive(dyn_any::DynAny))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, node_macro::ChoiceType)]
pub enum CellularDistanceFunction {
#[default]
@@ -637,7 +643,8 @@ pub enum CellularDistanceFunction {
}
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[cfg_attr(feature = "std", derive(dyn_any::DynAny, serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "std", derive(dyn_any::DynAny))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, node_macro::ChoiceType)]
pub enum CellularReturnType {
CellValue,
@@ -657,7 +664,8 @@ pub enum CellularReturnType {
}
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[cfg_attr(feature = "std", derive(dyn_any::DynAny, serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "std", derive(dyn_any::DynAny))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, node_macro::ChoiceType)]
#[widget(Dropdown)]
pub enum DomainWarpType {
@@ -771,7 +779,8 @@ fn channel_mixer<T: Adjust<Color>>(
#[repr(u32)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[cfg_attr(feature = "std", derive(dyn_any::DynAny, serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "std", derive(dyn_any::DynAny))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, node_macro::ChoiceType, BufferStruct, FromPrimitive, IntoPrimitive)]
#[widget(Radio)]
pub enum RelativeAbsolute {
@@ -782,7 +791,8 @@ pub enum RelativeAbsolute {
#[repr(u32)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[cfg_attr(feature = "std", derive(dyn_any::DynAny, serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "std", derive(dyn_any::DynAny))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, node_macro::ChoiceType, BufferStruct, FromPrimitive, IntoPrimitive)]
pub enum SelectiveColorChoice {
#[default]

View File

@@ -30,13 +30,17 @@ mod blend_std {
impl Blend<Color> for Table<Raster<CPU>> {
fn blend(&self, under: &Self, blend_fn: impl Fn(Color, Color) -> Color) -> Self {
let mut result_table = self.clone();
for (over, under) in result_table.iter_mut().zip(under.iter()) {
let data = over.element.data.iter().zip(under.element.data.iter()).map(|(a, b)| blend_fn(*a, *b)).collect();
let pair_count = result_table.len().min(under.len());
for index in 0..pair_count {
let Some(over) = result_table.element(index) else { break };
let Some(under_element) = under.element(index) else { break };
let data = over.data.iter().zip(under_element.data.iter()).map(|(a, b)| blend_fn(*a, *b)).collect();
let (width, height) = (over.width, over.height);
*over.element = Raster::new_cpu(Image {
*result_table.element_mut(index).unwrap() = Raster::new_cpu(Image {
data,
width: over.element.width,
height: over.element.height,
width,
height,
base64_string: None,
});
}
@@ -46,8 +50,12 @@ mod blend_std {
impl Blend<Color> for Table<Color> {
fn blend(&self, under: &Self, blend_fn: impl Fn(Color, Color) -> Color) -> Self {
let mut result_table = self.clone();
for (over, under) in result_table.iter_mut().zip(under.iter()) {
*over.element = blend_fn(*over.element, *under.element);
let pair_count = result_table.len().min(under.len());
for index in 0..pair_count {
let Some(over) = result_table.element(index) else { break };
let Some(under_element) = under.element(index) else { break };
let new_val = blend_fn(*over, *under_element);
*result_table.element_mut(index).unwrap() = new_val;
}
result_table
}
@@ -55,8 +63,12 @@ mod blend_std {
impl Blend<Color> for Table<GradientStops> {
fn blend(&self, under: &Self, blend_fn: impl Fn(Color, Color) -> Color) -> Self {
let mut result_table = self.clone();
for (over, under) in result_table.iter_mut().zip(under.iter()) {
*over.element = over.element.blend(under.element, &blend_fn);
let pair_count = result_table.len().min(under.len());
for index in 0..pair_count {
let Some(over) = result_table.element(index) else { break };
let Some(under_element) = under.element(index) else { break };
let new_val = over.blend(under_element, &blend_fn);
*result_table.element_mut(index).unwrap() = new_val;
}
result_table
}
@@ -201,7 +213,7 @@ mod test {
let opacity = 100.;
let result = super::color_overlay((), Table::new_from_element(Raster::new_cpu(image.clone())), overlay_color, BlendMode::Multiply, opacity);
let result = result.iter().next().unwrap().element;
let result = result.element(0).unwrap().clone();
// The output should just be the original green and alpha channels (as we multiply them by 1 and other channels by 0)
assert_eq!(result.data[0], Color::from_rgbaf32_unchecked(0., image_color.g(), 0., image_color.a()));

View File

@@ -4,13 +4,14 @@ use dyn_any::{DynAny, StaticType, StaticTypeSized};
use std::ops::{Add, Mul, Sub};
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Debug, Clone, PartialEq, core_types::CacheHash, DynAny, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Clone, PartialEq, core_types::CacheHash, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Curve {
#[serde(rename = "manipulatorGroups")]
#[cfg_attr(feature = "serde", serde(rename = "manipulatorGroups"))]
pub manipulator_groups: Vec<CurveManipulatorGroup>,
#[serde(rename = "firstHandle")]
#[cfg_attr(feature = "serde", serde(rename = "firstHandle"))]
pub first_handle: [f32; 2],
#[serde(rename = "lastHandle")]
#[cfg_attr(feature = "serde", serde(rename = "lastHandle"))]
pub last_handle: [f32; 2],
}
@@ -25,7 +26,8 @@ impl Default for Curve {
}
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Debug, Clone, Copy, PartialEq, core_types::CacheHash, DynAny, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Clone, Copy, PartialEq, core_types::CacheHash, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct CurveManipulatorGroup {
pub anchor: [f32; 2],
pub handles: [[f32; 2]; 2],

View File

@@ -12,7 +12,7 @@ async fn dehaze(_: impl Ctx, image_frame: Table<Raster<CPU>>, strength: Percenta
image_frame
.into_iter()
.map(|mut row| {
let image = row.element;
let image = std::mem::replace(row.element_mut(), Raster::new_cpu(Image::default()));
// Prepare the image data for processing
let image_data = bytemuck::cast_vec(image.data.clone());
let image_buffer = image::Rgba32FImage::from_raw(image.width, image.height, image_data).expect("Failed to convert internal image format into image-rs data type.");
@@ -31,7 +31,7 @@ async fn dehaze(_: impl Ctx, image_frame: Table<Raster<CPU>>, strength: Percenta
base64_string: None,
};
row.element = Raster::new_cpu(dehazed_image);
*row.element_mut() = Raster::new_cpu(dehazed_image);
row
})
.collect()

View File

@@ -24,7 +24,7 @@ async fn blur(
image_frame
.into_iter()
.map(|mut row| {
let image = row.element.clone();
let image = row.element().clone();
// Run blur algorithm
let blurred_image = if radius < 0.1 {
@@ -36,7 +36,7 @@ async fn blur(
Raster::new_cpu(gaussian_blur_algorithm(image.into_data(), radius, gamma))
};
row.element = blurred_image;
*row.element_mut() = blurred_image;
row
})
.collect()
@@ -56,7 +56,7 @@ async fn median_filter(
image_frame
.into_iter()
.map(|mut row| {
let image = row.element.clone();
let image = row.element().clone();
// Apply median filter
let filtered_image = if radius < 0.5 {
@@ -66,7 +66,7 @@ async fn median_filter(
Raster::new_cpu(median_filter_algorithm(image.into_data(), radius as u32))
};
row.element = filtered_image;
*row.element_mut() = filtered_image;
row
})
.collect()

View File

@@ -21,12 +21,12 @@ async fn gradient_map<T: Adjust<Color>>(
gradient: Table<GradientStops>,
reverse: bool,
) -> T {
let Some(row) = gradient.get(0) else { return image };
let Some(gradient) = gradient.element(0) else { return image };
image.adjust(|color| {
let intensity = color.luminance_srgb();
let intensity = if reverse { 1. - intensity } else { intensity };
row.element.evaluate(intensity as f64).to_linear_srgb()
gradient.evaluate(intensity as f64).to_linear_srgb()
});
image

View File

@@ -18,8 +18,8 @@ async fn image_color_palette(
let mut histogram = vec![0; (bins + 1.) as usize];
let mut color_bins = vec![Vec::new(); (bins + 1.) as usize];
for row in image.iter() {
for pixel in row.element.data.iter() {
for element in image.iter_element_values() {
for pixel in element.data.iter() {
let r = pixel.r() * GRID;
let g = pixel.g() * GRID;
let b = pixel.b() * GRID;

View File

@@ -33,9 +33,9 @@ impl From<std::io::Error> for Error {
pub fn sample_image(ctx: impl ExtractFootprint + Clone + Send, image_frame: Table<Raster<CPU>>) -> Table<Raster<CPU>> {
image_frame
.into_iter()
.filter_map(|mut row| {
let image_frame_transform = row.transform;
let image = row.element;
.filter_map(|row| {
let image_frame_transform: DAffine2 = row.attribute_cloned_or_default("transform");
let (image, mut attributes) = row.into_parts();
// Resize the image using the image crate
let data = bytemuck::cast_vec(image.data.clone());
@@ -86,10 +86,9 @@ pub fn sample_image(ctx: impl ExtractFootprint + Clone + Send, image_frame: Tabl
// we need to adjust the offset if we truncate the offset calculation
let new_transform = image_frame_transform * DAffine2::from_translation(offset) * DAffine2::from_scale(size);
attributes.insert("transform", new_transform);
row.transform = new_transform;
row.element = Raster::new_cpu(image);
Some(row)
Some(TableRow::from_parts(Raster::new_cpu(image), attributes))
})
.collect()
}
@@ -114,23 +113,20 @@ pub fn combine_channels(
.zip(alpha)
.filter_map(|(((red, green), blue), alpha)| {
// Turn any default zero-sized image rows into None
let red = red.filter(|i| i.element.width > 0 && i.element.height > 0);
let green = green.filter(|i| i.element.width > 0 && i.element.height > 0);
let blue = blue.filter(|i| i.element.width > 0 && i.element.height > 0);
let alpha = alpha.filter(|i| i.element.width > 0 && i.element.height > 0);
let red = red.filter(|i| i.element().width > 0 && i.element().height > 0);
let green = green.filter(|i| i.element().width > 0 && i.element().height > 0);
let blue = blue.filter(|i| i.element().width > 0 && i.element().height > 0);
let alpha = alpha.filter(|i| i.element().width > 0 && i.element().height > 0);
// Get this row's transform and alpha blending mode from the first non-empty channel
let (transform, alpha_blending, source_node_id) = [&red, &green, &blue, &alpha]
.iter()
.find_map(|i| i.as_ref())
.map(|i| (i.transform, i.alpha_blending, i.source_node_id))?;
let attributes = [&red, &green, &blue, &alpha].iter().find_map(|i| i.as_ref()).map(|i| i.attributes().clone())?;
// Get the common width and height of the channels, which must have equal dimensions
let channel_dimensions = [
red.as_ref().map(|r| (r.element.width, r.element.height)),
green.as_ref().map(|g| (g.element.width, g.element.height)),
blue.as_ref().map(|b| (b.element.width, b.element.height)),
alpha.as_ref().map(|a| (a.element.width, a.element.height)),
red.as_ref().map(|r| (r.element().width, r.element().height)),
green.as_ref().map(|g| (g.element().width, g.element().height)),
blue.as_ref().map(|b| (b.element().width, b.element().height)),
alpha.as_ref().map(|a| (a.element().width, a.element().height)),
];
if channel_dimensions.iter().all(Option::is_none)
|| channel_dimensions
@@ -150,22 +146,22 @@ pub fn combine_channels(
for x in 0..image.width() {
let image_pixel = image.get_pixel_mut(x, y).unwrap();
if let Some(r) = red.as_ref().and_then(|r| r.element.get_pixel(x, y)) {
if let Some(r) = red.as_ref().and_then(|r| r.element().get_pixel(x, y)) {
image_pixel.set_red(r.l().cast_linear_channel());
} else {
image_pixel.set_red(Channel::from_linear(0.));
}
if let Some(g) = green.as_ref().and_then(|g| g.element.get_pixel(x, y)) {
if let Some(g) = green.as_ref().and_then(|g| g.element().get_pixel(x, y)) {
image_pixel.set_green(g.l().cast_linear_channel());
} else {
image_pixel.set_green(Channel::from_linear(0.));
}
if let Some(b) = blue.as_ref().and_then(|b| b.element.get_pixel(x, y)) {
if let Some(b) = blue.as_ref().and_then(|b| b.element().get_pixel(x, y)) {
image_pixel.set_blue(b.l().cast_linear_channel());
} else {
image_pixel.set_blue(Channel::from_linear(0.));
}
if let Some(a) = alpha.as_ref().and_then(|a| a.element.get_pixel(x, y)) {
if let Some(a) = alpha.as_ref().and_then(|a| a.element().get_pixel(x, y)) {
image_pixel.set_alpha(a.l().cast_linear_channel());
} else {
image_pixel.set_alpha(Channel::from_linear(1.));
@@ -173,12 +169,7 @@ pub fn combine_channels(
}
}
Some(TableRow {
element: Raster::new_cpu(image),
transform,
alpha_blending,
source_node_id,
})
Some(TableRow::from_parts(Raster::new_cpu(image), attributes))
})
.collect()
}
@@ -197,32 +188,34 @@ pub fn mask(
// No stencil provided so we return the original image
return image;
};
let stencil_size = DVec2::new(stencil.element.width as f64, stencil.element.height as f64);
let stencil_size = DVec2::new(stencil.element().width as f64, stencil.element().height as f64);
image
.into_iter()
.filter_map(|mut row| {
let image_size = DVec2::new(row.element.width as f64, row.element.height as f64);
let mask_size = stencil.transform.scale_magnitudes();
let image_size = DVec2::new(row.element().width as f64, row.element().height as f64);
let stencil_transform: DAffine2 = stencil.attribute_cloned_or_default("transform");
let mask_size = stencil_transform.scale_magnitudes();
if mask_size == DVec2::ZERO {
return None;
}
// Transforms a point from the background image to the foreground image
let bg_to_fg = row.transform * DAffine2::from_scale(1. / image_size);
let stencil_transform_inverse = stencil.transform.inverse();
let transform_attribute: DAffine2 = row.attribute_cloned_or_default("transform");
let bg_to_fg = transform_attribute * DAffine2::from_scale(1. / image_size);
let stencil_transform_inverse = stencil_transform.inverse();
for y in 0..row.element.height {
for x in 0..row.element.width {
for y in 0..row.element().height {
for x in 0..row.element().width {
let image_point = DVec2::new(x as f64, y as f64);
let mask_point = bg_to_fg.transform_point2(image_point);
let local_mask_point = stencil_transform_inverse.transform_point2(mask_point);
let mask_point = stencil.transform.transform_point2(local_mask_point.clamp(DVec2::ZERO, DVec2::ONE));
let mask_point = (DAffine2::from_scale(stencil_size) * stencil.transform.inverse()).transform_point2(mask_point);
let mask_point = stencil_transform.transform_point2(local_mask_point.clamp(DVec2::ZERO, DVec2::ONE));
let mask_point = (DAffine2::from_scale(stencil_size) * stencil_transform.inverse()).transform_point2(mask_point);
let image_pixel = row.element.data_mut().get_pixel_mut(x, y).unwrap();
let mask_pixel = stencil.element.sample(mask_point);
let image_pixel = row.element_mut().data_mut().get_pixel_mut(x, y).unwrap();
let mask_pixel = stencil.element().sample(mask_point);
*image_pixel = image_pixel.multiplied_alpha(mask_pixel.l().cast_linear_channel());
}
}
@@ -237,20 +230,21 @@ pub fn extend_image_to_bounds(_: impl Ctx, image: Table<Raster<CPU>>, bounds: DA
image
.into_iter()
.map(|mut row| {
let image_aabb = Bbox::unit().affine_transform(row.transform).to_axis_aligned_bbox();
let row_transform: DAffine2 = row.attribute_cloned_or_default("transform");
let image_aabb = Bbox::unit().affine_transform(row_transform).to_axis_aligned_bbox();
let bounds_aabb = Bbox::unit().affine_transform(bounds.transform()).to_axis_aligned_bbox();
if image_aabb.contains(bounds_aabb.start) && image_aabb.contains(bounds_aabb.end) {
return row;
}
let image_data = &row.element.data;
let (image_width, image_height) = (row.element.width, row.element.height);
let image_data = &row.element().data;
let (image_width, image_height) = (row.element().width, row.element().height);
if image_width == 0 || image_height == 0 {
return empty_image((), bounds, Table::new_from_element(Color::TRANSPARENT)).into_iter().next().unwrap();
}
let orig_image_scale = DVec2::new(image_width as f64, image_height as f64);
let layer_to_image_space = DAffine2::from_scale(orig_image_scale) * row.transform.inverse();
let layer_to_image_space = DAffine2::from_scale(orig_image_scale) * row_transform.inverse();
let bounds_in_image_space = Bbox::unit().affine_transform(layer_to_image_space * bounds).to_axis_aligned_bbox();
let new_start = bounds_in_image_space.start.floor().min(DVec2::ZERO);
@@ -270,10 +264,10 @@ pub fn extend_image_to_bounds(_: impl Ctx, image: Table<Raster<CPU>>, bounds: DA
// Compute new transform.
// let layer_to_new_texture_space = (DAffine2::from_scale(1. / new_scale) * DAffine2::from_translation(new_start) * layer_to_image_space).inverse();
let new_texture_to_layer_space = row.transform * DAffine2::from_scale(1. / orig_image_scale) * DAffine2::from_translation(new_start) * DAffine2::from_scale(new_scale);
let new_texture_to_layer_space = row_transform * DAffine2::from_scale(1. / orig_image_scale) * DAffine2::from_translation(new_start) * DAffine2::from_scale(new_scale);
row.element = Raster::new_cpu(new_image);
row.transform = new_texture_to_layer_space;
*row.element_mut() = Raster::new_cpu(new_image);
row.set_attribute("transform", new_texture_to_layer_space);
row
})
.collect()
@@ -284,13 +278,12 @@ pub fn empty_image(_: impl Ctx, transform: DAffine2, color: Table<Color>) -> Tab
let width = transform.transform_vector2(DVec2::new(1., 0.)).length() as u32;
let height = transform.transform_vector2(DVec2::new(0., 1.)).length() as u32;
let color: Option<Color> = color.into();
let image = Image::new(width, height, color.unwrap_or(Color::WHITE));
let color = color.element(0).copied().unwrap_or(Color::WHITE);
let image = Image::new(width, height, color);
let mut result_table = Table::new_from_element(Raster::new_cpu(image));
let row = result_table.get_mut(0).unwrap();
*row.transform = transform;
*row.alpha_blending = AlphaBlending::default();
result_table.set_attribute("transform", 0, transform);
result_table.set_attribute("alpha_blending", 0, AlphaBlending::default());
// Callers of empty_image can safely unwrap on returned table
result_table
@@ -383,11 +376,12 @@ pub fn noise_pattern(
}
}
return Table::new_from_row(TableRow {
element: Raster::new_cpu(image),
transform: DAffine2::from_translation(offset) * DAffine2::from_scale(size),
..Default::default()
});
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>),
);
}
};
noise.set_noise_type(Some(noise_type));
@@ -445,11 +439,12 @@ pub fn noise_pattern(
}
}
Table::new_from_row(TableRow {
element: Raster::new_cpu(image),
transform: DAffine2::from_translation(offset) * DAffine2::from_scale(size),
..Default::default()
})
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>),
)
}
#[node_macro::node(category("Raster: Pattern"))]
@@ -487,16 +482,17 @@ pub fn mandelbrot(ctx: impl ExtractFootprint + Send) -> Table<Raster<CPU>> {
}
}
Table::new_from_row(TableRow {
element: Raster::new_cpu(Image {
Table::new_from_row(
TableRow::new_from_element(Raster::new_cpu(Image {
width,
height,
data,
..Default::default()
}),
transform: DAffine2::from_translation(offset) * DAffine2::from_scale(size),
..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>),
)
}
#[inline(always)]

View File

@@ -1,7 +1,7 @@
use crate::gcore::Context;
use core::f64::consts::TAU;
use core_types::registry::types::{Angle, PixelSize};
use core_types::table::{Table, TableRowRef};
use core_types::table::Table;
use core_types::{CloneVarArgs, Color, Ctx, ExtractAll, InjectVarArgs, OwnedContextImpl};
use glam::{DAffine2, DVec2};
use graphic_types::{Graphic, Vector};
@@ -77,12 +77,13 @@ pub async fn repeat_array<T: Into<Graphic> + Default + Send + Clone + 'static>(
let new_ctx = OwnedContextImpl::from(ctx.clone()).with_index(index as usize);
let generated_instance = instance.eval(new_ctx.into_context()).await;
for row in generated_instance.iter() {
let mut row = row.into_cloned();
for row_index in 0..generated_instance.len() {
let Some(mut row) = generated_instance.clone_row(row_index) else { continue };
let local_translation = DAffine2::from_translation(row.transform.translation);
let local_matrix = DAffine2::from_mat2(row.transform.matrix2);
row.transform = local_translation * transform * local_matrix;
let local_transform: DAffine2 = row.attribute_cloned_or_default("transform");
let local_translation = DAffine2::from_translation(local_transform.translation);
let local_matrix = DAffine2::from_mat2(local_transform.matrix2);
*row.attribute_mut_or_insert_default("transform") = local_translation * transform * local_matrix;
result_table.push(row);
}
@@ -122,12 +123,13 @@ async fn repeat_radial<T: Into<Graphic> + Default + Send + Clone + 'static>(
let new_ctx = OwnedContextImpl::from(ctx.clone()).with_index(index as usize);
let generated_instance = instance.eval(new_ctx.into_context()).await;
for row in generated_instance.iter() {
let mut row = row.into_cloned();
for row_index in 0..generated_instance.len() {
let Some(mut row) = generated_instance.clone_row(row_index) else { continue };
let local_translation = DAffine2::from_translation(row.transform.translation);
let local_matrix = DAffine2::from_mat2(row.transform.matrix2);
row.transform = local_translation * transform * local_matrix;
let local_transform: DAffine2 = row.attribute_cloned_or_default("transform");
let local_translation = DAffine2::from_translation(local_transform.translation);
let local_matrix = DAffine2::from_mat2(local_transform.matrix2);
*row.attribute_mut_or_insert_default("transform") = local_translation * transform * local_matrix;
result_table.push(row);
}
@@ -152,7 +154,10 @@ async fn repeat_on_points<T: Into<Graphic> + Default + Send + Clone + 'static>(
) -> Table<T> {
let mut result_table = Table::new();
for TableRowRef { element: points, transform, .. } in points.iter() {
for points_index in 0..points.len() {
let Some(points_element) = points.element(points_index) else { continue };
let transform: DAffine2 = points.attribute_cloned_or_default("transform", points_index);
let mut iteration = async |index, point| {
let transformed_point = transform.transform_point2(point);
@@ -160,12 +165,12 @@ async fn repeat_on_points<T: Into<Graphic> + Default + Send + Clone + 'static>(
let generated_instance = instance.eval(new_ctx.into_context()).await;
for mut generated_row in generated_instance.into_iter() {
generated_row.transform.translation = transformed_point;
generated_row.attribute_mut_or_insert_default::<DAffine2>("transform").translation = transformed_point;
result_table.push(generated_row);
}
};
let range = points.point_domain.positions().iter().enumerate();
let range = points_element.point_domain.positions().iter().enumerate();
if reverse {
for (index, &point) in range.rev() {
iteration(index, point).await;
@@ -228,8 +233,12 @@ mod test {
let points = Table::new_from_element(Vector::from_subpath(Subpath::from_anchors(positions, false)));
let generated = super::repeat_on_points(context, points, &rect, false).await;
assert_eq!(generated.len(), positions.len());
for (position, generated_row) in positions.into_iter().zip(generated.iter()) {
let bounds = generated_row.element.bounding_box_with_transform(*generated_row.transform).unwrap();
for (position, index) in positions.into_iter().zip(0..generated.len()) {
let bounds = generated
.element(index)
.unwrap()
.bounding_box_with_transform(generated.attribute_cloned_or_default("transform", index))
.unwrap();
assert!(position.abs_diff_eq((bounds[0] + bounds[1]) / 2., 1e-10));
assert_eq!((bounds[1] - bounds[0]).x, position.y);
}
@@ -249,7 +258,7 @@ mod test {
)
.await;
let vector_table = vector_nodes::flatten_path(Footprint::default(), repeated).await;
let vector = vector_table.iter().next().unwrap().element;
let vector = vector_table.element(0).unwrap();
assert_eq!(vector.region_manipulator_groups().count(), 3);
for (index, (_, manipulator_groups)) in vector.region_manipulator_groups().enumerate() {
assert!((manipulator_groups[0].anchor - direction * index as f64 / (count - 1) as f64).length() < 1e-5);
@@ -270,7 +279,7 @@ mod test {
)
.await;
let vector_table = vector_nodes::flatten_path(Footprint::default(), repeated).await;
let vector = vector_table.iter().next().unwrap().element;
let vector = vector_table.element(0).unwrap();
assert_eq!(vector.region_manipulator_groups().count(), 8);
for (index, (_, manipulator_groups)) in vector.region_manipulator_groups().enumerate() {
assert!((manipulator_groups[0].anchor - direction * index as f64 / (count - 1) as f64).length() < 1e-5);
@@ -282,7 +291,7 @@ mod test {
let context = OwnedContextImpl::default().into_context();
let repeated = super::repeat_radial(context, &FutureWrapperNode(vector_node_from_bezpath(Rect::new(-1., -1., 1., 1.).to_path(DEFAULT_ACCURACY))), 45., 4., 8).await;
let vector_table = vector_nodes::flatten_path(Footprint::default(), repeated).await;
let vector = vector_table.iter().next().unwrap().element;
let vector = vector_table.element(0).unwrap();
assert_eq!(vector.region_manipulator_groups().count(), 8);
for (index, (_, manipulator_groups)) in vector.region_manipulator_groups().enumerate() {

View File

@@ -8,6 +8,7 @@ license = "MIT OR Apache-2.0"
[features]
default = ["serde"]
serde = ["dep:serde", "core-types/serde", "vector-types/serde"]
wasm = ["core-types/wasm", "tsify", "wasm-bindgen"]
[dependencies]

View File

@@ -6,13 +6,14 @@ use std::sync::Arc;
/// A font type (storing font family and font style and an optional preview URL)
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Eq, DynAny)]
#[derive(Debug, Clone, Eq, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Font {
#[serde(rename = "fontFamily")]
#[cfg_attr(feature = "serde", serde(rename = "fontFamily"))]
pub font_family: String,
#[serde(rename = "fontStyle", deserialize_with = "migrate_font_style")]
#[cfg_attr(feature = "serde", serde(rename = "fontStyle", deserialize_with = "migrate_font_style"))]
pub font_style: String,
#[serde(skip)]
#[cfg_attr(feature = "serde", serde(skip))]
pub font_style_to_restore: Option<String>,
}
@@ -72,7 +73,8 @@ impl Default for Font {
}
/// A cache of all loaded font data and preview urls along with the default font (send from `init_app` in `editor_api.rs`)
#[derive(Clone, serde::Serialize, serde::Deserialize, Default, DynAny)]
#[derive(Clone, Default, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct FontCache {
/// Actual font file data used for rendering a font
font_file_data: HashMap<Font, Vec<u8>>,

View File

@@ -26,7 +26,8 @@ pub use vector_types;
/// Alignment of lines of type within a text block.
#[repr(C)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, CacheHash, DynAny, node_macro::ChoiceType)]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, CacheHash, DynAny, node_macro::ChoiceType)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[widget(Radio)]
pub enum TextAlign {
#[default]
@@ -49,7 +50,8 @@ impl From<TextAlign> for parley::Alignment {
}
}
#[derive(PartialEq, Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
#[derive(PartialEq, Clone, Copy, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct TypesettingConfig {
pub font_size: f64,
pub line_height_ratio: f64,

View File

@@ -1,3 +1,4 @@
use core_types::AlphaBlending;
use core_types::table::{Table, TableRow};
use glam::{DAffine2, DVec2};
use parley::GlyphRun;
@@ -51,15 +52,16 @@ impl<Upstream: Default + 'static> PathBuilder<Upstream> {
}
if per_glyph_instances {
self.vector_table.push(TableRow {
element: Vector::from_subpaths(core::mem::take(&mut self.glyph_subpaths), false),
transform: DAffine2::from_translation(glyph_offset),
..Default::default()
});
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>),
);
} else {
for subpath in self.glyph_subpaths.drain(..) {
// Unwrapping here is ok because `self.vector_table` is initialized with a single `Vector` table element
self.vector_table.get_mut(0).unwrap().element.append_subpath(subpath, false);
self.vector_table.element_mut(0).unwrap().append_subpath(subpath, false);
}
}
}

View File

@@ -66,24 +66,20 @@ fn reset_transform<T>(
reset_rotation: bool,
reset_scale: bool,
) -> Table<T> {
for row in content.iter_mut() {
// Translation
for row_transform in content.iter_attribute_values_mut_or_default::<DAffine2>("transform") {
if reset_translation {
row.transform.translation = DVec2::ZERO;
row_transform.translation = DVec2::ZERO;
}
// (Rotation, Scale)
match (reset_rotation, reset_scale) {
(true, true) => {
row.transform.matrix2 = DMat2::IDENTITY;
}
(true, true) => row_transform.matrix2 = DMat2::IDENTITY,
(true, false) => {
let scale = row.transform.scale_magnitudes();
row.transform.matrix2 = DMat2::from_diagonal(scale);
let scale = row_transform.scale_magnitudes();
row_transform.matrix2 = DMat2::from_diagonal(scale);
}
(false, true) => {
let rotation = row.transform.decompose_rotation();
let rotation_matrix = DMat2::from_angle(rotation);
row.transform.matrix2 = rotation_matrix;
let rotation = row_transform.decompose_rotation();
row_transform.matrix2 = DMat2::from_angle(rotation);
}
(false, false) => {}
}
@@ -106,8 +102,8 @@ fn replace_transform<T>(
mut content: Table<T>,
transform: DAffine2,
) -> Table<T> {
for row in content.iter_mut() {
*row.transform = transform.transform();
for row_transform in content.iter_attribute_values_mut_or_default::<DAffine2>("transform") {
*row_transform = transform.transform();
}
content
}
@@ -127,7 +123,7 @@ async fn extract_transform<T>(
)]
content: Table<T>,
) -> DAffine2 {
content.iter().next().map(|row| *row.transform).unwrap_or_default()
content.attribute_cloned_or_default::<DAffine2>("transform", 0)
}
/// Produces the inverse of the input transform, which is the transform that undoes the effect of the original transform.

View File

@@ -8,6 +8,7 @@ license = "MIT OR Apache-2.0"
[features]
default = ["serde"]
serde = ["dep:serde", "core-types/serde", "vector-types/serde", "graphic-types/serde"]
wasm = ["core-types/wasm", "tsify", "wasm-bindgen"]
[dependencies]

View File

@@ -188,7 +188,8 @@ fn star<T: AsU64>(
}
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, CacheHash, DynAny, node_macro::ChoiceType)]
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, CacheHash, DynAny, node_macro::ChoiceType)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[widget(Radio)]
pub enum QRCodeErrorCorrectionLevel {
/// Allows recovery from up to 7% data loss.
@@ -399,9 +400,9 @@ mod tests {
// Works properly
let grid = grid((), (), GridType::Isometric, 10., 5, 5, (30., 30.).into());
assert_eq!(grid.iter().next().unwrap().element.point_domain.ids().len(), 5 * 5);
assert_eq!(grid.iter().next().unwrap().element.segment_bezier_iter().count(), 4 * 5 + 4 * 9);
for (_, bezier, _, _) in grid.iter().next().unwrap().element.segment_bezier_iter() {
assert_eq!(grid.element(0).unwrap().point_domain.ids().len(), 5 * 5);
assert_eq!(grid.element(0).unwrap().segment_bezier_iter().count(), 4 * 5 + 4 * 9);
for (_, bezier, _, _) in grid.element(0).unwrap().segment_bezier_iter() {
assert_eq!(bezier.handles, subpath::BezierHandles::Linear);
assert!(
((bezier.start - bezier.end).length() - 10.).abs() < 1e-5,
@@ -414,9 +415,9 @@ mod tests {
#[test]
fn skew_isometric_grid_test() {
let grid = grid((), (), GridType::Isometric, 10., 5, 5, (40., 30.).into());
assert_eq!(grid.iter().next().unwrap().element.point_domain.ids().len(), 5 * 5);
assert_eq!(grid.iter().next().unwrap().element.segment_bezier_iter().count(), 4 * 5 + 4 * 9);
for (_, bezier, _, _) in grid.iter().next().unwrap().element.segment_bezier_iter() {
assert_eq!(grid.element(0).unwrap().point_domain.ids().len(), 5 * 5);
assert_eq!(grid.element(0).unwrap().segment_bezier_iter().count(), 4 * 5 + 4 * 9);
for (_, bezier, _, _) in grid.element(0).unwrap().segment_bezier_iter() {
assert_eq!(bezier.handles, subpath::BezierHandles::Linear);
let vector = bezier.start - bezier.end;
let angle = (vector.angle_to(DVec2::X).to_degrees() + 180.) % 180.;
@@ -427,7 +428,7 @@ mod tests {
#[test]
fn qr_code_test() {
let qr = qr_code((), (), "https://graphite.art".to_string(), false, 1., QRCodeErrorCorrectionLevel::Low, true);
assert!(qr.iter().next().unwrap().element.point_domain.ids().len() > 0);
assert!(qr.iter().next().unwrap().element.segment_domain.ids().len() > 0);
assert!(qr.element(0).unwrap().point_domain.ids().len() > 0);
assert!(qr.element(0).unwrap().segment_domain.ids().len() > 0);
}
}

View File

@@ -13,12 +13,12 @@ async fn path_modify(_ctx: impl Ctx, mut vector: Table<Vector>, modification: Bo
if vector.is_empty() {
vector.push(TableRow::default());
}
let row = vector.get_mut(0).expect("push should give one item");
modification.apply(row.element);
modification.apply(vector.element_mut(0).expect("push should give one item"));
// Update the source node id
let this_node_path = node_path.iter().rev().nth(1).copied();
*row.source_node_id = row.source_node_id.or(this_node_path);
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));
if vector.len() > 1 {
warn!("The path modify ran on {} vector rows. Only the first can be modified.", vector.len());
@@ -29,16 +29,14 @@ async fn path_modify(_ctx: impl Ctx, mut vector: Table<Vector>, modification: Bo
/// Applies the vector path's local transformation to its geometry and resets the transform to the identity.
#[node_macro::node(category("Vector"))]
async fn apply_transform(_ctx: impl Ctx, mut vector: Table<Vector>) -> Table<Vector> {
for row in vector.iter_mut() {
let vector = row.element;
let transform = *row.transform;
for (_, point) in vector.point_domain.positions_mut() {
let (elements, transforms) = vector.element_and_attribute_slices_mut::<DAffine2>("transform");
for (element, transform) in elements.iter_mut().zip(transforms.iter_mut()) {
for (_, point) in element.point_domain.positions_mut() {
*point = transform.transform_point2(*point);
}
vector.segment_domain.transform(transform);
element.segment_domain.transform(*transform);
*row.transform = DAffine2::IDENTITY;
*transform = DAffine2::IDENTITY;
}
vector

File diff suppressed because it is too large Load Diff