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

@@ -7,6 +7,8 @@ authors = ["Graphite Authors <contact@graphite.art>"]
license = "MIT OR Apache-2.0"
[features]
default = ["serde"]
serde = ["dep:serde", "core-types/serde", "vector-types/serde", "text-nodes/serde"]
wasm = ["dep:web-sys"]
wgpu = ["dep:wgpu"]
@@ -19,9 +21,11 @@ text-nodes = { workspace = true }
# Workspace dependencies
glam = { workspace = true }
serde = { workspace = true }
log = { workspace = true }
# Optional workspace dependencies
serde = { workspace = true, optional = true }
# Optional workspace dependencies
web-sys = { workspace = true, optional = true }
wgpu = { workspace = true, optional = true }

View File

@@ -73,7 +73,8 @@ pub enum ApplicationError {
InvalidUrl,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum NodeGraphUpdateMessage {}
pub trait NodeGraphUpdateSender {
@@ -90,26 +91,29 @@ pub trait GetEditorPreferences {
fn max_render_region_area(&self) -> u32;
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum ExportFormat {
#[default]
Svg,
Raster,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, DynAny, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Default, Clone, Copy, PartialEq, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct TimingInformation {
pub time: f64,
pub animation_time: Duration,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, DynAny, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Default, Clone, Copy, PartialEq, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct RenderConfig {
pub viewport: Footprint,
pub scale: f64,
pub time: TimingInformation,
pub pointer: DVec2,
#[serde(alias = "view_mode")]
#[cfg_attr(feature = "serde", serde(alias = "view_mode"))]
pub render_mode: RenderMode,
pub export_format: ExportFormat,
pub for_export: bool,

View File

@@ -8,6 +8,7 @@ license = "MIT OR Apache-2.0"
[features]
default = ["serde"]
serde = ["dep:serde"]
nightly = []
type_id_logging = []
dealloc_nodes = []

View File

@@ -130,7 +130,8 @@ impl<T: Ctx + InjectVarArgs + ExtractVarArgs> ModifyVarArgs for T {}
// ================
// Public enum for flexible node macro codegen
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum ContextFeature {
ExtractFootprint,
ExtractRealTime,
@@ -151,7 +152,8 @@ pub enum ContextFeature {
// Internal bitflags for fast compiler analysis
use bitflags::bitflags;
bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, dyn_any::DynAny, serde::Serialize, serde::Deserialize, Default)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, dyn_any::DynAny, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ContextFeatures: u32 {
const FOOTPRINT = 1 << 0;
const REAL_TIME = 1 << 1;
@@ -188,7 +190,8 @@ impl ContextFeatures {
// CONTEXT DEPENDENCIES
// ====================
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, graphene_hash::CacheHash, dyn_any::DynAny, serde::Serialize, serde::Deserialize, Default)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, graphene_hash::CacheHash, dyn_any::DynAny, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ContextDependencies {
pub extract: ContextFeatures,
pub inject: ContextFeatures,

View File

@@ -67,8 +67,8 @@ pub fn migrate_color<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Resul
use no_std_types::color::Color;
use serde::Deserialize;
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(untagged)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(untagged))]
enum ColorFormat {
Color(Color),
OptionalColor(Option<Color>),

View File

@@ -62,11 +62,9 @@ impl<U, T: TableConvert<U> + Send> Convert<Table<U>, ()> for Table<T> {
async fn convert(self, _: Footprint, _: ()) -> Table<U> {
let table: Table<U> = self
.into_iter()
.map(|row| TableRow {
element: row.element.convert_row(),
transform: row.transform,
alpha_blending: row.alpha_blending,
source_node_id: row.source_node_id,
.map(|row| {
let (element, attributes) = row.into_parts();
TableRow::from_parts(element.convert_row(), attributes)
})
.collect();
table

View File

@@ -10,7 +10,7 @@ pub trait RenderComplexity {
impl<T: RenderComplexity> RenderComplexity for Table<T> {
fn render_complexity(&self) -> usize {
self.iter().map(|row| row.element.render_complexity()).fold(0, usize::saturating_add)
self.iter_element_values().map(|element| element.render_complexity()).fold(0, usize::saturating_add)
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -11,7 +11,8 @@ pub use to_path::*;
/// 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, DynAny, node_macro::ChoiceType)]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[widget(Radio)]
pub enum TextAlign {
#[default]
@@ -34,7 +35,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

@@ -6,7 +6,8 @@ use glam::{DAffine2, DMat2, DVec2, UVec2};
/// Controls whether the Decompose Scale node returns axis-length magnitudes or pure scale factors.
#[repr(C)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, graphene_hash::CacheHash, DynAny, node_macro::ChoiceType)]
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, graphene_hash::CacheHash, DynAny, node_macro::ChoiceType)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[widget(Radio)]
pub enum ScaleType {
/// The visual length of each axis (always positive, includes any skew contribution).
@@ -141,7 +142,8 @@ impl TransformMut for Footprint {
}
}
#[derive(Debug, Clone, Copy, dyn_any::DynAny, PartialEq, graphene_hash::CacheHash, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Clone, Copy, dyn_any::DynAny, PartialEq, graphene_hash::CacheHash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum RenderQuality {
/// Low quality, fast rendering
Preview,
@@ -154,7 +156,8 @@ pub enum RenderQuality {
/// Render at full quality
Full,
}
#[derive(Debug, Clone, Copy, dyn_any::DynAny, PartialEq, graphene_hash::CacheHash, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Clone, Copy, dyn_any::DynAny, PartialEq, graphene_hash::CacheHash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Footprint {
/// Inverse of the transform which will be applied to the node output during the rendering process
pub transform: DAffine2,

View File

@@ -77,7 +77,8 @@ macro_rules! fn_type_fut {
}
// TODO: Rename to NodeSignatureMonomorphization
#[derive(Clone, PartialEq, Eq, Hash, graphene_hash::CacheHash, Default, serde::Serialize, serde::Deserialize)]
#[derive(Clone, PartialEq, Eq, Hash, graphene_hash::CacheHash, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct NodeIOTypes {
pub call_argument: Type,
pub return_value: Type,
@@ -126,7 +127,8 @@ impl std::fmt::Debug for NodeIOTypes {
}
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Clone, Debug, PartialEq, Eq, Hash, graphene_hash::CacheHash, serde::Serialize, serde::Deserialize)]
#[derive(Clone, Debug, PartialEq, Eq, Hash, graphene_hash::CacheHash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ProtoNodeIdentifier {
name: Cow<'static, str>,
}
@@ -180,17 +182,18 @@ fn migrate_type_descriptor_names<'de, D: serde::Deserializer<'de>>(deserializer:
}
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Clone, Debug, Eq, serde::Serialize, serde::Deserialize)]
#[derive(Clone, Debug, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct TypeDescriptor {
#[serde(skip)]
#[cfg_attr(feature = "serde", serde(skip))]
pub id: Option<TypeId>,
#[serde(deserialize_with = "migrate_type_descriptor_names")]
#[cfg_attr(feature = "serde", serde(deserialize_with = "migrate_type_descriptor_names"))]
pub name: Cow<'static, str>,
#[serde(default)]
#[cfg_attr(feature = "serde", serde(default))]
pub alias: Option<Cow<'static, str>>,
#[serde(skip)]
#[cfg_attr(feature = "serde", serde(skip))]
pub size: usize,
#[serde(skip)]
#[cfg_attr(feature = "serde", serde(skip))]
pub align: usize,
}
@@ -228,7 +231,8 @@ impl PartialEq for TypeDescriptor {
/// Graph runtime type information used for type inference.
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Clone, PartialEq, Eq, Hash, graphene_hash::CacheHash, serde::Serialize, serde::Deserialize)]
#[derive(Clone, PartialEq, Eq, Hash, graphene_hash::CacheHash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Type {
/// A wrapper for some type variable used within the inference system. Resolved at inference time and replaced with a concrete type.
Generic(Cow<'static, str>),

View File

@@ -2,7 +2,8 @@ use dyn_any::DynAny;
pub use uuid_generation::*;
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Clone, Copy, serde::Serialize, serde::Deserialize)]
#[derive(Clone, Copy)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Uuid(#[serde(with = "u64_string")] u64);
mod u64_string {
@@ -68,7 +69,8 @@ mod uuid_generation {
#[repr(transparent)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify), tsify(large_number_types_as_bigints))]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, graphene_hash::CacheHash, PartialOrd, Ord, serde::Serialize, serde::Deserialize, DynAny)]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, graphene_hash::CacheHash, PartialOrd, Ord, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct NodeId(pub u64);
impl NodeId {

View File

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

View File

@@ -12,7 +12,8 @@ use glam::{DAffine2, DVec2, IVec2};
use graphene_hash::CacheHash;
/// Some [`ArtboardData`] with some optional clipping bounds that can be exported.
#[derive(Clone, Debug, CacheHash, PartialEq, DynAny, serde::Serialize, serde::Deserialize)]
#[derive(Clone, Debug, CacheHash, PartialEq, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Artboard {
pub content: Table<Graphic>,
pub label: String,
@@ -49,9 +50,22 @@ impl BoundingBox for Artboard {
return RenderBoundingBox::Rectangle(artboard_bounds());
}
match self.content.bounding_box(transform, include_stroke) {
RenderBoundingBox::Rectangle(content_bounds) => RenderBoundingBox::Rectangle(Quad::combine_bounds(content_bounds, artboard_bounds())),
other => other,
let mut combined_bounds = None;
for (element, row_transform) in self.content.iter_element_values().zip(self.content.iter_attribute_values_or_default::<DAffine2>("transform")) {
match element.bounding_box(transform * row_transform, include_stroke) {
RenderBoundingBox::None => continue,
RenderBoundingBox::Infinite => return RenderBoundingBox::Infinite,
RenderBoundingBox::Rectangle(bounds) => match combined_bounds {
Some(existing) => combined_bounds = Some(Quad::combine_bounds(existing, bounds)),
None => combined_bounds = Some(bounds),
},
}
}
match combined_bounds {
Some(content_bounds) => RenderBoundingBox::Rectangle(Quad::combine_bounds(content_bounds, artboard_bounds())),
None => RenderBoundingBox::Rectangle(artboard_bounds()),
}
}
}
@@ -76,22 +90,24 @@ impl Transform for Artboard {
pub fn migrate_artboard<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<Table<Artboard>, D::Error> {
use serde::Deserialize;
#[derive(Clone, Default, Debug, PartialEq, DynAny, serde::Serialize, serde::Deserialize)]
#[derive(Clone, Default, Debug, PartialEq, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ArtboardGroup {
pub artboards: Vec<(Artboard, Option<NodeId>)>,
}
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(untagged)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(untagged))]
enum ArtboardFormat {
ArtboardGroup(ArtboardGroup),
OldArtboardTable(OldTable<Artboard>),
ArtboardTable(Table<Artboard>),
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct OldTable<T> {
#[serde(alias = "instances", alias = "instance")]
#[cfg_attr(feature = "serde", serde(alias = "instances", alias = "instance"))]
element: Vec<T>,
transform: Vec<DAffine2>,
alpha_blending: Vec<AlphaBlending>,
@@ -101,12 +117,12 @@ pub fn migrate_artboard<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Re
ArtboardFormat::ArtboardGroup(artboard_group) => {
let mut table = Table::new();
for (artboard, source_node_id) in artboard_group.artboards {
table.push(TableRow {
element: artboard,
transform: DAffine2::IDENTITY,
alpha_blending: AlphaBlending::default(),
source_node_id,
});
table.push(
TableRow::new_from_element(artboard)
.with_attribute("transform", DAffine2::IDENTITY)
.with_attribute("alpha_blending", AlphaBlending::default())
.with_attribute("source_node_id", source_node_id),
);
}
table
}
@@ -114,11 +130,11 @@ pub fn migrate_artboard<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Re
.element
.into_iter()
.zip(old_table.transform.into_iter().zip(old_table.alpha_blending))
.map(|(element, (transform, alpha_blending))| TableRow {
element,
transform,
alpha_blending,
source_node_id: None,
.map(|(element, (transform, alpha_blending))| {
TableRow::new_from_element(element)
.with_attribute("transform", transform)
.with_attribute("alpha_blending", alpha_blending)
.with_attribute("source_node_id", None::<NodeId>)
})
.collect(),
ArtboardFormat::ArtboardTable(artboard_table) => artboard_table,

View File

@@ -15,7 +15,8 @@ use vector_types::GradientStops;
pub type Vector = vector_types::Vector<Option<Table<Graphic>>>;
/// The possible forms of graphical content that can be rendered by the Render node into either an image or SVG syntax.
#[derive(Clone, Debug, CacheHash, PartialEq, DynAny, serde::Serialize, serde::Deserialize)]
#[derive(Clone, Debug, CacheHash, PartialEq, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Graphic {
Graphic(Table<Graphic>),
Vector(Table<Vector>),
@@ -31,6 +32,28 @@ impl Default for Graphic {
}
}
// Explicit `Send`/`Sync` impls. All fields are themselves `Send`/`Sync`, so these would normally
// be inferred, but the type participates in two mutually recursive cycles through `Table<Graphic>`
// and `Table<Vector>` (where `Vector = vector_types::Vector<Option<Table<Graphic>>>`). The second
// path, wrapped in `Option<_>` and a generic type parameter, produces a distinct auto-trait
// obligation that the solver cannot recognize as the same cycle node, causing
// `overflow evaluating the requirement` errors at the workspace's `once_cell::sync::Lazy` statics.
// Providing these impls explicitly anchors the proof and lets the coinductive cache close both cycles.
//
// These can be removed (reverting to auto-derived `Send`/`Sync`) once any of the following holds:
// - We remove the TaggedValue or its variants that contain tables.
// - The `Vector` alias no longer references `Graphic` through a generic type parameter, breaking
// the second cycle so only the direct `Table<Graphic>` self-cycle remains (which the solver
// already handles on its own).
// - `Graphic` stops containing `Table<Graphic>` directly, e.g. by boxing children through a trait
// object or opaque handle so the recursion is no longer structural.
// - A future rustc release improves the auto-trait solver to recognize cycles across generic-
// parameter substitutions. Try deleting these impls and running:
// `cargo check --tests -p graphite-editor`
// If no `overflow evaluating the requirement` errors appear, they're no longer needed).
unsafe impl Send for Graphic {}
unsafe impl Sync for Graphic {}
// Graphic
impl From<Table<Graphic>> for Graphic {
fn from(graphic: Table<Graphic>) -> Self {
@@ -118,14 +141,19 @@ fn flatten_graphic_table<T>(content: Table<Graphic>, extract_variant: fn(Graphic
fn flatten_recursive<T>(output: &mut Table<T>, current_graphic_table: Table<Graphic>, extract_variant: fn(Graphic) -> Option<Table<T>>) {
for current_graphic_row in current_graphic_table.into_iter() {
let source_node_id = current_graphic_row.source_node_id;
let source_node_id: Option<NodeId> = current_graphic_row.attribute_cloned_or_default("source_node_id");
let current_transform: DAffine2 = current_graphic_row.attribute_cloned_or_default("transform");
let current_alpha_blending: AlphaBlending = current_graphic_row.attribute_cloned_or_default("alpha_blending");
match current_graphic_row.element {
match current_graphic_row.into_element() {
// Recurse into nested graphic tables, composing the parent's transform onto each child
Graphic::Graphic(mut sub_table) => {
for graphic in sub_table.iter_mut() {
*graphic.transform = current_graphic_row.transform * *graphic.transform;
*graphic.alpha_blending = compose_alpha_blending(current_graphic_row.alpha_blending, *graphic.alpha_blending);
for index in 0..sub_table.len() {
let child_transform: DAffine2 = sub_table.attribute_cloned_or_default("transform", index);
let child_alpha_blending: AlphaBlending = sub_table.attribute_cloned_or_default("alpha_blending", index);
sub_table.set_attribute("transform", index, current_transform * child_transform);
sub_table.set_attribute("alpha_blending", index, compose_alpha_blending(current_alpha_blending, child_alpha_blending));
}
flatten_recursive(output, sub_table, extract_variant);
@@ -134,12 +162,15 @@ fn flatten_graphic_table<T>(content: Table<Graphic>, extract_variant: fn(Graphic
other => {
if let Some(typed_table) = extract_variant(other) {
for row in typed_table.into_iter() {
output.push(TableRow {
element: row.element,
transform: current_graphic_row.transform * row.transform,
alpha_blending: compose_alpha_blending(current_graphic_row.alpha_blending, row.alpha_blending),
source_node_id,
});
let row_transform: DAffine2 = row.attribute_cloned_or_default("transform");
let row_alpha_blending: AlphaBlending = row.attribute_cloned_or_default("alpha_blending");
let (element, mut attributes) = row.into_parts();
attributes.insert("transform", current_transform * row_transform);
attributes.insert("alpha_blending", compose_alpha_blending(current_alpha_blending, row_alpha_blending));
attributes.insert("source_node_id", source_node_id);
output.push(TableRow::from_parts(element, attributes));
}
}
}
@@ -289,23 +320,27 @@ impl Graphic {
}
pub fn had_clip_enabled(&self) -> bool {
fn all_clipped<T>(table: &Table<T>) -> bool {
table.iter_attribute_values_or_default::<AlphaBlending>("alpha_blending").all(|a| a.clip)
}
match self {
Graphic::Vector(vector) => vector.iter().all(|row| row.alpha_blending.clip),
Graphic::Graphic(graphic) => graphic.iter().all(|row| row.alpha_blending.clip),
Graphic::RasterCPU(raster) => raster.iter().all(|row| row.alpha_blending.clip),
Graphic::RasterGPU(raster) => raster.iter().all(|row| row.alpha_blending.clip),
Graphic::Color(color) => color.iter().all(|row| row.alpha_blending.clip),
Graphic::Gradient(gradient) => gradient.iter().all(|row| row.alpha_blending.clip),
Graphic::Vector(table) => all_clipped(table),
Graphic::Graphic(table) => all_clipped(table),
Graphic::RasterCPU(table) => all_clipped(table),
Graphic::RasterGPU(table) => all_clipped(table),
Graphic::Color(table) => all_clipped(table),
Graphic::Gradient(table) => all_clipped(table),
}
}
pub fn can_reduce_to_clip_path(&self) -> bool {
match self {
Graphic::Vector(vector) => vector.iter().all(|row| {
let style = &row.element.style;
let alpha_blending = &row.alpha_blending;
(alpha_blending.opacity > 1. - f32::EPSILON) && style.fill().is_opaque() && style.stroke().is_none_or(|stroke| !stroke.has_renderable_stroke())
}),
Graphic::Vector(vector) => vector
.iter_element_values()
.zip(vector.iter_attribute_values_or_default::<AlphaBlending>("alpha_blending"))
.all(|(element, alpha_blending)| {
(alpha_blending.opacity > 1. - f32::EPSILON) && element.style.fill().is_opaque() && element.style.stroke().is_none_or(|stroke| !stroke.has_renderable_stroke())
}),
_ => false,
}
}
@@ -314,12 +349,12 @@ impl Graphic {
impl BoundingBox for Graphic {
fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox {
match self {
Graphic::Vector(vector) => vector.bounding_box(transform, include_stroke),
Graphic::RasterCPU(raster) => raster.bounding_box(transform, include_stroke),
Graphic::RasterGPU(raster) => raster.bounding_box(transform, include_stroke),
Graphic::Graphic(graphic) => graphic.bounding_box(transform, include_stroke),
Graphic::Color(color) => color.bounding_box(transform, include_stroke),
Graphic::Gradient(gradient) => gradient.bounding_box(transform, include_stroke),
Graphic::Vector(table) => table.bounding_box(transform, include_stroke),
Graphic::RasterCPU(table) => table.bounding_box(transform, include_stroke),
Graphic::RasterGPU(table) => table.bounding_box(transform, include_stroke),
Graphic::Graphic(table) => table.bounding_box(transform, include_stroke),
Graphic::Color(table) => table.bounding_box(transform, include_stroke),
Graphic::Gradient(table) => table.bounding_box(transform, include_stroke),
}
}
}
@@ -375,25 +410,15 @@ impl<T: Clone> AtIndex for Table<T> {
type Output = Table<T>;
fn at_index(&self, index: usize) -> Option<Self::Output> {
let mut result_table = Self::default();
if let Some(row) = self.iter().nth(index) {
result_table.push(row.into_cloned());
Some(result_table)
} else {
None
}
self.clone_row(index).map(|row| {
let mut result_table = Self::default();
result_table.push(row);
result_table
})
}
fn at_index_from_end(&self, index: usize) -> Option<Self::Output> {
let mut result_table = Self::default();
if index == 0 || index > self.len() {
None
} else if let Some(row) = self.iter().nth(self.len() - index) {
result_table.push(row.into_cloned());
Some(result_table)
} else {
None
}
if index == 0 || index > self.len() { None } else { self.at_index(self.len() - index) }
}
}
@@ -416,9 +441,11 @@ impl<T: Clone> OmitIndex for Vec<T> {
impl<T: Clone> OmitIndex for Table<T> {
fn omit_index(&self, index: usize) -> Self {
let mut result = Self::default();
for (i, row) in self.iter().enumerate() {
if i != index {
result.push(row.into_cloned());
for i in 0..self.len() {
if i != index
&& let Some(row) = self.clone_row(i)
{
result.push(row);
}
}
result
@@ -436,35 +463,39 @@ impl<T: Clone> OmitIndex for Table<T> {
pub fn migrate_graphic<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<Table<Graphic>, D::Error> {
use serde::Deserialize;
#[derive(Clone, Debug, PartialEq, DynAny, Default, serde::Serialize, serde::Deserialize)]
#[derive(Clone, Debug, PartialEq, DynAny, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct OldGraphicGroup {
elements: Vec<(Graphic, Option<NodeId>)>,
transform: DAffine2,
alpha_blending: AlphaBlending,
}
#[derive(Clone, Debug, PartialEq, DynAny, Default, serde::Serialize, serde::Deserialize)]
#[derive(Clone, Debug, PartialEq, DynAny, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct GraphicGroup {
elements: Vec<(Graphic, Option<NodeId>)>,
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct OlderTable<T> {
id: Vec<u64>,
#[serde(alias = "instances", alias = "instance")]
#[cfg_attr(feature = "serde", serde(alias = "instances", alias = "instance"))]
element: Vec<T>,
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct OldTable<T> {
id: Vec<u64>,
#[serde(alias = "instances", alias = "instance")]
#[cfg_attr(feature = "serde", serde(alias = "instances", alias = "instance"))]
element: Vec<T>,
transform: Vec<DAffine2>,
alpha_blending: Vec<AlphaBlending>,
}
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(untagged)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(untagged))]
enum GraphicFormat {
OldGraphicGroup(OldGraphicGroup),
OlderTableOldGraphicGroup(OlderTable<OldGraphicGroup>),
@@ -477,12 +508,12 @@ pub fn migrate_graphic<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Res
GraphicFormat::OldGraphicGroup(old) => {
let mut graphic_table = Table::new();
for (graphic, source_node_id) in old.elements {
graphic_table.push(TableRow {
element: graphic,
transform: old.transform,
alpha_blending: old.alpha_blending,
source_node_id,
});
graphic_table.push(
TableRow::new_from_element(graphic)
.with_attribute("transform", old.transform)
.with_attribute("alpha_blending", old.alpha_blending)
.with_attribute("source_node_id", source_node_id),
);
}
graphic_table
}
@@ -490,11 +521,11 @@ pub fn migrate_graphic<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Res
.element
.into_iter()
.flat_map(|element| {
element.elements.into_iter().map(move |(graphic, source_node_id)| TableRow {
element: graphic,
transform: element.transform,
alpha_blending: element.alpha_blending,
source_node_id,
element.elements.into_iter().map(move |(graphic, source_node_id)| {
TableRow::new_from_element(graphic)
.with_attribute("transform", element.transform)
.with_attribute("alpha_blending", element.alpha_blending)
.with_attribute("source_node_id", source_node_id)
})
})
.collect(),
@@ -502,11 +533,11 @@ pub fn migrate_graphic<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Res
.element
.into_iter()
.flat_map(|element| {
element.elements.into_iter().map(move |(graphic, source_node_id)| TableRow {
element: graphic,
transform: element.transform,
alpha_blending: element.alpha_blending,
source_node_id,
element.elements.into_iter().map(move |(graphic, source_node_id)| {
TableRow::new_from_element(graphic)
.with_attribute("transform", element.transform)
.with_attribute("alpha_blending", element.alpha_blending)
.with_attribute("source_node_id", source_node_id)
})
})
.collect(),
@@ -514,11 +545,11 @@ pub fn migrate_graphic<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Res
.element
.into_iter()
.flat_map(|element| {
element.elements.into_iter().map(move |(graphic, source_node_id)| TableRow {
element: graphic,
transform: Default::default(),
alpha_blending: Default::default(),
source_node_id,
element.elements.into_iter().map(move |(graphic, source_node_id)| {
TableRow::new_from_element(graphic)
.with_attribute("transform", DAffine2::IDENTITY)
.with_attribute("alpha_blending", AlphaBlending::default())
.with_attribute("source_node_id", source_node_id)
})
})
.collect(),
@@ -526,14 +557,17 @@ pub fn migrate_graphic<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Res
// Try to deserialize as either table format
if let Ok(old_table) = serde_json::from_value::<Table<GraphicGroup>>(value.clone()) {
let mut graphic_table = Table::new();
for row in old_table.iter() {
for (graphic, source_node_id) in &row.element.elements {
graphic_table.push(TableRow {
element: graphic.clone(),
transform: *row.transform,
alpha_blending: *row.alpha_blending,
source_node_id: *source_node_id,
});
for index in 0..old_table.len() {
let row_transform: DAffine2 = old_table.attribute_cloned_or_default("transform", index);
let row_alpha_blending: AlphaBlending = old_table.attribute_cloned_or_default("alpha_blending", index);
for (graphic, source_node_id) in &old_table.element(index).unwrap().elements {
graphic_table.push(
TableRow::new_from_element(graphic.clone())
.with_attribute("transform", row_transform)
.with_attribute("alpha_blending", row_alpha_blending)
.with_attribute("source_node_id", *source_node_id),
);
}
}
graphic_table

View File

@@ -25,7 +25,8 @@ pub mod migrations {
pub fn migrate_vector<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<Table<Vector>, D::Error> {
use serde::Deserialize;
#[derive(Clone, Debug, PartialEq, DynAny, serde::Serialize, serde::Deserialize)]
#[derive(Clone, Debug, PartialEq, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct OldVectorData {
pub transform: DAffine2,
pub alpha_blending: AlphaBlending,
@@ -41,23 +42,25 @@ pub mod migrations {
pub upstream_graphic_group: Option<Table<Graphic>>,
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct OldTable<T> {
#[serde(alias = "instances", alias = "instance")]
#[cfg_attr(feature = "serde", serde(alias = "instances", alias = "instance"))]
element: Vec<T>,
transform: Vec<DAffine2>,
alpha_blending: Vec<AlphaBlending>,
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct OlderTable<T> {
id: Vec<u64>,
#[serde(alias = "instances", alias = "instance")]
#[cfg_attr(feature = "serde", serde(alias = "instances", alias = "instance"))]
element: Vec<T>,
}
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(untagged)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(untagged))]
#[allow(clippy::large_enum_variant)]
enum VectorFormat {
Vector(Vector),
@@ -78,20 +81,29 @@ pub mod migrations {
region_domain: old.region_domain,
upstream_data: old.upstream_graphic_group,
});
*vector_table.iter_mut().next().unwrap().transform = old.transform;
*vector_table.iter_mut().next().unwrap().alpha_blending = old.alpha_blending;
vector_table.set_attribute("transform", 0, old.transform);
vector_table.set_attribute("alpha_blending", 0, old.alpha_blending);
vector_table
}
VectorFormat::OlderVectorTable(older_table) => older_table.element.into_iter().map(|element| TableRow { element, ..Default::default() }).collect(),
VectorFormat::OlderVectorTable(older_table) => older_table
.element
.into_iter()
.map(|element| {
TableRow::new_from_element(element)
.with_attribute("transform", DAffine2::IDENTITY)
.with_attribute("alpha_blending", AlphaBlending::default())
.with_attribute("source_node_id", None::<core_types::uuid::NodeId>)
})
.collect(),
VectorFormat::OldVectorTable(old_table) => old_table
.element
.into_iter()
.zip(old_table.transform.into_iter().zip(old_table.alpha_blending))
.map(|(element, (transform, alpha_blending))| TableRow {
element,
transform,
alpha_blending,
source_node_id: None,
.map(|(element, (transform, alpha_blending))| {
TableRow::new_from_element(element)
.with_attribute("transform", transform)
.with_attribute("alpha_blending", alpha_blending)
.with_attribute("source_node_id", None::<core_types::uuid::NodeId>)
})
.collect(),
VectorFormat::VectorTable(vector_table) => vector_table,

View File

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

View File

@@ -39,15 +39,16 @@ mod base64_serde {
}
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Clone, Eq, Default, serde::Serialize, serde::Deserialize)]
#[derive(Clone, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Image<P: Pixel> {
pub width: u32,
pub height: u32,
#[serde(serialize_with = "base64_serde::as_base64", deserialize_with = "base64_serde::from_base64")]
#[cfg_attr(feature = "serde", serde(serialize_with = "base64_serde::as_base64", deserialize_with = "base64_serde::from_base64"))]
pub data: Vec<P>,
/// Optional: Stores a base64 string representation of the image which can be used to speed up the conversion
/// to an svg string. This is used as a cache in order to not have to encode the data on every graph evaluation.
#[serde(skip)]
#[cfg_attr(feature = "serde", serde(skip))]
pub base64_string: Option<String>,
// TODO: Add an `origin` field to store where in the local space the image is anchored.
// TODO: Currently it is always anchored at the top left corner at (0, 0). The bottom right corner of the new origin field would correspond to (1, 1).
@@ -60,7 +61,8 @@ impl<P: Pixel + PartialEq> PartialEq for Image<P> {
}
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Debug, Clone, dyn_any::DynAny, Default, PartialEq, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Clone, dyn_any::DynAny, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct TransformImage(pub DAffine2);
impl core_types::CacheHash for TransformImage {
@@ -238,13 +240,15 @@ pub fn migrate_image_frame<'de, D: serde::Deserializer<'de>>(deserializer: D) ->
}
}
#[derive(Clone, Debug, core_types::CacheHash, PartialEq, DynAny, serde::Serialize, serde::Deserialize)]
#[derive(Clone, Debug, core_types::CacheHash, PartialEq, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum GraphicElement {
GraphicGroup(Table<GraphicElement>),
RasterFrame(RasterFrame),
}
#[derive(Clone, Default, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
#[derive(Clone, Default, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ImageFrame<P: Pixel> {
pub image: Image<P>,
}
@@ -257,7 +261,7 @@ pub fn migrate_image_frame<'de, D: serde::Deserializer<'de>>(deserializer: D) ->
fn from(element: GraphicElement) -> Self {
match element {
GraphicElement::RasterFrame(RasterFrame::ImageFrame(image)) => Self {
image: image.iter().next().unwrap().element.clone(),
image: image.element(0).unwrap().clone(),
},
_ => panic!("Expected Image, found {element:?}"),
}
@@ -272,15 +276,16 @@ pub fn migrate_image_frame<'de, D: serde::Deserializer<'de>>(deserializer: D) ->
type Static = ImageFrame<P::Static>;
}
#[derive(Clone, Default, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
#[derive(Clone, Default, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct OldImageFrame<P: Pixel> {
image: Image<P>,
transform: DAffine2,
alpha_blending: AlphaBlending,
}
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(untagged)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(untagged))]
enum FormatVersions {
Image(Image<Color>),
OldImageFrame(OldImageFrame<Color>),
@@ -293,23 +298,25 @@ pub fn migrate_image_frame<'de, D: serde::Deserializer<'de>>(deserializer: D) ->
RasterTable(Table<Raster<CPU>>),
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct OldTable<T> {
#[serde(alias = "instances", alias = "instance")]
#[cfg_attr(feature = "serde", serde(alias = "instances", alias = "instance"))]
element: Vec<T>,
transform: Vec<DAffine2>,
alpha_blending: Vec<AlphaBlending>,
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct OlderTable<T> {
id: Vec<u64>,
#[serde(alias = "instances", alias = "instance")]
#[cfg_attr(feature = "serde", serde(alias = "instances", alias = "instance"))]
element: Vec<T>,
}
fn from_image_table(table: Table<Image<Color>>) -> Table<Raster<CPU>> {
Table::new_from_element(Raster::new_cpu(table.iter().next().unwrap().element.clone()))
Table::new_from_element(Raster::new_cpu(table.element(0).unwrap().clone()))
}
fn old_table_to_new_table<T>(old_table: OldTable<T>) -> Table<T> {
@@ -317,11 +324,11 @@ pub fn migrate_image_frame<'de, D: serde::Deserializer<'de>>(deserializer: D) ->
.element
.into_iter()
.zip(old_table.transform.into_iter().zip(old_table.alpha_blending))
.map(|(element, (transform, alpha_blending))| TableRow {
element,
transform,
alpha_blending,
source_node_id: None,
.map(|(element, (transform, alpha_blending))| {
TableRow::new_from_element(element)
.with_attribute("transform", transform)
.with_attribute("alpha_blending", alpha_blending)
.with_attribute("source_node_id", None::<core_types::uuid::NodeId>)
})
.collect()
}
@@ -330,33 +337,27 @@ pub fn migrate_image_frame<'de, D: serde::Deserializer<'de>>(deserializer: D) ->
old_table
.element
.into_iter()
.map(|element| TableRow {
element,
transform: DAffine2::IDENTITY,
alpha_blending: AlphaBlending::default(),
source_node_id: None,
.map(|element| {
TableRow::new_from_element(element)
.with_attribute("transform", DAffine2::IDENTITY)
.with_attribute("alpha_blending", AlphaBlending::default())
.with_attribute("source_node_id", None::<core_types::uuid::NodeId>)
})
.collect()
}
fn from_image_frame_table(image_frame: Table<ImageFrame<Color>>) -> Table<Raster<CPU>> {
Table::new_from_element(Raster::new_cpu(
image_frame
.iter()
.next()
.unwrap_or(Table::new_from_element(ImageFrame::default()).iter().next().unwrap())
.element
.image
.clone(),
))
let default = ImageFrame::default();
let element = image_frame.element(0).unwrap_or(&default);
Table::new_from_element(Raster::new_cpu(element.image.clone()))
}
Ok(match FormatVersions::deserialize(deserializer)? {
FormatVersions::Image(image) => Table::new_from_element(Raster::new_cpu(image)),
FormatVersions::OldImageFrame(OldImageFrame { image, transform, alpha_blending }) => {
let mut image_frame_table = Table::new_from_element(Raster::new_cpu(image));
*image_frame_table.iter_mut().next().unwrap().transform = transform;
*image_frame_table.iter_mut().next().unwrap().alpha_blending = alpha_blending;
image_frame_table.set_attribute("transform", 0, transform);
image_frame_table.set_attribute("alpha_blending", 0, alpha_blending);
image_frame_table
}
FormatVersions::OlderImageFrameTable(old_table) => from_image_frame_table(older_table_to_new_table(old_table)),
@@ -391,14 +392,16 @@ pub fn migrate_image_frame_row<'de, D: serde::Deserializer<'de>>(deserializer: D
}
}
#[derive(Clone, Debug, PartialEq, DynAny, serde::Serialize, serde::Deserialize)]
#[derive(Clone, Debug, PartialEq, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum GraphicElement {
/// Equivalent to the SVG <g> tag: https://developer.mozilla.org/en-US/docs/Web/SVG/Element/g
GraphicGroup(Table<GraphicElement>),
RasterFrame(RasterFrame),
}
#[derive(Clone, Default, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
#[derive(Clone, Default, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ImageFrame<P: Pixel> {
pub image: Image<P>,
}
@@ -411,7 +414,7 @@ pub fn migrate_image_frame_row<'de, D: serde::Deserializer<'de>>(deserializer: D
fn from(element: GraphicElement) -> Self {
match element {
GraphicElement::RasterFrame(RasterFrame::ImageFrame(image)) => Self {
image: image.iter().next().unwrap().element.clone(),
image: image.element(0).unwrap().clone(),
},
_ => panic!("Expected Image, found {element:?}"),
}
@@ -426,15 +429,16 @@ pub fn migrate_image_frame_row<'de, D: serde::Deserializer<'de>>(deserializer: D
type Static = ImageFrame<P::Static>;
}
#[derive(Clone, Default, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
#[derive(Clone, Default, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct OldImageFrame<P: Pixel> {
image: Image<P>,
transform: DAffine2,
alpha_blending: AlphaBlending,
}
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(untagged)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(untagged))]
enum FormatVersions {
Image(Image<Color>),
OldImageFrame(OldImageFrame<Color>),
@@ -444,20 +448,18 @@ pub fn migrate_image_frame_row<'de, D: serde::Deserializer<'de>>(deserializer: D
}
Ok(match FormatVersions::deserialize(deserializer)? {
FormatVersions::Image(image) => TableRow {
element: Raster::new_cpu(image),
..Default::default()
},
FormatVersions::OldImageFrame(image_frame_with_transform_and_blending) => TableRow {
element: Raster::new_cpu(image_frame_with_transform_and_blending.image),
transform: image_frame_with_transform_and_blending.transform,
alpha_blending: image_frame_with_transform_and_blending.alpha_blending,
source_node_id: None,
},
FormatVersions::ImageFrameTable(image_frame) => TableRow {
element: Raster::new_cpu(image_frame.iter().next().unwrap().element.image.clone()),
..Default::default()
},
FormatVersions::Image(image) => TableRow::new_from_element(Raster::new_cpu(image))
.with_attribute("transform", DAffine2::IDENTITY)
.with_attribute("alpha_blending", AlphaBlending::default())
.with_attribute("source_node_id", None::<core_types::uuid::NodeId>),
FormatVersions::OldImageFrame(image_frame_with_transform_and_blending) => TableRow::new_from_element(Raster::new_cpu(image_frame_with_transform_and_blending.image))
.with_attribute("transform", image_frame_with_transform_and_blending.transform)
.with_attribute("alpha_blending", image_frame_with_transform_and_blending.alpha_blending)
.with_attribute("source_node_id", None::<core_types::uuid::NodeId>),
FormatVersions::ImageFrameTable(image_frame) => TableRow::new_from_element(Raster::new_cpu(image_frame.element(0).unwrap().image.clone()))
.with_attribute("transform", DAffine2::IDENTITY)
.with_attribute("alpha_blending", AlphaBlending::default())
.with_attribute("source_node_id", None::<core_types::uuid::NodeId>),
FormatVersions::RasterTable(image_frame_table) => image_frame_table.into_iter().next().unwrap_or_default(),
FormatVersions::RasterTableRow(image_table_row) => image_table_row,
})

View File

@@ -6,6 +6,10 @@ description = "SVG rendering for Graphene"
authors = ["Graphite Authors <contact@graphite.art>"]
license = "MIT OR Apache-2.0"
[features]
default = ["serde"]
serde = ["dep:serde", "core-types/serde", "vector-types/serde", "graphic-types/serde"]
[dependencies]
# Local dependencies
dyn-any = { workspace = true }
@@ -14,7 +18,6 @@ graphene-hash = { workspace = true }
# Workspace dependencies
glam = { workspace = true }
serde = { workspace = true }
base64 = { workspace = true }
log = { workspace = true }
num-traits = { workspace = true }
@@ -22,7 +25,7 @@ usvg = { workspace = true }
kurbo = { workspace = true }
vector-types = { workspace = true }
graphic-types = { workspace = true }
# Workspace dependencies
vello = { workspace = true }
# Optional workspace dependencies
serde = { workspace = true, optional = true }

View File

@@ -1,5 +1,6 @@
use crate::render_ext::RenderExt;
use crate::to_peniko::BlendModeExt;
use core_types::AlphaBlending;
use core_types::CacheHash;
use core_types::blending::BlendMode;
use core_types::bounds::{BoundingBox, RenderBoundingBox};
@@ -68,7 +69,8 @@ pub fn checkerboard_brush() -> peniko::Brush {
})
}
#[derive(Clone, Copy, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
enum MaskType {
Clip,
Mask,
@@ -316,7 +318,8 @@ fn draw_raster_outline(scene: &mut Scene, outline_transform: &DAffine2, render_p
// TODO: Click targets can be removed from the render output, since the vector data is available in the vector modify data from Monitor nodes.
// This will require that the transform for child layers into that layer space be calculated, or it could be returned from the RenderOutput instead of click targets.
#[derive(Debug, Default, Clone, PartialEq, DynAny, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Default, Clone, PartialEq, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct RenderMetadata {
pub upstream_footprints: HashMap<NodeId, Footprint>,
pub local_transforms: HashMap<NodeId, DAffine2>,
@@ -408,41 +411,44 @@ impl Render for Graphic {
Graphic::Vector(table) => {
metadata.upstream_footprints.insert(element_id, footprint);
// TODO: Find a way to handle more than the first row
if let Some(row) = table.iter().next() {
metadata.first_element_source_id.insert(element_id, *row.source_node_id);
metadata.local_transforms.insert(element_id, *row.transform);
if !table.is_empty() {
let source_node_id: Option<NodeId> = table.attribute_cloned_or_default("source_node_id", 0);
let transform: DAffine2 = table.attribute_cloned_or_default("transform", 0);
metadata.first_element_source_id.insert(element_id, source_node_id);
metadata.local_transforms.insert(element_id, transform);
}
}
Graphic::RasterCPU(table) => {
metadata.upstream_footprints.insert(element_id, footprint);
// TODO: Find a way to handle more than the first row
if let Some(row) = table.iter().next() {
metadata.local_transforms.insert(element_id, *row.transform);
if !table.is_empty() {
metadata.local_transforms.insert(element_id, table.attribute_cloned_or_default("transform", 0));
}
}
Graphic::RasterGPU(table) => {
metadata.upstream_footprints.insert(element_id, footprint);
// TODO: Find a way to handle more than the first row
if let Some(row) = table.iter().next() {
metadata.local_transforms.insert(element_id, *row.transform);
if !table.is_empty() {
metadata.local_transforms.insert(element_id, table.attribute_cloned_or_default("transform", 0));
}
}
Graphic::Color(table) => {
metadata.upstream_footprints.insert(element_id, footprint);
// TODO: Find a way to handle more than the first row
if let Some(row) = table.iter().next() {
metadata.local_transforms.insert(element_id, *row.transform);
if !table.is_empty() {
metadata.local_transforms.insert(element_id, table.attribute_cloned_or_default("transform", 0));
}
}
Graphic::Gradient(table) => {
metadata.upstream_footprints.insert(element_id, footprint);
// TODO: Find a way to handle more than the first row
if let Some(row) = table.iter().next() {
metadata.local_transforms.insert(element_id, *row.transform);
if !table.is_empty() {
metadata.local_transforms.insert(element_id, table.attribute_cloned_or_default("transform", 0));
}
}
}
@@ -636,65 +642,69 @@ impl Render for Artboard {
impl Render for Table<Artboard> {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
for artboard in self.iter() {
artboard.element.render_svg(render, render_params);
for element in self.iter_element_values() {
element.render_svg(render, render_params);
}
}
fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) {
for row in self.iter() {
row.element.render_to_vello(scene, transform, context, render_params);
for element in self.iter_element_values() {
element.render_to_vello(scene, transform, context, render_params);
}
}
fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, _element_id: Option<NodeId>) {
for row in self.iter() {
row.element.collect_metadata(metadata, footprint, *row.source_node_id);
for index in 0..self.len() {
let source_node_id: Option<NodeId> = self.attribute_cloned_or_default("source_node_id", index);
self.element(index).unwrap().collect_metadata(metadata, footprint, source_node_id);
}
}
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
for row in self.iter() {
row.element.add_upstream_click_targets(click_targets);
for element in self.iter_element_values() {
element.add_upstream_click_targets(click_targets);
}
}
fn contains_artboard(&self) -> bool {
self.iter().count() > 0
!self.is_empty()
}
}
impl Render for Table<Graphic> {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
let mut iter = self.iter().peekable();
let mut mask_state = None;
while let Some(row) = iter.next() {
for index in 0..self.len() {
let transform: DAffine2 = self.attribute_cloned_or_default("transform", index);
let alpha_blending: AlphaBlending = self.attribute_cloned_or_default("alpha_blending", index);
let element = self.element(index).unwrap();
render.parent_tag(
"g",
|attributes| {
let matrix = format_transform_matrix(*row.transform);
let matrix = format_transform_matrix(transform);
if !matrix.is_empty() {
attributes.push("transform", matrix);
}
let opacity = row.alpha_blending.opacity(render_params.for_mask);
let opacity = alpha_blending.opacity(render_params.for_mask);
if opacity < 1. {
attributes.push("opacity", opacity.to_string());
}
if row.alpha_blending.blend_mode != BlendMode::default() {
attributes.push("style", row.alpha_blending.blend_mode.render());
if alpha_blending.blend_mode != BlendMode::default() {
attributes.push("style", alpha_blending.blend_mode.render());
}
let next_clips = iter.peek().is_some_and(|next_row| next_row.element.had_clip_enabled());
let next_clips = index + 1 < self.len() && self.element(index + 1).unwrap().had_clip_enabled();
if next_clips && mask_state.is_none() {
let uuid = generate_uuid();
let mask_type = if row.element.can_reduce_to_clip_path() { MaskType::Clip } else { MaskType::Mask };
let mask_type = if element.can_reduce_to_clip_path() { MaskType::Clip } else { MaskType::Mask };
mask_state = Some((uuid, mask_type));
let mut svg = SvgRender::new();
row.element.render_svg(&mut svg, &render_params.for_clipper());
element.render_svg(&mut svg, &render_params.for_clipper());
write!(&mut attributes.0.svg_defs, r##"{}"##, svg.svg_defs).unwrap();
mask_type.write_to_defs(&mut attributes.0.svg_defs, uuid, svg.svg.to_svg_string());
@@ -710,19 +720,20 @@ impl Render for Table<Graphic> {
}
},
|render| {
row.element.render_svg(render, render_params);
element.render_svg(render, render_params);
},
);
}
}
fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) {
let mut iter = self.iter().peekable();
let mut mask_element_and_transform = None;
while let Some(row) = iter.next() {
let transform = transform * *row.transform;
let alpha_blending = *row.alpha_blending;
for index in 0..self.len() {
let row_transform: DAffine2 = self.attribute_cloned_or_default("transform", index);
let transform = transform * row_transform;
let alpha_blending: AlphaBlending = self.attribute_cloned_or_default("alpha_blending", index);
let element = self.element(index).unwrap();
let mut layer = false;
@@ -732,9 +743,9 @@ impl Render for Table<Graphic> {
};
let mut bounds = RenderBoundingBox::None;
let opacity = row.alpha_blending.opacity(render_params.for_mask);
let opacity = alpha_blending.opacity(render_params.for_mask);
if opacity < 1. || (render_params.render_mode != RenderMode::Outline && alpha_blending.blend_mode != BlendMode::default()) {
bounds = row.element.bounding_box(transform, true);
bounds = element.bounding_box(transform, true);
if let RenderBoundingBox::Rectangle(bounds) = bounds {
scene.push_layer(
@@ -748,17 +759,17 @@ impl Render for Table<Graphic> {
}
}
let next_clips = iter.peek().is_some_and(|next_row| next_row.element.had_clip_enabled());
let next_clips = index + 1 < self.len() && self.element(index + 1).unwrap().had_clip_enabled();
if next_clips && mask_element_and_transform.is_none() {
mask_element_and_transform = Some((row.element, transform));
mask_element_and_transform = Some((element, transform));
row.element.render_to_vello(scene, transform, context, render_params);
element.render_to_vello(scene, transform, context, render_params);
} else if let Some((mask_element, transform_mask)) = mask_element_and_transform {
if !next_clips {
mask_element_and_transform = None;
}
if !layer {
bounds = row.element.bounding_box(transform, true);
bounds = element.bounding_box(transform, true);
}
if let RenderBoundingBox::Rectangle(bounds) = bounds {
@@ -775,14 +786,14 @@ impl Render for Table<Graphic> {
);
}
row.element.render_to_vello(scene, transform, context, render_params);
element.render_to_vello(scene, transform, context, render_params);
if matches!(bounds, RenderBoundingBox::Rectangle(_)) {
scene.pop_layer();
scene.pop_layer();
}
} else {
row.element.render_to_vello(scene, transform, context, render_params);
element.render_to_vello(scene, transform, context, render_params);
}
if layer {
@@ -792,27 +803,33 @@ impl Render for Table<Graphic> {
}
fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option<NodeId>) {
for row in self.iter() {
let mut footprint = footprint;
footprint.transform *= *row.transform;
for index in 0..self.len() {
let row_transform: DAffine2 = self.attribute_cloned_or_default("transform", index);
let source_node_id: Option<NodeId> = self.attribute_cloned_or_default("source_node_id", index);
let element = self.element(index).unwrap();
if let Some(element_id) = row.source_node_id {
row.element.collect_metadata(metadata, footprint, Some(*element_id));
let mut footprint = footprint;
footprint.transform *= row_transform;
if let Some(element_id) = source_node_id {
element.collect_metadata(metadata, footprint, Some(element_id));
} else {
// Recurse through anonymous wrapper rows to reach nested content with source_node_ids
row.element.collect_metadata(metadata, footprint, None);
element.collect_metadata(metadata, footprint, None);
}
}
if let Some(element_id) = element_id {
let mut all_upstream_click_targets = Vec::new();
for row in self.iter() {
for index in 0..self.len() {
let row_transform: DAffine2 = self.attribute_cloned_or_default("transform", index);
let element = self.element(index).unwrap();
let mut new_click_targets = Vec::new();
row.element.add_upstream_click_targets(&mut new_click_targets);
element.add_upstream_click_targets(&mut new_click_targets);
for click_target in new_click_targets.iter_mut() {
click_target.apply_transform(*row.transform)
click_target.apply_transform(row_transform)
}
all_upstream_click_targets.extend(new_click_targets);
@@ -823,13 +840,15 @@ impl Render for Table<Graphic> {
}
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
for row in self.iter() {
for index in 0..self.len() {
let row_transform: DAffine2 = self.attribute_cloned_or_default("transform", index);
let element = self.element(index).unwrap();
let mut new_click_targets = Vec::new();
row.element.add_upstream_click_targets(&mut new_click_targets);
element.add_upstream_click_targets(&mut new_click_targets);
for click_target in new_click_targets.iter_mut() {
click_target.apply_transform(*row.transform)
click_target.apply_transform(row_transform)
}
click_targets.extend(new_click_targets);
@@ -837,25 +856,28 @@ impl Render for Table<Graphic> {
}
fn contains_artboard(&self) -> bool {
self.iter().any(|row| row.element.contains_artboard())
self.iter_element_values().any(|element| element.contains_artboard())
}
fn new_ids_from_hash(&mut self, _reference: Option<NodeId>) {
for row in self.iter_mut() {
row.element.new_ids_from_hash(*row.source_node_id);
let (elements, source_node_ids) = self.element_and_attribute_slices_mut::<Option<NodeId>>("source_node_id");
for (element, source_node_id) in elements.iter_mut().zip(source_node_ids.iter()) {
element.new_ids_from_hash(*source_node_id);
}
}
}
impl Render for Table<Vector> {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
for row in self.iter() {
let multiplied_transform = *row.transform;
let vector = &row.element;
for index in 0..self.len() {
let Some(vector) = self.element(index) else { continue };
let multiplied_transform: DAffine2 = self.attribute_cloned_or_default("transform", index);
let alpha_blending: AlphaBlending = self.attribute_cloned_or_default("alpha_blending", index);
// Only consider strokes with non-zero weight, since default strokes with zero weight would prevent assigning the correct stroke transform
let has_real_stroke = vector.style.stroke().filter(|stroke| stroke.weight() > 0.);
let set_stroke_transform = has_real_stroke.map(|stroke| stroke.transform).filter(|transform| transform.matrix2.determinant() != 0.);
let applied_stroke_transform = set_stroke_transform.unwrap_or(*row.transform);
let applied_stroke_transform = set_stroke_transform.unwrap_or(multiplied_transform);
let applied_stroke_transform = render_params.alignment_parent_transform.unwrap_or(applied_stroke_transform);
let element_transform = set_stroke_transform.map(|stroke_transform| multiplied_transform * stroke_transform.inverse());
let element_transform = element_transform.unwrap_or(DAffine2::IDENTITY);
@@ -867,7 +889,7 @@ impl Render for Table<Vector> {
let mut path = String::new();
for mut bezpath in row.element.stroke_bezpath_iter() {
for mut bezpath in vector.stroke_bezpath_iter() {
bezpath.apply_affine(Affine::new(applied_stroke_transform.to_cols_array()));
path.push_str(bezpath.to_svg().as_str());
}
@@ -880,7 +902,7 @@ impl Render for Table<Vector> {
let path_is_closed = vector.stroke_bezier_paths().all(|path| path.closed());
let can_draw_aligned_stroke = path_is_closed && vector.style.stroke().is_some_and(|stroke| stroke.has_renderable_stroke() && stroke.align.is_not_centered());
let can_use_paint_order = !(row.element.style.fill().is_none() || !row.element.style.fill().is_opaque() || mask_type == MaskType::Clip);
let can_use_paint_order = !(vector.style.fill().is_none() || !vector.style.fill().is_opaque() || mask_type == MaskType::Clip);
let needs_separate_alignment_fill = can_draw_aligned_stroke && !can_use_paint_order;
let wants_stroke_below = vector.style.stroke().map(|s| s.paint_order) == Some(PaintOrder::StrokeBelow);
@@ -892,7 +914,7 @@ impl Render for Table<Vector> {
if !matrix.is_empty() {
attributes.push("transform", matrix);
}
let mut style = row.element.style.clone();
let mut style = vector.style.clone();
style.clear_stroke();
let fill_and_stroke = style.render(
&mut attributes.0.svg_defs,
@@ -909,16 +931,16 @@ impl Render for Table<Vector> {
let push_id = needs_separate_alignment_fill.then_some({
let id = format!("alignment-{}", generate_uuid());
let mut element = row.element.clone();
element.style.clear_stroke();
element.style.set_fill(Fill::solid(Color::BLACK));
let mut cloned_vector = vector.clone();
cloned_vector.style.clear_stroke();
cloned_vector.style.set_fill(Fill::solid(Color::BLACK));
let vector_row = Table::new_from_row(TableRow {
element,
alpha_blending: *row.alpha_blending,
transform: *row.transform,
source_node_id: None,
});
let vector_row = Table::new_from_row(
TableRow::new_from_element(cloned_vector)
.with_attribute("transform", multiplied_transform)
.with_attribute("alpha_blending", alpha_blending)
.with_attribute("source_node_id", None::<NodeId>),
);
(id, mask_type, vector_row)
});
@@ -935,7 +957,7 @@ impl Render for Table<Vector> {
if !matrix.is_empty() {
attributes.push("transform", matrix);
}
let mut style = row.element.style.clone();
let mut style = vector.style.clone();
style.clear_stroke();
let fill_only = style.render(
&mut attributes.0.svg_defs,
@@ -961,7 +983,7 @@ impl Render for Table<Vector> {
if let Some((ref id, mask_type, ref vector_row)) = push_id {
let mut svg = SvgRender::new();
vector_row.render_svg(&mut svg, &render_params.for_alignment(applied_stroke_transform));
let stroke = row.element.style.stroke().unwrap();
let stroke = vector.style.stroke().unwrap();
let weight = stroke.effective_width() * max_scale(applied_stroke_transform);
let quad = Quad::from_box(transformed_bounds).inflate(weight);
let (x, y) = quad.top_left().into();
@@ -986,7 +1008,7 @@ impl Render for Table<Vector> {
render_params.aligned_strokes = can_draw_aligned_stroke;
render_params.override_paint_order = can_draw_aligned_stroke && can_use_paint_order;
let mut style = row.element.style.clone();
let mut style = vector.style.clone();
if needs_separate_alignment_fill || use_face_fill {
style.clear_fill();
}
@@ -1003,13 +1025,13 @@ impl Render for Table<Vector> {
attributes.push("fill-rule", "evenodd");
}
let opacity = row.alpha_blending.opacity(render_params.for_mask);
let opacity = alpha_blending.opacity(render_params.for_mask);
if opacity < 1. {
attributes.push("opacity", opacity.to_string());
}
if row.alpha_blending.blend_mode != BlendMode::default() {
attributes.push("style", row.alpha_blending.blend_mode.render());
if alpha_blending.blend_mode != BlendMode::default() {
attributes.push("style", alpha_blending.blend_mode.render());
}
});
@@ -1021,7 +1043,7 @@ impl Render for Table<Vector> {
if !matrix.is_empty() {
attributes.push("transform", matrix);
}
let mut style = row.element.style.clone();
let mut style = vector.style.clone();
style.clear_stroke();
let fill_and_stroke = style.render(
&mut attributes.0.svg_defs,
@@ -1040,11 +1062,14 @@ impl Render for Table<Vector> {
fn render_to_vello(&self, scene: &mut Scene, parent_transform: DAffine2, _context: &mut RenderContext, render_params: &RenderParams) {
use graphic_types::vector_types::vector::style::{GradientType, StrokeCap, StrokeJoin};
for row in self.iter() {
for index in 0..self.len() {
use graphic_types::vector_types::vector;
let multiplied_transform = parent_transform * *row.transform;
let has_real_stroke = row.element.style.stroke().filter(|stroke| stroke.weight() > 0.);
let Some(element) = self.element(index) else { continue };
let row_transform: DAffine2 = self.attribute_cloned_or_default("transform", index);
let alpha_blending: AlphaBlending = self.attribute_cloned_or_default("alpha_blending", index);
let multiplied_transform = parent_transform * row_transform;
let has_real_stroke = element.style.stroke().filter(|stroke| stroke.weight() > 0.);
let set_stroke_transform = has_real_stroke.map(|stroke| stroke.transform).filter(|transform| transform.matrix2.determinant() != 0.);
let mut applied_stroke_transform = set_stroke_transform.unwrap_or(multiplied_transform);
let mut element_transform = set_stroke_transform
@@ -1058,11 +1083,11 @@ impl Render for Table<Vector> {
multiplied_transform
};
}
let layer_bounds = row.element.bounding_box().unwrap_or_default();
let layer_bounds = element.bounding_box().unwrap_or_default();
let to_point = |p: DVec2| kurbo::Point::new(p.x, p.y);
let mut path = kurbo::BezPath::new();
for mut bezpath in row.element.stroke_bezpath_iter() {
for mut bezpath in element.stroke_bezpath_iter() {
bezpath.apply_affine(Affine::new(applied_stroke_transform.to_cols_array()));
for element in bezpath {
path.push(element);
@@ -1072,14 +1097,14 @@ impl Render for Table<Vector> {
// If we're using opacity or a blend mode, we need to push a layer
let blend_mode = match render_params.render_mode {
RenderMode::Outline => peniko::Mix::Normal,
_ => row.alpha_blending.blend_mode.to_peniko(),
_ => alpha_blending.blend_mode.to_peniko(),
};
let mut layer = false;
let opacity = row.alpha_blending.opacity(render_params.for_mask);
if opacity < 1. || row.alpha_blending.blend_mode != BlendMode::default() {
let opacity = alpha_blending.opacity(render_params.for_mask);
if opacity < 1. || alpha_blending.blend_mode != BlendMode::default() {
layer = true;
let weight = row.element.style.stroke().as_ref().map_or(0., Stroke::effective_width);
let weight = element.style.stroke().as_ref().map_or(0., Stroke::effective_width);
let quad = Quad::from_box(layer_bounds).inflate(weight * max_scale(applied_stroke_transform));
let layer_bounds = quad.bounding_box();
scene.push_layer(
@@ -1092,13 +1117,13 @@ impl Render for Table<Vector> {
}
let can_draw_aligned_stroke =
row.element.style.stroke().is_some_and(|stroke| stroke.has_renderable_stroke() && stroke.align.is_not_centered()) && row.element.stroke_bezier_paths().all(|path| path.closed());
element.style.stroke().is_some_and(|stroke| stroke.has_renderable_stroke() && stroke.align.is_not_centered()) && element.stroke_bezier_paths().all(|path| path.closed());
let use_layer = can_draw_aligned_stroke;
let wants_stroke_below = row.element.style.stroke().is_some_and(|s| s.paint_order == vector::style::PaintOrder::StrokeBelow);
let wants_stroke_below = element.style.stroke().is_some_and(|s| s.paint_order == vector::style::PaintOrder::StrokeBelow);
// Closures to avoid duplicated fill/stroke drawing logic
let do_fill_path = |scene: &mut Scene, path: &kurbo::BezPath, fill_rule: peniko::Fill| match row.element.style.fill() {
let do_fill_path = |scene: &mut Scene, path: &kurbo::BezPath, fill_rule: peniko::Fill| match element.style.fill() {
Fill::Solid(color) => {
let fill = peniko::Brush::Solid(peniko::Color::new([color.r(), color.g(), color.b(), color.a()]));
scene.fill(fill_rule, kurbo::Affine::new(element_transform.to_cols_array()), &fill, None, path);
@@ -1112,7 +1137,7 @@ impl Render for Table<Vector> {
});
}
let bounds = row.element.nonzero_bounding_box();
let bounds = element.nonzero_bounding_box();
let bound_transform = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]);
let inverse_parent_transform = if parent_transform.matrix2.determinant() != 0. {
@@ -1164,10 +1189,10 @@ impl Render for Table<Vector> {
};
// Branching vectors without regions (e.g. mesh grids) need face-by-face fill rendering.
let use_face_fill = row.element.use_face_fill();
let use_face_fill = element.use_face_fill();
let do_fill = |scene: &mut Scene| {
if use_face_fill {
for mut face_path in row.element.construct_faces().filter(|face| face.area() >= 0.) {
for mut face_path in element.construct_faces().filter(|face| face.area() >= 0.) {
face_path.apply_affine(Affine::new(applied_stroke_transform.to_cols_array()));
let mut kurbo_path = kurbo::BezPath::new();
for element in face_path {
@@ -1175,7 +1200,7 @@ impl Render for Table<Vector> {
}
do_fill_path(scene, &kurbo_path, peniko::Fill::NonZero);
}
} else if row.element.is_branching() {
} else if element.is_branching() {
do_fill_path(scene, &path, peniko::Fill::EvenOdd);
} else {
do_fill_path(scene, &path, peniko::Fill::NonZero);
@@ -1183,7 +1208,7 @@ impl Render for Table<Vector> {
};
let do_stroke = |scene: &mut Scene, width_scale: f64| {
if let Some(stroke) = row.element.style.stroke() {
if let Some(stroke) = element.style.stroke() {
let color = match stroke.color {
Some(color) => peniko::Color::new([color.r(), color.g(), color.b(), color.a()]),
None => peniko::Color::TRANSPARENT,
@@ -1224,24 +1249,24 @@ impl Render for Table<Vector> {
}
_ => {
if use_layer {
let mut element = row.element.clone();
element.style.clear_stroke();
element.style.set_fill(Fill::solid(Color::BLACK));
let mut cloned_element = element.clone();
cloned_element.style.clear_stroke();
cloned_element.style.set_fill(Fill::solid(Color::BLACK));
let vector_table = Table::new_from_row(TableRow {
element,
alpha_blending: *row.alpha_blending,
transform: *row.transform,
source_node_id: None,
});
let vector_table = Table::new_from_row(
TableRow::new_from_element(cloned_element)
.with_attribute("transform", row_transform)
.with_attribute("alpha_blending", alpha_blending)
.with_attribute("source_node_id", None::<NodeId>),
);
let bounds = row.element.bounding_box_with_transform(multiplied_transform).unwrap_or(layer_bounds);
let weight = row.element.style.stroke().as_ref().map_or(0., Stroke::effective_width);
let bounds = element.bounding_box_with_transform(multiplied_transform).unwrap_or(layer_bounds);
let weight = element.style.stroke().as_ref().map_or(0., Stroke::effective_width);
let quad = Quad::from_box(bounds).inflate(weight * max_scale(applied_stroke_transform));
let bounds = quad.bounding_box();
let rect = kurbo::Rect::new(bounds[0].x, bounds[0].y, bounds[1].x, bounds[1].y);
let compose = if row.element.style.stroke().is_some_and(|x| x.align == StrokeAlign::Outside) {
let compose = if element.style.stroke().is_some_and(|x| x.align == StrokeAlign::Outside) {
peniko::Compose::SrcOut
} else {
peniko::Compose::SrcIn
@@ -1278,7 +1303,7 @@ impl Render for Table<Vector> {
Stroke,
}
let order = match row.element.style.stroke().is_some_and(|stroke| !stroke.paint_order.is_default()) {
let order = match element.style.stroke().is_some_and(|stroke| !stroke.paint_order.is_default()) {
true => [Op::Stroke, Op::Fill],
false => [Op::Fill, Op::Stroke], // Default
};
@@ -1301,11 +1326,12 @@ impl Render for Table<Vector> {
}
fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, caller_element_id: Option<NodeId>) {
for row in self.iter() {
let transform = *row.transform;
let vector = row.element;
for index in 0..self.len() {
let Some(vector) = self.element(index) else { continue };
let transform: DAffine2 = self.attribute_cloned_or_default("transform", index);
let source_node_id: Option<NodeId> = self.attribute_cloned_or_default("source_node_id", index);
if let Some(element_id) = caller_element_id.or(*row.source_node_id) {
if let Some(element_id) = caller_element_id.or(source_node_id) {
// When recovering element_id from the row's source_node_id (because the caller
// passed None), also store the transform metadata that Graphic::collect_metadata
// normally provides but skipped due to the None element_id.
@@ -1356,32 +1382,35 @@ impl Render for Table<Vector> {
}
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
for row in self.iter() {
let stroke_width = row.element.style.stroke().as_ref().map_or(0., Stroke::effective_width);
let filled = row.element.style.fill() != &Fill::None;
for index in 0..self.len() {
let Some(vector) = self.element(index) else { continue };
let transform: DAffine2 = self.attribute_cloned_or_default("transform", index);
let stroke_width = vector.style.stroke().as_ref().map_or(0., Stroke::effective_width);
let filled = vector.style.fill() != &Fill::None;
let fill = |mut subpath: Subpath<_>| {
if filled {
subpath.set_closed(true);
}
subpath
};
click_targets.extend(row.element.stroke_bezier_paths().map(fill).map(|subpath| {
click_targets.extend(vector.stroke_bezier_paths().map(fill).map(|subpath| {
let mut click_target = ClickTarget::new_with_subpath(subpath, stroke_width);
click_target.apply_transform(*row.transform);
click_target.apply_transform(transform);
click_target
}));
// For free-floating anchors, we need to add a click target for each
let single_anchors_targets = row.element.point_domain.ids().iter().filter_map(|&point_id| {
if row.element.any_connected(point_id) {
let single_anchors_targets = vector.point_domain.ids().iter().filter_map(|&point_id| {
if vector.any_connected(point_id) {
return None;
}
let anchor = row.element.point_domain.position_from_id(point_id).unwrap_or_default();
let anchor = vector.point_domain.position_from_id(point_id).unwrap_or_default();
let point = FreePoint::new(point_id, anchor);
let mut click_target = ClickTarget::new_with_free_point(point);
click_target.apply_transform(*row.transform);
click_target.apply_transform(transform);
Some(click_target)
});
click_targets.extend(single_anchors_targets);
@@ -1389,18 +1418,19 @@ impl Render for Table<Vector> {
}
fn new_ids_from_hash(&mut self, reference: Option<NodeId>) {
for row in self.iter_mut() {
row.element.vector_new_ids_from_hash(reference.map(|id| id.0).unwrap_or_default());
for vector in self.iter_element_values_mut() {
vector.vector_new_ids_from_hash(reference.map(|id| id.0).unwrap_or_default());
}
}
}
impl Render for Table<Raster<CPU>> {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
for row in self.iter() {
let image = row.element;
for index in 0..self.len() {
let Some(image) = self.element(index) else { continue };
let transform = *row.transform;
let transform: DAffine2 = self.attribute_cloned_or_default("transform", index);
let alpha_blending: AlphaBlending = self.attribute_cloned_or_default("alpha_blending", index);
if image.data.is_empty() {
continue;
@@ -1427,13 +1457,13 @@ impl Render for Table<Raster<CPU>> {
attributes.push("width", size.x.to_string());
attributes.push("height", size.y.to_string());
let opacity = row.alpha_blending.opacity(render_params.for_mask);
let opacity = alpha_blending.opacity(render_params.for_mask);
if opacity < 1. {
attributes.push("opacity", opacity.to_string());
}
if row.alpha_blending.blend_mode != BlendMode::default() {
attributes.push("style", row.alpha_blending.blend_mode.render());
if alpha_blending.blend_mode != BlendMode::default() {
attributes.push("style", alpha_blending.blend_mode.render());
}
},
|render| {
@@ -1467,12 +1497,12 @@ impl Render for Table<Raster<CPU>> {
attributes.push("transform", matrix);
}
let opacity = row.alpha_blending.opacity(render_params.for_mask);
let opacity = alpha_blending.opacity(render_params.for_mask);
if opacity < 1. {
attributes.push("opacity", opacity.to_string());
}
if row.alpha_blending.blend_mode != BlendMode::default() {
attributes.push("style", row.alpha_blending.blend_mode.render());
if alpha_blending.blend_mode != BlendMode::default() {
attributes.push("style", alpha_blending.blend_mode.render());
}
});
}
@@ -1480,13 +1510,13 @@ impl Render for Table<Raster<CPU>> {
}
fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, _: &mut RenderContext, render_params: &RenderParams) {
for row in self.iter() {
let image = &row.element;
for index in 0..self.len() {
let Some(image) = self.element(index) else { continue };
if image.data.is_empty() {
continue;
}
let alpha_blending = *row.alpha_blending;
let alpha_blending: AlphaBlending = self.attribute_cloned_or_default("alpha_blending", index);
let blend_mode = alpha_blending.blend_mode.to_peniko();
let opacity = alpha_blending.opacity(render_params.for_mask);
@@ -1501,8 +1531,10 @@ impl Render for Table<Raster<CPU>> {
layer = true;
}
let transform_attribute: DAffine2 = self.attribute_cloned_or_default("transform", index);
if let RenderMode::Outline = render_params.render_mode {
let outline_transform = transform * *row.transform;
let outline_transform: DAffine2 = transform * transform_attribute;
draw_raster_outline(scene, &outline_transform, render_params);
if layer {
@@ -1512,7 +1544,7 @@ impl Render for Table<Raster<CPU>> {
continue;
}
let image_transform = transform * *row.transform * DAffine2::from_scale(1. / DVec2::new(image.width as f64, image.height as f64));
let image_transform = transform * transform_attribute * DAffine2::from_scale(1. / DVec2::new(image.width as f64, image.height as f64));
let image_brush = peniko::ImageBrush::new(peniko::ImageData {
data: image.to_flat_u8().0.into(),
@@ -1538,8 +1570,8 @@ impl Render for Table<Raster<CPU>> {
metadata.click_targets.insert(element_id, vec![ClickTarget::new_with_subpath(subpath, 0.).into()]);
metadata.upstream_footprints.insert(element_id, footprint);
// TODO: Find a way to handle more than one row of the raster table
if let Some(raster) = self.iter().next() {
metadata.local_transforms.insert(element_id, *raster.transform);
if !self.is_empty() {
metadata.local_transforms.insert(element_id, self.attribute_cloned_or_default("transform", 0));
}
}
@@ -1557,8 +1589,9 @@ impl Render for Table<Raster<GPU>> {
}
fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) {
for row in self.iter() {
let alpha_blending = *row.alpha_blending;
for index in 0..self.len() {
let Some(raster) = self.element(index) else { continue };
let alpha_blending: AlphaBlending = self.attribute_cloned_or_default("alpha_blending", index);
let blend_mode = match render_params.render_mode {
RenderMode::Outline => peniko::Mix::Normal,
_ => alpha_blending.blend_mode.to_peniko(),
@@ -1575,8 +1608,10 @@ impl Render for Table<Raster<GPU>> {
layer = true;
}
let transform_attribute: DAffine2 = self.attribute_cloned_or_default("transform", index);
if let RenderMode::Outline = render_params.render_mode {
let outline_transform = transform * *row.transform;
let outline_transform = transform * transform_attribute;
draw_raster_outline(scene, &outline_transform, render_params);
if layer {
@@ -1586,8 +1621,8 @@ impl Render for Table<Raster<GPU>> {
continue;
}
let width = row.element.data().width();
let height = row.element.data().height();
let width = raster.data().width();
let height = raster.data().height();
let image = peniko::ImageBrush::new(peniko::ImageData {
data: peniko::Blob::new(LAZY_ARC_VEC_ZERO_U8.deref().clone()),
format: peniko::ImageFormat::Rgba8,
@@ -1596,9 +1631,9 @@ impl Render for Table<Raster<GPU>> {
alpha_type: peniko::ImageAlphaType::Alpha,
})
.with_extend(peniko::Extend::Repeat);
let image_transform = transform * *row.transform * DAffine2::from_scale(1. / DVec2::new(width as f64, height as f64));
let image_transform = transform * transform_attribute * DAffine2::from_scale(1. / DVec2::new(width as f64, height as f64));
scene.draw_image(&image, kurbo::Affine::new(image_transform.to_cols_array()));
context.resource_overrides.push((image, row.element.data().clone()));
context.resource_overrides.push((image, raster.data().clone()));
if layer {
scene.pop_layer()
@@ -1613,8 +1648,8 @@ impl Render for Table<Raster<GPU>> {
metadata.click_targets.insert(element_id, vec![ClickTarget::new_with_subpath(subpath, 0.).into()]);
metadata.upstream_footprints.insert(element_id, footprint);
// TODO: Find a way to handle more than one row of the raster table
if let Some(raster) = self.iter().next() {
metadata.local_transforms.insert(element_id, *raster.transform);
if !self.is_empty() {
metadata.local_transforms.insert(element_id, self.attribute_cloned_or_default("transform", 0));
}
}
@@ -1632,25 +1667,24 @@ impl Render for Table<Raster<GPU>> {
// later replace with the current viewport transform before each render.
impl Render for Table<Color> {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
for row in self.iter() {
for (color, alpha_blending) in self.iter_element_values().zip(self.iter_attribute_values_or_default::<AlphaBlending>("alpha_blending")) {
render.leaf_tag("polyline", |attributes| {
// Chrome doesn't like drawing centered rectangles bigger than ~20 million so we draw a polyline quad instead
let max = u64::MAX;
attributes.push("points", format!("{max},{max} -{max},{max} -{max},-{max} {max},-{max}"));
let color = row.element;
attributes.push("fill", format!("#{}", color.to_rgb_hex_srgb_from_gamma()));
if color.a() < 1. {
attributes.push("fill-opacity", ((color.a() * 1000.).round() / 1000.).to_string());
}
let opacity = row.alpha_blending.opacity(render_params.for_mask);
let opacity = alpha_blending.opacity(render_params.for_mask);
if opacity < 1. {
attributes.push("opacity", opacity.to_string());
}
if row.alpha_blending.blend_mode != BlendMode::default() {
attributes.push("style", row.alpha_blending.blend_mode.render());
if alpha_blending.blend_mode != BlendMode::default() {
attributes.push("style", alpha_blending.blend_mode.render());
}
});
}
@@ -1659,12 +1693,10 @@ impl Render for Table<Color> {
fn render_to_vello(&self, scene: &mut Scene, _parent_transform: DAffine2, _context: &mut RenderContext, render_params: &RenderParams) {
use vello::peniko;
for row in self.iter() {
let alpha_blending = *row.alpha_blending;
for (color, alpha_blending) in self.iter_element_values().zip(self.iter_attribute_values_or_default::<AlphaBlending>("alpha_blending")) {
let blend_mode = alpha_blending.blend_mode.to_peniko();
let opacity = alpha_blending.opacity(render_params.for_mask);
let color = row.element;
let vello_color = peniko::Color::new([color.r(), color.g(), color.b(), color.a()]);
let rect = kurbo::Rect::from_origin_size(kurbo::Point::ZERO, kurbo::Size::new(1., 1.));
@@ -1688,14 +1720,17 @@ impl Render for Table<Color> {
impl Render for Table<GradientStops> {
// TODO: Fix infinite gradient rendering
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
for row in self.iter() {
for index in 0..self.len() {
let Some(gradient) = self.element(index) else { continue };
let transform: DAffine2 = self.attribute_cloned_or_default("transform", index);
let alpha_blending: AlphaBlending = self.attribute_cloned_or_default("alpha_blending", index);
render.leaf_tag("rect", |attributes| {
// Chrome doesn't like drawing centered rectangles bigger than ~20 million so we draw a polyline quad instead
let max = u64::MAX;
attributes.push("points", format!("{max},{max} -{max},{max} -{max},-{max} {max},-{max}"));
let mut stop_string = String::new();
for (position, color, original_midpoint) in row.element.interpolated_samples() {
for (position, color, original_midpoint) in gradient.interpolated_samples() {
let _ = write!(stop_string, r##"<stop offset="{}" stop-color="#{}""##, position, color.to_rgb_hex_srgb_from_gamma());
if color.a() < 1. {
let _ = write!(stop_string, r#" stop-opacity="{}""#, color.a());
@@ -1706,7 +1741,7 @@ impl Render for Table<GradientStops> {
stop_string.push_str(" />");
}
let gradient_transform = render_params.footprint.transform * *row.transform;
let gradient_transform = render_params.footprint.transform * transform;
let gradient_transform_matrix = format_transform_matrix(gradient_transform);
let gradient_transform_attribute = if gradient_transform_matrix.is_empty() {
String::new()
@@ -1739,13 +1774,13 @@ impl Render for Table<GradientStops> {
attributes.push("fill", format!("url('#{gradient_id}')"));
let opacity = row.alpha_blending.opacity(render_params.for_mask);
let opacity = alpha_blending.opacity(render_params.for_mask);
if opacity < 1. {
attributes.push("opacity", opacity.to_string());
}
if row.alpha_blending.blend_mode != BlendMode::default() {
attributes.push("style", row.alpha_blending.blend_mode.render());
if alpha_blending.blend_mode != BlendMode::default() {
attributes.push("style", alpha_blending.blend_mode.render());
}
});
}
@@ -1755,12 +1790,11 @@ impl Render for Table<GradientStops> {
fn render_to_vello(&self, scene: &mut Scene, _parent_transform: DAffine2, _context: &mut RenderContext, render_params: &RenderParams) {
use vello::peniko;
for row in self.iter() {
let alpha_blending = *row.alpha_blending;
for (gradient, alpha_blending) in self.iter_element_values().zip(self.iter_attribute_values_or_default::<AlphaBlending>("alpha_blending")) {
let blend_mode = alpha_blending.blend_mode.to_peniko();
let opacity = alpha_blending.opacity(render_params.for_mask);
let color = row.element.color.first().copied().unwrap_or(Color::MAGENTA);
let color = gradient.color.first().copied().unwrap_or(Color::MAGENTA);
let vello_color = peniko::Color::new([color.r(), color.g(), color.b(), color.a()]);
let rect = kurbo::Rect::from_origin_size(kurbo::Point::ZERO, kurbo::Size::new(1., 1.));

View File

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

View File

@@ -3,7 +3,8 @@ use dyn_any::DynAny;
use glam::{DAffine2, DVec2};
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Hash, graphene_hash::CacheHash, serde::Serialize, serde::Deserialize, DynAny, node_macro::ChoiceType)]
#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Hash, graphene_hash::CacheHash, DynAny, node_macro::ChoiceType)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[widget(Radio)]
pub enum GradientType {
#[default]
@@ -15,7 +16,8 @@ pub enum GradientType {
// TODO: Use linear not gamma colors
/// A list of colors associated with positions (in the range 0 to 1) along a gradient.
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Debug, Clone, PartialEq, graphene_hash::CacheHash, serde::Serialize, DynAny)]
#[derive(Debug, Clone, PartialEq, graphene_hash::CacheHash, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct GradientStops {
/// The position of this stop, a factor from 0-1 along the length of the full gradient.
pub position: Vec<f64>,
@@ -36,7 +38,7 @@ impl<'de> serde::Deserialize<'de> for GradientStops {
}
#[derive(serde::Deserialize)]
#[serde(untagged)]
#[cfg_attr(feature = "serde", serde(untagged))]
enum GradientStopsFormat {
New(NewFormat),
Old(Vec<(f64, Color)>),
@@ -325,7 +327,8 @@ impl GradientStops {
#[repr(C)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Hash, graphene_hash::CacheHash, serde::Serialize, serde::Deserialize, DynAny, node_macro::ChoiceType)]
#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Hash, graphene_hash::CacheHash, DynAny, node_macro::ChoiceType)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[widget(Radio)]
pub enum GradientSpreadMethod {
#[default]
@@ -349,13 +352,14 @@ impl GradientSpreadMethod {
/// Contains the start and end points, along with the colors at varying points along the length.
#[repr(C)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Debug, Clone, PartialEq, graphene_hash::CacheHash, serde::Serialize, serde::Deserialize, DynAny)]
#[derive(Debug, Clone, PartialEq, graphene_hash::CacheHash, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Gradient {
pub stops: GradientStops,
pub gradient_type: GradientType,
pub start: DVec2,
pub end: DVec2,
#[serde(default)]
#[cfg_attr(feature = "serde", serde(default))]
pub spread_method: GradientSpreadMethod,
}
@@ -468,7 +472,7 @@ pub fn migrate_gradient_stops<'de, D: serde::Deserializer<'de>>(deserializer: D)
use serde::Deserialize;
#[derive(serde::Deserialize)]
#[serde(untagged)]
#[cfg_attr(feature = "serde", serde(untagged))]
enum GradientStopsFormat {
GradientStops(GradientStops),
GradientTable(Table<GradientStops>),

View File

@@ -13,7 +13,8 @@ use kurbo::{Affine, BezPath, ParamCurve, PathSeg, Shape};
type BoundingBox = Option<[DVec2; 2]>;
#[derive(Copy, Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
#[derive(Copy, Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct FreePoint {
pub id: PointId,
pub position: DVec2,
@@ -29,7 +30,8 @@ impl FreePoint {
}
}
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum ClickTargetType {
Subpath(Subpath<PointId>),
FreePoint(FreePoint),
@@ -115,12 +117,13 @@ impl BoundingBoxCache {
}
/// Represents a clickable target for the layer
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ClickTarget {
target_type: ClickTargetType,
stroke_width: f64,
bounding_box: BoundingBox,
#[serde(skip)]
#[cfg_attr(feature = "serde", serde(skip))]
bounding_box_cache: Arc<RwLock<BoundingBoxCache>>,
}

View File

@@ -8,7 +8,8 @@ use kurbo::{BezPath, CubicBez, Line, ParamCurve, ParamCurveDeriv, PathSeg, Point
use std::ops::Sub;
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, node_macro::ChoiceType)]
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[widget(Radio)]
pub enum BooleanOperation {
#[default]
@@ -26,7 +27,8 @@ pub enum BooleanOperation {
/// Represents different geometric interpretations of calculating the centroid (center of mass).
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, node_macro::ChoiceType)]
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[widget(Radio)]
pub enum CentroidType {
/// The center of mass for the area of a solid shape's interior, as if made out of an infinitely flat material.
@@ -38,7 +40,8 @@ pub enum CentroidType {
#[repr(C)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, node_macro::ChoiceType)]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[widget(Radio)]
pub enum RowsOrColumns {
#[default]
@@ -85,7 +88,8 @@ impl AsI64 for f64 {
}
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, node_macro::ChoiceType)]
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[widget(Radio)]
pub enum GridType {
#[default]
@@ -95,7 +99,8 @@ pub enum GridType {
#[repr(C)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, node_macro::ChoiceType)]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[widget(Radio)]
pub enum ArcType {
#[default]
@@ -106,7 +111,8 @@ pub enum ArcType {
#[repr(C)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, node_macro::ChoiceType)]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[widget(Radio)]
pub enum MergeByDistanceAlgorithm {
#[default]
@@ -116,7 +122,8 @@ pub enum MergeByDistanceAlgorithm {
#[repr(C)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, node_macro::ChoiceType)]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[widget(Radio)]
pub enum ExtrudeJoiningAlgorithm {
All,
@@ -127,7 +134,8 @@ pub enum ExtrudeJoiningAlgorithm {
#[repr(C)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, node_macro::ChoiceType)]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[widget(Radio)]
pub enum PointSpacingType {
#[default]
@@ -368,7 +376,8 @@ impl Tangent for kurbo::PathSeg {
}
/// A selectable part of a curve, either an anchor (start or end of a bézier) or a handle (doesn't necessarily go through the bézier but influences curvature).
#[derive(Clone, Copy, PartialEq, Eq, Hash, graphene_hash::CacheHash, Debug, DynAny, serde::Serialize, serde::Deserialize)]
#[derive(Clone, Copy, PartialEq, Eq, Hash, graphene_hash::CacheHash, Debug, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum ManipulatorPointId {
/// A control anchor - the start or end point of a bézier.
Anchor(PointId),
@@ -479,7 +488,8 @@ impl ManipulatorPointId {
}
/// The type of handle found on a bézier curve.
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, graphene_hash::CacheHash, Debug, DynAny, serde::Serialize, serde::Deserialize)]
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, graphene_hash::CacheHash, Debug, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum HandleType {
/// The first handle on a cubic bézier or the only handle on a quadratic bézier.
Primary,
@@ -488,7 +498,8 @@ pub enum HandleType {
}
/// Represents a primary or end handle found in a particular segment.
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, graphene_hash::CacheHash, Debug, DynAny, serde::Serialize, serde::Deserialize)]
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, graphene_hash::CacheHash, Debug, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct HandleId {
pub ty: HandleType,
pub segment: SegmentId,
@@ -547,7 +558,8 @@ impl HandleId {
}
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, node_macro::ChoiceType)]
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[widget(Dropdown)]
pub enum SpiralType {
#[default]
@@ -557,7 +569,8 @@ pub enum SpiralType {
/// Controls how the morph/blend progression spends its time along the interpolation path, allowing for constant speed/spacing with respect to different parameters of change.
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, node_macro::ChoiceType)]
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[widget(Dropdown)]
pub enum InterpolationDistribution {
/// All objects occupy an equal portion of the progression range, regardless of their changing distances, angles, sizes, or slants.

View File

@@ -2,7 +2,8 @@ use core_types::math::bbox::AxisAlignedBbox;
use glam::DVec2;
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Clone, Copy, Debug, Default, Hash, graphene_hash::CacheHash, Eq, PartialEq, dyn_any::DynAny, serde::Serialize, serde::Deserialize)]
#[derive(Clone, Copy, Debug, Default, Hash, graphene_hash::CacheHash, Eq, PartialEq, dyn_any::DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum ReferencePoint {
#[default]
None,

View File

@@ -1,6 +1,7 @@
//! Contains stylistic options for SVG elements.
pub use crate::gradient::*;
use core_types::AlphaBlending;
use core_types::Color;
use core_types::color::Alpha;
use core_types::table::Table;
@@ -16,7 +17,8 @@ use std::f64::consts::{PI, TAU};
/// In the future we'll probably also add a pattern fill. This will probably be named "Paint" in the future.
#[repr(C)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Default, Debug, Clone, PartialEq, graphene_hash::CacheHash, serde::Serialize, serde::Deserialize, DynAny)]
#[derive(Default, Debug, Clone, PartialEq, graphene_hash::CacheHash, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Fill {
#[default]
None,
@@ -133,8 +135,8 @@ impl From<Option<Color>> for Fill {
impl From<Table<Color>> for Fill {
fn from(color: Table<Color>) -> Fill {
let alpha = color.get(0).map(|c| c.alpha_blending.opacity).unwrap_or(1.);
let color: Option<Color> = color.into();
let alpha = color.attribute_cloned_or_default::<AlphaBlending>("alpha_blending", 0).opacity;
let color = color.element(0).copied();
Fill::solid_or_none(color.map(|c| c.with_alpha(c.alpha() * alpha)))
}
}
@@ -142,7 +144,7 @@ impl From<Table<Color>> for Fill {
impl From<Table<GradientStops>> for Fill {
fn from(gradient: Table<GradientStops>) -> Fill {
Fill::Gradient(Gradient {
stops: gradient.iter().nth(0).map(|row| row.element.clone()).unwrap_or_default(),
stops: gradient.element(0).cloned().unwrap_or_default(),
..Default::default()
})
}
@@ -161,7 +163,8 @@ impl From<Gradient> for Fill {
/// In the future we'll probably also add a pattern fill.
#[repr(C)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Default, Debug, Clone, PartialEq, graphene_hash::CacheHash, serde::Serialize, serde::Deserialize, DynAny)]
#[derive(Default, Debug, Clone, PartialEq, graphene_hash::CacheHash, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum FillChoice {
#[default]
None,
@@ -209,7 +212,8 @@ impl From<Fill> for FillChoice {
#[repr(C)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Debug, Clone, Copy, Default, PartialEq, serde::Serialize, serde::Deserialize, DynAny, Hash, graphene_hash::CacheHash, node_macro::ChoiceType)]
#[derive(Debug, Clone, Copy, Default, PartialEq, DynAny, Hash, graphene_hash::CacheHash, node_macro::ChoiceType)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[widget(Radio)]
pub enum FillType {
#[default]
@@ -220,7 +224,8 @@ pub enum FillType {
/// The stroke (outline) style of an SVG element.
#[repr(C)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, graphene_hash::CacheHash, DynAny, node_macro::ChoiceType)]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, graphene_hash::CacheHash, DynAny, node_macro::ChoiceType)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[widget(Radio)]
pub enum StrokeCap {
#[default]
@@ -241,7 +246,8 @@ impl StrokeCap {
#[repr(C)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, graphene_hash::CacheHash, DynAny, node_macro::ChoiceType)]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, graphene_hash::CacheHash, DynAny, node_macro::ChoiceType)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[widget(Radio)]
pub enum StrokeJoin {
#[default]
@@ -262,7 +268,8 @@ impl StrokeJoin {
#[repr(C)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, graphene_hash::CacheHash, DynAny, node_macro::ChoiceType)]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, graphene_hash::CacheHash, DynAny, node_macro::ChoiceType)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[widget(Radio)]
pub enum StrokeAlign {
#[default]
@@ -279,7 +286,8 @@ impl StrokeAlign {
#[repr(C)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, graphene_hash::CacheHash, DynAny, node_macro::ChoiceType)]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, graphene_hash::CacheHash, DynAny, node_macro::ChoiceType)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[widget(Radio)]
pub enum PaintOrder {
#[default]
@@ -299,8 +307,9 @@ fn daffine2_identity() -> DAffine2 {
#[repr(C)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Debug, Clone, PartialEq, graphene_hash::CacheHash, serde::Serialize, serde::Deserialize, DynAny)]
#[serde(default)]
#[derive(Debug, Clone, PartialEq, graphene_hash::CacheHash, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(default))]
pub struct Stroke {
/// Stroke color
pub color: Option<Color>,
@@ -308,17 +317,17 @@ pub struct Stroke {
pub weight: f64,
pub dash_lengths: Vec<f64>,
pub dash_offset: f64,
#[serde(alias = "line_cap")]
#[cfg_attr(feature = "serde", serde(alias = "line_cap"))]
pub cap: StrokeCap,
#[serde(alias = "line_join")]
#[cfg_attr(feature = "serde", serde(alias = "line_join"))]
pub join: StrokeJoin,
#[serde(alias = "line_join_miter_limit")]
#[cfg_attr(feature = "serde", serde(alias = "line_join_miter_limit"))]
pub join_miter_limit: f64,
#[serde(default)]
#[cfg_attr(feature = "serde", serde(default))]
pub align: StrokeAlign,
#[serde(default = "daffine2_identity")]
#[cfg_attr(feature = "serde", serde(default = "daffine2_identity"))]
pub transform: DAffine2,
#[serde(default)]
#[cfg_attr(feature = "serde", serde(default))]
pub paint_order: PaintOrder,
}
@@ -494,7 +503,8 @@ impl Default for Stroke {
#[repr(C)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Debug, Clone, PartialEq, Default, graphene_hash::CacheHash, serde::Serialize, serde::Deserialize, DynAny)]
#[derive(Debug, Clone, PartialEq, Default, graphene_hash::CacheHash, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct PathStyle {
pub stroke: Option<Stroke>,
pub fill: Fill,
@@ -655,7 +665,8 @@ impl PathStyle {
/// Ways the user can choose to view the artwork in the viewport.
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, graphene_hash::CacheHash, DynAny)]
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, graphene_hash::CacheHash, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum RenderMode {
/// Render with normal coloration at the current viewport resolution
#[default]

View File

@@ -14,7 +14,7 @@ macro_rules! create_ids {
($($id:ident),*) => {
$(
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Ord, Eq, Hash, graphene_hash::CacheHash, DynAny)]
#[derive(serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
/// A strongly typed ID
pub struct $id(u64);
@@ -79,11 +79,12 @@ impl std::hash::BuildHasher for NoHashBuilder {
}
}
#[derive(Clone, Debug, Default, PartialEq, graphene_hash::CacheHash, DynAny, serde::Serialize, serde::Deserialize)]
#[derive(Clone, Debug, Default, PartialEq, graphene_hash::CacheHash, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
/// Stores data which is per-point. Each point is merely a position and can be used in a point cloud or to for a bézier path. In future this will be extendable at runtime with custom attributes.
pub struct PointDomain {
id: Vec<PointId>,
#[serde(alias = "positions")]
#[cfg_attr(feature = "serde", serde(alias = "positions"))]
pub(crate) position: Vec<DVec2>,
}
@@ -205,10 +206,11 @@ impl PointDomain {
}
}
#[derive(Clone, Debug, Default, PartialEq, graphene_hash::CacheHash, DynAny, serde::Serialize, serde::Deserialize)]
#[derive(Clone, Debug, Default, PartialEq, graphene_hash::CacheHash, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
/// Stores data which is per-segment. A segment is a bézier curve between two end points with a stroke. In future this will be extendable at runtime with custom attributes.
pub struct SegmentDomain {
#[serde(alias = "ids")]
#[cfg_attr(feature = "serde", serde(alias = "ids"))]
id: Vec<SegmentId>,
start_point: Vec<usize>,
end_point: Vec<usize>,
@@ -587,11 +589,12 @@ impl SegmentDomain {
}
}
#[derive(Clone, Debug, Default, PartialEq, Hash, graphene_hash::CacheHash, DynAny, serde::Serialize, serde::Deserialize)]
#[derive(Clone, Debug, Default, PartialEq, Hash, graphene_hash::CacheHash, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
/// Stores data which is per-region. A region is an enclosed area composed of a range of segments from the
/// [`SegmentDomain`] that can be given a fill. In future this will be extendable at runtime with custom attributes.
pub struct RegionDomain {
#[serde(alias = "ids")]
#[cfg_attr(feature = "serde", serde(alias = "ids"))]
id: Vec<RegionId>,
segment_range: Vec<std::ops::RangeInclusive<SegmentId>>,
fill: Vec<FillId>,

View File

@@ -9,11 +9,12 @@ use std::collections::{HashMap, HashSet};
use std::hash::BuildHasher;
/// Represents a procedural change to the [`PointDomain`] in [`Vector`].
#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
#[derive(Clone, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct PointModification {
add: Vec<PointId>,
remove: HashSet<PointId>,
#[serde(serialize_with = "serialize_hashmap", deserialize_with = "deserialize_hashmap")]
#[cfg_attr(feature = "serde", serde(serialize_with = "serialize_hashmap", deserialize_with = "deserialize_hashmap"))]
delta: HashMap<PointId, DVec2>,
}
@@ -74,19 +75,20 @@ impl PointModification {
}
/// Represents a procedural change to the [`SegmentDomain`] in [`Vector`].
#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
#[derive(Clone, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct SegmentModification {
add: Vec<SegmentId>,
remove: HashSet<SegmentId>,
#[serde(serialize_with = "serialize_hashmap", deserialize_with = "deserialize_hashmap")]
#[cfg_attr(feature = "serde", serde(serialize_with = "serialize_hashmap", deserialize_with = "deserialize_hashmap"))]
start_point: HashMap<SegmentId, PointId>,
#[serde(serialize_with = "serialize_hashmap", deserialize_with = "deserialize_hashmap")]
#[cfg_attr(feature = "serde", serde(serialize_with = "serialize_hashmap", deserialize_with = "deserialize_hashmap"))]
end_point: HashMap<SegmentId, PointId>,
#[serde(serialize_with = "serialize_hashmap", deserialize_with = "deserialize_hashmap")]
#[cfg_attr(feature = "serde", serde(serialize_with = "serialize_hashmap", deserialize_with = "deserialize_hashmap"))]
handle_primary: HashMap<SegmentId, Option<DVec2>>,
#[serde(serialize_with = "serialize_hashmap", deserialize_with = "deserialize_hashmap")]
#[cfg_attr(feature = "serde", serde(serialize_with = "serialize_hashmap", deserialize_with = "deserialize_hashmap"))]
handle_end: HashMap<SegmentId, Option<DVec2>>,
#[serde(serialize_with = "serialize_hashmap", deserialize_with = "deserialize_hashmap")]
#[cfg_attr(feature = "serde", serde(serialize_with = "serialize_hashmap", deserialize_with = "deserialize_hashmap"))]
stroke: HashMap<SegmentId, StrokeId>,
}
@@ -244,13 +246,14 @@ impl SegmentModification {
}
/// Represents a procedural change to the [`RegionDomain`] in [`Vector`].
#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
#[derive(Clone, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct RegionModification {
add: Vec<RegionId>,
remove: HashSet<RegionId>,
#[serde(serialize_with = "serialize_hashmap", deserialize_with = "deserialize_hashmap")]
#[cfg_attr(feature = "serde", serde(serialize_with = "serialize_hashmap", deserialize_with = "deserialize_hashmap"))]
segment_range: HashMap<RegionId, std::ops::RangeInclusive<SegmentId>>,
#[serde(serialize_with = "serialize_hashmap", deserialize_with = "deserialize_hashmap")]
#[cfg_attr(feature = "serde", serde(serialize_with = "serialize_hashmap", deserialize_with = "deserialize_hashmap"))]
fill: HashMap<RegionId, FillId>,
}
@@ -288,7 +291,8 @@ impl RegionModification {
}
/// Represents a procedural change to the [`Vector`].
#[derive(Clone, Debug, Default, PartialEq, DynAny, serde::Serialize, serde::Deserialize)]
#[derive(Clone, Debug, Default, PartialEq, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct VectorModification {
points: PointModification,
segments: SegmentModification,
@@ -298,7 +302,8 @@ pub struct VectorModification {
}
/// A modification type that can be added to a [`VectorModification`].
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
#[derive(PartialEq, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum VectorModificationType {
InsertSegment { id: SegmentId, points: [PointId; 2], handles: [Option<DVec2>; 2] },
InsertPoint { id: PointId, position: DVec2 },

View File

@@ -20,7 +20,8 @@ use std::collections::HashMap;
/// Generic over `Upstream` to avoid circular dependency with the Graphic type.
/// - Use `Vector<()>` for basic vectors without upstream tracking
/// - Use `Vector<Option<Table<Graphic>>>` in the graphic crate for vectors with upstream layers
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Vector<Upstream> {
pub style: PathStyle,
@@ -34,7 +35,7 @@ pub struct Vector<Upstream> {
/// Used to store the upstream group/folder of nested layers during destructive Boolean Operations (and other nodes with a similar effect) so that click targets can be preserved for the child layers.
/// Without this, the tools would be working with a collapsed version of the data which has no reference to the original child layers that were booleaned together, resulting in the inner layers not being editable.
#[serde(alias = "upstream_group")]
#[cfg_attr(feature = "serde", serde(alias = "upstream_group"))]
pub upstream_data: Upstream,
}
unsafe impl<Upstream: 'static> StaticType for Vector<Upstream> {

View File

@@ -168,10 +168,10 @@ impl PerPixelAdjustGraphicsPipeline {
let mut cmd = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some(&format!("{name} cmd encoder")),
});
let out = textures
.iter()
.map(|instance| {
let tex_in = &instance.element.texture;
let out = (0..textures.len())
.map(|index| {
let element = textures.element(index).unwrap();
let tex_in = &element.texture;
let view_in = tex_in.create_view(&TextureViewDescriptor::default());
let format = tex_in.format();
@@ -233,12 +233,8 @@ impl PerPixelAdjustGraphicsPipeline {
rp.set_bind_group(0, Some(&bind_group), &[]);
rp.draw(0..3, 0..1);
TableRow {
element: Raster::new(GPU { texture: tex_out }),
transform: *instance.transform,
alpha_blending: *instance.alpha_blending,
source_node_id: *instance.source_node_id,
}
let attributes = textures.clone_row_attributes(index);
TableRow::from_parts(Raster::new(GPU { texture: tex_out }), attributes)
})
.collect::<Table<_>>();
context.queue.submit([cmd.finish()]);

View File

@@ -150,17 +150,12 @@ impl<'i> Convert<Table<Raster<GPU>>, &'i WgpuExecutor> for Table<Raster<CPU>> {
let device = &executor.context.device;
let queue = &executor.context.queue;
let table = self
.iter()
.into_iter()
.map(|row| {
let image = row.element;
let texture = upload_to_texture(device, queue, image);
let (image, attributes) = row.into_parts();
let texture = upload_to_texture(device, queue, &image);
TableRow {
element: Raster::new_gpu(texture),
transform: *row.transform,
alpha_blending: *row.alpha_blending,
source_node_id: *row.source_node_id,
}
TableRow::from_parts(Raster::new_gpu(texture), attributes)
})
.collect();
@@ -204,14 +199,9 @@ impl<'i> Convert<Table<Raster<CPU>>, &'i WgpuExecutor> for Table<Raster<GPU>> {
let mut rows_meta = Vec::new();
for row in self {
let gpu_raster = row.element;
converters.push(RasterGpuToRasterCpuConverter::new(device, &mut encoder, gpu_raster));
rows_meta.push(TableRow {
element: (),
transform: row.transform,
alpha_blending: row.alpha_blending,
source_node_id: row.source_node_id,
});
let (element, attributes) = row.into_parts();
converters.push(RasterGpuToRasterCpuConverter::new(device, &mut encoder, element));
rows_meta.push(TableRow::from_parts((), attributes));
}
queue.submit([encoder.finish()]);
@@ -229,11 +219,9 @@ impl<'i> Convert<Table<Raster<CPU>>, &'i WgpuExecutor> for Table<Raster<GPU>> {
map_results
.into_iter()
.zip(rows_meta.into_iter())
.map(|(element, row)| TableRow {
element,
transform: row.transform,
alpha_blending: row.alpha_blending,
source_node_id: row.source_node_id,
.map(|(element, row)| {
let (_, attributes) = row.into_parts();
TableRow::from_parts(element, attributes)
})
.collect()
}