diff --git a/Cargo.lock b/Cargo.lock index 324b645471..b78a3ede44 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -350,6 +350,7 @@ dependencies = [ "core-types", "dyn-any", "glam", + "graphene-hash", "node-macro", "raster-nodes", "raster-types", @@ -873,6 +874,7 @@ dependencies = [ "ctor", "dyn-any", "glam", + "graphene-hash", "image", "kurbo", "log", @@ -1996,6 +1998,7 @@ dependencies = [ "core-types", "dyn-any", "glam", + "graphene-hash", "graphic-types", "log", "node-macro", @@ -2005,6 +2008,24 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "graphene-hash" +version = "0.0.0" +dependencies = [ + "glam", + "graphene-hash-derive", +] + +[[package]] +name = "graphene-hash-derive" +version = "0.0.0" +dependencies = [ + "graphene-hash", + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "graphene-std" version = "0.1.0" @@ -2065,6 +2086,7 @@ dependencies = [ "core-types", "dyn-any", "glam", + "graphene-hash", "node-macro", "raster-types", "serde", @@ -3296,6 +3318,7 @@ dependencies = [ "core-types", "dyn-any", "glam", + "graphene-hash", "half", "log", "node-macro", @@ -4316,6 +4339,7 @@ dependencies = [ "fastnoise-lite", "futures", "glam", + "graphene-hash", "image", "kurbo", "ndarray", @@ -4361,6 +4385,7 @@ dependencies = [ "core-types", "dyn-any", "glam", + "graphene-hash", "image", "node-macro", "serde", @@ -4501,6 +4526,7 @@ dependencies = [ "core-types", "dyn-any", "glam", + "graphene-hash", "graphic-types", "kurbo", "log", @@ -5513,6 +5539,7 @@ dependencies = [ "dyn-any", "fancy-regex", "glam", + "graphene-hash", "log", "node-macro", "parley", @@ -6157,6 +6184,7 @@ dependencies = [ "futures", "glam", "graphene-core", + "graphene-hash", "graphic-types", "kurbo", "log", @@ -6182,6 +6210,7 @@ dependencies = [ "dyn-any", "fixedbitset", "glam", + "graphene-hash", "kurbo", "log", "lyon_geom", diff --git a/Cargo.toml b/Cargo.toml index 802a5acce1..c6c6d60924 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ members = [ "frontend/wrapper", "libraries/dyn-any", "libraries/math-parser", + "node-graph/libraries/graphene-hash", "node-graph/libraries/*", "node-graph/nodes/*", "node-graph/nodes/raster/shaders", @@ -63,6 +64,7 @@ dyn-any = { path = "libraries/dyn-any", features = [ "log-bad-types", "rc", ] } +graphene-hash = { path = "node-graph/libraries/graphene-hash", features = ["derive"] } preprocessor = { path = "node-graph/preprocessor" } math-parser = { path = "libraries/math-parser" } graphene-application-io = { path = "node-graph/libraries/application-io" } diff --git a/editor/src/messages/tool/tool_messages/gradient_tool.rs b/editor/src/messages/tool/tool_messages/gradient_tool.rs index f6b8f49be9..3859505e36 100644 --- a/editor/src/messages/tool/tool_messages/gradient_tool.rs +++ b/editor/src/messages/tool/tool_messages/gradient_tool.rs @@ -26,7 +26,7 @@ pub struct GradientOptions { #[impl_message(Message, ToolMessage, Gradient)] #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(PartialEq, Clone, Debug, Hash, serde::Serialize, serde::Deserialize)] +#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)] pub enum GradientToolMessage { // Standard messages Abort, diff --git a/frontend/wrapper/src/editor_wrapper.rs b/frontend/wrapper/src/editor_wrapper.rs index 74a76d7c3e..e9dd0edf6d 100644 --- a/frontend/wrapper/src/editor_wrapper.rs +++ b/frontend/wrapper/src/editor_wrapper.rs @@ -22,6 +22,7 @@ use editor::messages::portfolio::utility_types::{DockingSplitDirection, FontCata use editor::messages::prelude::*; use editor::messages::tool::tool_messages::tool_prelude::WidgetId; use graph_craft::document::NodeId; +use graphene_std::graphene_hash::CacheHashWrapper; use graphene_std::raster::color::Color; use graphene_std::vector::GradientStops; use serde::Serialize; @@ -131,7 +132,7 @@ impl EditorWrapper { // Sends a FrontendMessage to JavaScript pub(crate) fn send_frontend_message_to_js(&self, message: FrontendMessage) { if let FrontendMessage::UpdateImageData { ref image_data } = message { - let new_hash = calculate_hash(image_data); + let new_hash = calculate_hash(&CacheHashWrapper(image_data)); let prev_hash = IMAGE_DATA_HASH.load(Ordering::Relaxed); if new_hash != prev_hash { diff --git a/node-graph/graph-craft/src/document.rs b/node-graph/graph-craft/src/document.rs index 0cb33bf3ff..ad89254eb9 100644 --- a/node-graph/graph-craft/src/document.rs +++ b/node-graph/graph-craft/src/document.rs @@ -1136,6 +1136,12 @@ fn migrate_call_argument<'de, D: serde::Deserializer<'de>>(deserializer: D) -> R }) } +impl core_types::graphene_hash::CacheHash for DocumentNode { + fn cache_hash(&self, state: &mut H) { + core::hash::Hash::hash(self, state); + } +} + #[cfg(test)] mod test { use super::*; diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index d89e8f7996..caf7cec451 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -5,7 +5,7 @@ use brush_nodes::brush_cache::BrushCache; use brush_nodes::brush_stroke::BrushStroke; use core_types::table::Table; use core_types::uuid::NodeId; -use core_types::{Color, ContextFeatures, MemoHash, Node, Type}; +use core_types::{CacheHash, Color, ContextFeatures, MemoHash, Node, Type}; use dyn_any::DynAny; pub use dyn_any::StaticType; use glam::{Affine2, Vec2}; @@ -43,19 +43,18 @@ macro_rules! tagged_value { EditorApi(Arc) } - // We must manually implement hashing because some values are floats and so do not reproducibly hash (see FakeHash below) - #[allow(clippy::derived_hash_with_manual_eq)] - impl Hash for TaggedValue { - fn hash(&self, state: &mut H) { + impl CacheHash for TaggedValue { + fn cache_hash(&self, state: &mut H) { core::mem::discriminant(self).hash(state); match self { Self::None => {} - $( Self::$identifier(x) => {x.hash(state)}),* - Self::RenderOutput(x) => x.hash(state), - Self::EditorApi(x) => x.hash(state), + $( Self::$identifier(x) => { x.cache_hash(state) }),* + Self::RenderOutput(x) => x.cache_hash(state), + Self::EditorApi(x) => x.cache_hash(state), } } } + impl<'a> TaggedValue { /// Converts to a Box pub fn to_dynany(self) -> DAny<'a> { @@ -495,96 +494,45 @@ pub enum RenderOutputType { }, } -impl Hash for RenderOutputType { - fn hash(&self, state: &mut H) { +impl CacheHash for RenderOutputType { + fn cache_hash(&self, state: &mut H) { + core::mem::discriminant(self).hash(state); match self { - Self::Texture(texture) => { - texture.hash(state); - } + Self::Texture(texture) => texture.hash(state), Self::Buffer { data, width, height } => { - data.hash(state); - width.hash(state); - height.hash(state); + data.cache_hash(state); + width.cache_hash(state); + height.cache_hash(state); } Self::Svg { svg, image_data } => { - svg.hash(state); - image_data.hash(state); + svg.cache_hash(state); + image_data.cache_hash(state); } #[cfg(target_family = "wasm")] Self::CanvasFrame { canvas_id, resolution } => { - canvas_id.hash(state); - resolution.to_array().iter().for_each(|x| x.to_bits().hash(state)); + canvas_id.cache_hash(state); + resolution.cache_hash(state); } } } } -impl Hash for RenderOutput { + +impl Hash for RenderOutputType { fn hash(&self, state: &mut H) { - self.data.hash(state) + CacheHash::cache_hash(self, state); } } -/// We hash the floats and so-forth despite it not being reproducible because all inputs to the node graph must be hashed otherwise the graph execution breaks (so sorry about this hack) -trait FakeHash { - fn hash(&self, state: &mut H); -} -mod fake_hash { - use super::*; - impl FakeHash for f64 { - fn hash(&self, state: &mut H) { - self.to_bits().hash(state) - } - } - impl FakeHash for f32 { - fn hash(&self, state: &mut H) { - self.to_bits().hash(state) - } - } - impl FakeHash for DVec2 { - fn hash(&self, state: &mut H) { - self.to_array().iter().for_each(|x| x.to_bits().hash(state)) - } - } - impl FakeHash for Vec2 { - fn hash(&self, state: &mut H) { - self.to_array().iter().for_each(|x| x.to_bits().hash(state)) - } - } - impl FakeHash for DAffine2 { - fn hash(&self, state: &mut H) { - self.to_cols_array().iter().for_each(|x| x.to_bits().hash(state)) - } - } - impl FakeHash for Affine2 { - fn hash(&self, state: &mut H) { - self.to_cols_array().iter().for_each(|x| x.to_bits().hash(state)) - } - } - impl FakeHash for Option { - fn hash(&self, state: &mut H) { - if let Some(x) = self { - 1.hash(state); - x.hash(state); - } else { - 0.hash(state); - } - } - } - impl FakeHash for Vec { - fn hash(&self, state: &mut H) { - self.len().hash(state); - self.iter().for_each(|x| x.hash(state)) - } - } - impl FakeHash for [T; N] { - fn hash(&self, state: &mut H) { - self.iter().for_each(|x| x.hash(state)) - } +// Metadata is excluded because it's editor-side auxiliary data (click targets, transforms) +// that shouldn't affect render cache invalidation, and it contains HashMaps with non-deterministic iteration order +impl CacheHash for RenderOutput { + fn cache_hash(&self, state: &mut H) { + self.data.cache_hash(state); } - impl FakeHash for (f64, Color) { - fn hash(&self, state: &mut H) { - self.0.to_bits().hash(state); - self.1.hash(state) - } +} + +impl Hash for RenderOutput { + fn hash(&self, state: &mut H) { + CacheHash::cache_hash(self, state); } } diff --git a/node-graph/libraries/application-io/src/lib.rs b/node-graph/libraries/application-io/src/lib.rs index c28a1baa7b..aec1fba5ee 100644 --- a/node-graph/libraries/application-io/src/lib.rs +++ b/node-graph/libraries/application-io/src/lib.rs @@ -164,6 +164,12 @@ impl Hash for EditorApi { } } +impl core_types::graphene_hash::CacheHash for EditorApi { + fn cache_hash(&self, state: &mut H) { + core::hash::Hash::hash(self, state); + } +} + impl PartialEq for EditorApi { fn eq(&self, other: &Self) -> bool { self.font_cache == other.font_cache diff --git a/node-graph/libraries/core-types/Cargo.toml b/node-graph/libraries/core-types/Cargo.toml index d6bc01c4db..e3259fd579 100644 --- a/node-graph/libraries/core-types/Cargo.toml +++ b/node-graph/libraries/core-types/Cargo.toml @@ -16,6 +16,7 @@ wasm = ["tsify", "wasm-bindgen", "no-std-types/wasm"] [dependencies] # Local dependencies no-std-types = { workspace = true, features = ["std"] } +graphene-hash = { workspace = true, features = ["derive"] } # Workspace dependencies bitflags = { workspace = true } diff --git a/node-graph/libraries/core-types/src/context.rs b/node-graph/libraries/core-types/src/context.rs index 443d2f8b2f..64e70aa8d8 100644 --- a/node-graph/libraries/core-types/src/context.rs +++ b/node-graph/libraries/core-types/src/context.rs @@ -163,6 +163,12 @@ bitflags! { } } +impl graphene_hash::CacheHash for ContextFeatures { + fn cache_hash(&self, state: &mut H) { + core::hash::Hash::hash(self, state); + } +} + impl ContextFeatures { pub fn name(&self) -> &'static str { match *self { @@ -536,14 +542,14 @@ impl Default for OwnedContextImpl { } } -impl Hash for OwnedContextImpl { - fn hash(&self, state: &mut H) { - self.footprint.hash(state); - self.real_time.map(|x| x.to_bits()).hash(state); - self.animation_time.map(|x| x.to_bits()).hash(state); - self.pointer_position.map(|v| (v.x.to_bits(), v.y.to_bits())).hash(state); - self.position.iter().flat_map(|x| x.iter()).map(|v| (v.x.to_bits(), v.y.to_bits())).for_each(|v| v.hash(state)); - self.index.hash(state); +impl graphene_hash::CacheHash for OwnedContextImpl { + fn cache_hash(&self, state: &mut H) { + self.footprint.cache_hash(state); + self.real_time.cache_hash(state); + self.animation_time.cache_hash(state); + self.pointer_position.cache_hash(state); + self.position.cache_hash(state); + self.index.cache_hash(state); self.hash_varargs(state); } } @@ -600,9 +606,9 @@ pub trait DynHash { fn dyn_hash(&self, state: &mut dyn Hasher); } -impl DynHash for H { +impl DynHash for H { fn dyn_hash(&self, mut state: &mut dyn Hasher) { - self.hash(&mut state); + graphene_hash::CacheHash::cache_hash(self, &mut state); } } diff --git a/node-graph/libraries/core-types/src/lib.rs b/node-graph/libraries/core-types/src/lib.rs index 1d2f540a40..f28758405f 100644 --- a/node-graph/libraries/core-types/src/lib.rs +++ b/node-graph/libraries/core-types/src/lib.rs @@ -21,6 +21,8 @@ pub use color::Color; pub use context::*; pub use ctor; pub use dyn_any::{StaticTypeSized, WasmNotSend, WasmNotSync}; +pub use graphene_hash; +pub use graphene_hash::CacheHash; pub use memo::MemoHash; pub use no_std_types::AsU32; pub use no_std_types::blending; diff --git a/node-graph/libraries/core-types/src/memo.rs b/node-graph/libraries/core-types/src/memo.rs index 42d2d4b608..17a618c9a6 100644 --- a/node-graph/libraries/core-types/src/memo.rs +++ b/node-graph/libraries/core-types/src/memo.rs @@ -1,3 +1,4 @@ +use graphene_hash::CacheHash; use std::hash::DefaultHasher; use std::hash::{Hash, Hasher}; use std::ops::Deref; @@ -11,12 +12,12 @@ pub struct IORecord { } #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] -pub struct MemoHash { +pub struct MemoHash { hash: u64, value: Arc, } -impl<'de, T: serde::Deserialize<'de> + Hash> serde::Deserialize<'de> for MemoHash { +impl<'de, T: serde::Deserialize<'de> + CacheHash> serde::Deserialize<'de> for MemoHash { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, @@ -25,7 +26,7 @@ impl<'de, T: serde::Deserialize<'de> + Hash> serde::Deserialize<'de> for MemoHas } } -impl serde::Serialize for MemoHash { +impl serde::Serialize for MemoHash { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, @@ -34,7 +35,7 @@ impl serde::Serialize for MemoHash { } } -impl MemoHash { +impl MemoHash { pub fn new(value: T) -> Self { let hash = Self::calc_hash(&value); Self { hash, value: value.into() } @@ -45,7 +46,7 @@ impl MemoHash { fn calc_hash(data: &T) -> u64 { let mut hasher = DefaultHasher::new(); - data.hash(&mut hasher); + data.cache_hash(&mut hasher); hasher.finish() } @@ -59,19 +60,26 @@ impl MemoHash { self.hash } } -impl From for MemoHash { + +impl From for MemoHash { fn from(value: T) -> Self { Self::new(value) } } -impl Hash for MemoHash { +impl Hash for MemoHash { fn hash(&self, state: &mut H) { self.hash.hash(state) } } -impl Deref for MemoHash { +impl CacheHash for MemoHash { + fn cache_hash(&self, state: &mut H) { + self.hash.hash(state); + } +} + +impl Deref for MemoHash { type Target = T; fn deref(&self) -> &Self::Target { @@ -79,18 +87,18 @@ impl Deref for MemoHash { } } -pub struct MemoHashGuard<'a, T: Hash> { +pub struct MemoHashGuard<'a, T: CacheHash> { inner: &'a mut MemoHash, } -impl Drop for MemoHashGuard<'_, T> { +impl Drop for MemoHashGuard<'_, T> { fn drop(&mut self) { let hash = MemoHash::::calc_hash(&self.inner.value); self.inner.hash = hash; } } -impl Deref for MemoHashGuard<'_, T> { +impl Deref for MemoHashGuard<'_, T> { type Target = T; fn deref(&self) -> &Self::Target { @@ -98,7 +106,7 @@ impl Deref for MemoHashGuard<'_, T> { } } -impl std::ops::DerefMut for MemoHashGuard<'_, T> { +impl std::ops::DerefMut for MemoHashGuard<'_, T> { fn deref_mut(&mut self) -> &mut Self::Target { Arc::make_mut(&mut self.inner.value) } diff --git a/node-graph/libraries/core-types/src/table.rs b/node-graph/libraries/core-types/src/table.rs index 4a814aaf58..648e6e0909 100644 --- a/node-graph/libraries/core-types/src/table.rs +++ b/node-graph/libraries/core-types/src/table.rs @@ -4,7 +4,6 @@ use crate::uuid::NodeId; use crate::{AlphaBlending, math::quad::Quad}; use dyn_any::{StaticType, StaticTypeSized}; use glam::DAffine2; -use std::hash::Hash; #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] pub struct Table { @@ -198,16 +197,16 @@ impl Default for Table { } } -impl Hash for Table { - fn hash(&self, state: &mut H) { +impl graphene_hash::CacheHash for Table { + fn cache_hash(&self, state: &mut H) { for element in &self.element { - element.hash(state); + element.cache_hash(state); } for transform in &self.transform { - transform.to_cols_array().map(|x| x.to_bits()).hash(state); + graphene_hash::CacheHash::cache_hash(transform, state); } for alpha_blending in &self.alpha_blending { - alpha_blending.hash(state); + alpha_blending.cache_hash(state); } } } diff --git a/node-graph/libraries/core-types/src/transform.rs b/node-graph/libraries/core-types/src/transform.rs index f92965ab97..c657b2958f 100644 --- a/node-graph/libraries/core-types/src/transform.rs +++ b/node-graph/libraries/core-types/src/transform.rs @@ -6,7 +6,7 @@ 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, DynAny, node_macro::ChoiceType)] +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, graphene_hash::CacheHash, DynAny, node_macro::ChoiceType)] #[widget(Radio)] pub enum ScaleType { /// The visual length of each axis (always positive, includes any skew contribution). @@ -141,7 +141,7 @@ impl TransformMut for Footprint { } } -#[derive(Debug, Clone, Copy, dyn_any::DynAny, PartialEq, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, Copy, dyn_any::DynAny, PartialEq, graphene_hash::CacheHash, serde::Serialize, serde::Deserialize)] pub enum RenderQuality { /// Low quality, fast rendering Preview, @@ -154,7 +154,7 @@ pub enum RenderQuality { /// Render at full quality Full, } -#[derive(Debug, Clone, Copy, dyn_any::DynAny, PartialEq, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, Copy, dyn_any::DynAny, PartialEq, graphene_hash::CacheHash, 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, @@ -214,13 +214,6 @@ impl From<()> for Footprint { } } -impl std::hash::Hash for Footprint { - fn hash(&self, state: &mut H) { - self.transform.to_cols_array().iter().for_each(|x| x.to_le_bytes().hash(state)); - self.resolution.hash(state) - } -} - pub trait ApplyTransform { fn apply_transform(&mut self, modification: &DAffine2); fn left_apply_transform(&mut self, modification: &DAffine2); diff --git a/node-graph/libraries/core-types/src/uuid.rs b/node-graph/libraries/core-types/src/uuid.rs index 9ddab56d4c..6847c2c384 100644 --- a/node-graph/libraries/core-types/src/uuid.rs +++ b/node-graph/libraries/core-types/src/uuid.rs @@ -68,7 +68,7 @@ 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, PartialOrd, Ord, serde::Serialize, serde::Deserialize, DynAny)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, graphene_hash::CacheHash, PartialOrd, Ord, serde::Serialize, serde::Deserialize, DynAny)] pub struct NodeId(pub u64); impl NodeId { diff --git a/node-graph/libraries/graphene-hash/Cargo.toml b/node-graph/libraries/graphene-hash/Cargo.toml new file mode 100644 index 0000000000..f1827e57f2 --- /dev/null +++ b/node-graph/libraries/graphene-hash/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "graphene-hash" +version = "0.0.0" +edition = "2024" +authors = ["Graphite Authors "] +description = "CacheHash trait and derive macro for cache invalidation hashing in Graphite" +license = "MIT OR Apache-2.0" +publish = false + +[features] +default = ["std"] +std = [] +derive = ["graphene-hash-derive"] + +[dependencies] +graphene-hash-derive = { path = "derive", optional = true } +glam = { workspace = true } diff --git a/node-graph/libraries/graphene-hash/derive/Cargo.toml b/node-graph/libraries/graphene-hash/derive/Cargo.toml new file mode 100644 index 0000000000..e96fd50016 --- /dev/null +++ b/node-graph/libraries/graphene-hash/derive/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "graphene-hash-derive" +version = "0.0.0" +edition = "2024" +authors = ["Graphite Authors "] +description = "#[derive(CacheHash)]" +license = "MIT OR Apache-2.0" +publish = false + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = { workspace = true } +quote = { workspace = true } +syn = { workspace = true } + +[dev-dependencies] +graphene-hash = { path = "..", features = ["derive"] } diff --git a/node-graph/libraries/graphene-hash/derive/src/lib.rs b/node-graph/libraries/graphene-hash/derive/src/lib.rs new file mode 100644 index 0000000000..85c1e828d6 --- /dev/null +++ b/node-graph/libraries/graphene-hash/derive/src/lib.rs @@ -0,0 +1,130 @@ +extern crate proc_macro; + +use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; +use quote::quote; +use syn::{Data, DeriveInput, Fields, parse_macro_input}; + +/// Derives `CacheHash` for a struct or enum. +/// +/// All fields must implement `CacheHash`. Fields annotated with `#[cache_hash(skip)]` +/// are excluded from hashing. +/// +/// # Example +/// +/// ``` +/// # use graphene_hash::CacheHash; +/// #[derive(CacheHash)] +/// pub struct MyNode { +/// pub value: f64, +/// pub count: u32, +/// #[cache_hash(skip)] +/// pub debug_label: String, +/// } +/// ``` +#[proc_macro_derive(CacheHash, attributes(cache_hash))] +pub fn derive_cache_hash(input: TokenStream) -> TokenStream { + let ast = parse_macro_input!(input as DeriveInput); + let name = &ast.ident; + let mut generics = ast.generics.clone(); + for param in &mut generics.params { + if let syn::GenericParam::Type(type_param) = param { + type_param.bounds.push(syn::parse_quote!(graphene_hash::CacheHash)); + } + } + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + + let body = match &ast.data { + Data::Struct(s) => hash_fields(&s.fields, quote! { self }), + Data::Enum(e) => { + let arms = e.variants.iter().map(|variant| { + let variant_name = &variant.ident; + let (pattern, hash_body) = match &variant.fields { + Fields::Unit => (quote! {}, quote! {}), + Fields::Unnamed(fields) => { + let bindings: Vec<_> = (0..fields.unnamed.len()) + .map(|i| { + let ident = proc_macro2::Ident::new(&format!("f{i}"), proc_macro2::Span::call_site()); + quote! { #ident } + }) + .collect(); + let hash_stmts = fields.unnamed.iter().enumerate().filter_map(|(i, field)| { + if has_skip_attr(&field.attrs) { + return None; + } + let ident = proc_macro2::Ident::new(&format!("f{i}"), proc_macro2::Span::call_site()); + Some(quote! { graphene_hash::CacheHash::cache_hash(#ident, state); }) + }); + (quote! { (#(#bindings,)*) }, quote! { #(#hash_stmts)* }) + } + Fields::Named(fields) => { + let names: Vec<_> = fields.named.iter().map(|f| f.ident.as_ref().unwrap()).collect(); + let hash_stmts = fields.named.iter().filter_map(|field| { + if has_skip_attr(&field.attrs) { + return None; + } + let ident = field.ident.as_ref().unwrap(); + Some(quote! { graphene_hash::CacheHash::cache_hash(#ident, state); }) + }); + (quote! { { #(#names,)* } }, quote! { #(#hash_stmts)* }) + } + }; + quote! { + Self::#variant_name #pattern => { #hash_body } + } + }); + quote! { + ::core::hash::Hash::hash(&::core::mem::discriminant(self), state); + match self { + #(#arms)* + } + } + } + Data::Union(_) => return syn::Error::new(ast.ident.span(), "CacheHash cannot be derived for unions").to_compile_error().into(), + }; + + quote! { + #[allow(clippy::derived_hash_with_manual_eq)] + impl #impl_generics graphene_hash::CacheHash for #name #ty_generics #where_clause { + fn cache_hash(&self, state: &mut H) { + #body + } + } + } + .into() +} + +fn hash_fields(fields: &Fields, self_expr: TokenStream2) -> TokenStream2 { + match fields { + Fields::Unit => quote! {}, + Fields::Unnamed(fields) => { + let stmts = fields.unnamed.iter().enumerate().filter_map(|(i, field)| { + if has_skip_attr(&field.attrs) { + return None; + } + let index = syn::Index::from(i); + Some(quote! { graphene_hash::CacheHash::cache_hash(&#self_expr.#index, state); }) + }); + quote! { #(#stmts)* } + } + Fields::Named(fields) => { + let stmts = fields.named.iter().filter_map(|field| { + if has_skip_attr(&field.attrs) { + return None; + } + let ident = field.ident.as_ref().unwrap(); + Some(quote! { graphene_hash::CacheHash::cache_hash(&#self_expr.#ident, state); }) + }); + quote! { #(#stmts)* } + } + } +} + +fn has_skip_attr(attrs: &[syn::Attribute]) -> bool { + attrs.iter().any(|attr| { + if !attr.path().is_ident("cache_hash") { + return false; + } + attr.parse_args::().map(|id| id == "skip").unwrap_or(false) + }) +} diff --git a/node-graph/libraries/graphene-hash/src/lib.rs b/node-graph/libraries/graphene-hash/src/lib.rs new file mode 100644 index 0000000000..2ec047b3eb --- /dev/null +++ b/node-graph/libraries/graphene-hash/src/lib.rs @@ -0,0 +1,253 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +#[cfg(feature = "std")] +extern crate std; + +#[cfg(feature = "derive")] +pub use graphene_hash_derive::CacheHash; + +pub trait CacheHash { + fn cache_hash(&self, state: &mut H); +} + +/// Wrapper that implements `std::hash::Hash` by delegating to `CacheHash`. +/// +/// Use this to store `CacheHash` types in `HashMap`/`HashSet` keys, +/// making it explicit that float fields are hashed via bit patterns. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct CacheHashWrapper(pub T); + +impl core::hash::Hash for CacheHashWrapper { + fn hash(&self, state: &mut H) { + self.0.cache_hash(state); + } +} + +impl CacheHash for core::ops::RangeInclusive { + #[inline] + fn cache_hash(&self, state: &mut H) { + self.start().cache_hash(state); + self.end().cache_hash(state); + } +} + +impl core::ops::Deref for CacheHashWrapper { + type Target = T; + fn deref(&self) -> &T { + &self.0 + } +} + +// Bulk impl for types that already implement std::hash::Hash — delegates directly. +#[macro_export] +macro_rules! impl_via_hash { + ($($t:ty),* $(,)?) => { + $( + impl $crate::CacheHash for $t { + #[inline] + fn cache_hash(&self, state: &mut H) { + core::hash::Hash::hash(self, state); + } + } + )* + }; +} + +impl_via_hash! { + bool, char, + u8, u16, u32, u64, u128, usize, + i8, i16, i32, i64, i128, isize, + // glam integer vector types have Hash + glam::UVec2, glam::UVec3, glam::UVec4, + glam::IVec2, glam::IVec3, glam::IVec4, + glam::I64Vec2, glam::I64Vec3, glam::I64Vec4, + glam::U64Vec2, glam::U64Vec3, glam::U64Vec4, + glam::BVec2, glam::BVec3, glam::BVec4, +} + +#[cfg(feature = "std")] +impl_via_hash! { + String, +} + +impl CacheHash for str { + #[inline] + fn cache_hash(&self, state: &mut H) { + core::hash::Hash::hash(self, state); + } +} + +impl CacheHash for () { + #[inline] + fn cache_hash(&self, _state: &mut H) {} +} + +// f32 and f64: hash via bit pattern so NaN is handled deterministically. +impl CacheHash for f32 { + #[inline] + fn cache_hash(&self, state: &mut H) { + core::hash::Hash::hash(&self.to_bits(), state); + } +} + +impl CacheHash for f64 { + #[inline] + fn cache_hash(&self, state: &mut H) { + core::hash::Hash::hash(&self.to_bits(), state); + } +} + +// glam float vector/matrix types: hash each component via to_bits(). +macro_rules! impl_glam_array { + ($($t:ty),* $(,)?) => { + $( + impl CacheHash for $t { + #[inline] + fn cache_hash(&self, state: &mut H) { + for v in self.to_array() { + CacheHash::cache_hash(&v, state); + } + } + } + )* + }; +} + +macro_rules! impl_glam_cols { + ($($t:ty),* $(,)?) => { + $( + impl CacheHash for $t { + #[inline] + fn cache_hash(&self, state: &mut H) { + for v in self.to_cols_array() { + CacheHash::cache_hash(&v, state); + } + } + } + )* + }; +} + +impl_glam_array! { + glam::Vec2, glam::Vec3, glam::Vec3A, glam::Vec4, + glam::DVec2, glam::DVec3, glam::DVec4, +} + +impl_glam_cols! { + glam::Mat2, glam::Mat3, glam::Mat3A, glam::Mat4, + glam::DMat2, glam::DMat3, glam::DMat4, + glam::Affine2, glam::Affine3A, + glam::DAffine2, glam::DAffine3, +} + +// Quat / DQuat — to_array gives [x, y, z, w] as floats +impl_glam_array! { + glam::Quat, glam::DQuat, +} + +// Generic container impls. +impl CacheHash for Option { + #[inline] + fn cache_hash(&self, state: &mut H) { + match self { + None => core::hash::Hash::hash(&0u8, state), + Some(v) => { + core::hash::Hash::hash(&1u8, state); + v.cache_hash(state); + } + } + } +} + +impl CacheHash for [T] { + #[inline] + fn cache_hash(&self, state: &mut H) { + core::hash::Hash::hash(&self.len(), state); + for item in self { + item.cache_hash(state); + } + } +} + +impl CacheHash for [T; N] { + #[inline] + fn cache_hash(&self, state: &mut H) { + for item in self { + item.cache_hash(state); + } + } +} + +#[cfg(feature = "std")] +impl CacheHash for Vec { + #[inline] + fn cache_hash(&self, state: &mut H) { + self.as_slice().cache_hash(state); + } +} + +#[cfg(feature = "std")] +impl CacheHash for Box { + #[inline] + fn cache_hash(&self, state: &mut H) { + (**self).cache_hash(state); + } +} + +#[cfg(feature = "std")] +impl CacheHash for std::sync::Arc { + #[inline] + fn cache_hash(&self, state: &mut H) { + (**self).cache_hash(state); + } +} + +#[cfg(feature = "std")] +impl CacheHash for std::collections::BTreeMap { + #[inline] + fn cache_hash(&self, state: &mut H) { + core::hash::Hash::hash(&self.len(), state); + for (key, value) in self { + key.cache_hash(state); + value.cache_hash(state); + } + } +} + +#[cfg(feature = "std")] +impl CacheHash for std::collections::BTreeSet { + #[inline] + fn cache_hash(&self, state: &mut H) { + core::hash::Hash::hash(&self.len(), state); + for item in self { + item.cache_hash(state); + } + } +} + +impl CacheHash for &T { + #[inline] + fn cache_hash(&self, state: &mut H) { + (**self).cache_hash(state); + } +} + +// Tuple impls. +macro_rules! impl_tuple { + ($($T:ident),+) => { + impl<$($T: CacheHash),+> CacheHash for ($($T,)+) { + #[inline] + #[allow(non_snake_case)] + fn cache_hash(&self, state: &mut H) { + let ($($T,)+) = self; + $($T.cache_hash(state);)+ + } + } + }; +} + +impl_tuple!(A, B); +impl_tuple!(A, B, C); +impl_tuple!(A, B, C, D); +impl_tuple!(A, B, C, D, E); +impl_tuple!(A, B, C, D, E, F); diff --git a/node-graph/libraries/graphic-types/Cargo.toml b/node-graph/libraries/graphic-types/Cargo.toml index 1dac611f07..6140e239f3 100644 --- a/node-graph/libraries/graphic-types/Cargo.toml +++ b/node-graph/libraries/graphic-types/Cargo.toml @@ -18,6 +18,7 @@ wasm = [ [dependencies] # Local dependencies core-types = { workspace = true } +graphene-hash = { workspace = true } raster-types = { workspace = true, features = ["wgpu"] } vector-types = { workspace = true } node-macro = { workspace = true } diff --git a/node-graph/libraries/graphic-types/src/artboard.rs b/node-graph/libraries/graphic-types/src/artboard.rs index 7595f2cd52..d42c71842c 100644 --- a/node-graph/libraries/graphic-types/src/artboard.rs +++ b/node-graph/libraries/graphic-types/src/artboard.rs @@ -9,10 +9,10 @@ use core_types::transform::Transform; use core_types::uuid::NodeId; use dyn_any::DynAny; use glam::{DAffine2, DVec2, IVec2}; -use std::hash::Hash; +use graphene_hash::CacheHash; /// Some [`ArtboardData`] with some optional clipping bounds that can be exported. -#[derive(Clone, Debug, Hash, PartialEq, DynAny, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, CacheHash, PartialEq, DynAny, serde::Serialize, serde::Deserialize)] pub struct Artboard { pub content: Table, pub label: String, @@ -76,7 +76,7 @@ impl Transform for Artboard { pub fn migrate_artboard<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result, D::Error> { use serde::Deserialize; - #[derive(Clone, Default, Debug, Hash, PartialEq, DynAny, serde::Serialize, serde::Deserialize)] + #[derive(Clone, Default, Debug, PartialEq, DynAny, serde::Serialize, serde::Deserialize)] pub struct ArtboardGroup { pub artboards: Vec<(Artboard, Option)>, } diff --git a/node-graph/libraries/graphic-types/src/graphic.rs b/node-graph/libraries/graphic-types/src/graphic.rs index 878d702fdf..776d1ce316 100644 --- a/node-graph/libraries/graphic-types/src/graphic.rs +++ b/node-graph/libraries/graphic-types/src/graphic.rs @@ -1,6 +1,7 @@ use core_types::Color; use core_types::blending::AlphaBlending; use core_types::bounds::{BoundingBox, RenderBoundingBox}; +use core_types::graphene_hash::CacheHash; use core_types::ops::TableConvert; use core_types::render_complexity::RenderComplexity; use core_types::table::{Table, TableRow}; @@ -8,14 +9,13 @@ use core_types::uuid::NodeId; use dyn_any::DynAny; use glam::DAffine2; use raster_types::{CPU, GPU, Raster}; -use std::hash::Hash; use vector_types::GradientStops; // use vector_types::Vector; pub type Vector = vector_types::Vector>>; /// The possible forms of graphical content that can be rendered by the Render node into either an image or SVG syntax. -#[derive(Clone, Debug, Hash, PartialEq, DynAny, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, CacheHash, PartialEq, DynAny, serde::Serialize, serde::Deserialize)] pub enum Graphic { Graphic(Table), Vector(Table), diff --git a/node-graph/libraries/no-std-types/Cargo.toml b/node-graph/libraries/no-std-types/Cargo.toml index 10d8bf67e1..3258cc0328 100644 --- a/node-graph/libraries/no-std-types/Cargo.toml +++ b/node-graph/libraries/no-std-types/Cargo.toml @@ -14,6 +14,7 @@ license = "MIT OR Apache-2.0" # should be in this list instead of `[workspace.dependency]` std = [ "dep:dyn-any", + "dep:graphene-hash", "dep:serde", "dep:log", "glam/debug-glam-assert", @@ -32,6 +33,7 @@ node-macro = { workspace = true } # Local std dependencies dyn-any = { workspace = true, optional = true } +graphene-hash = { workspace = true, optional = true } # Workspace dependencies bytemuck = { workspace = true } diff --git a/node-graph/libraries/no-std-types/src/blending.rs b/node-graph/libraries/no-std-types/src/blending.rs index f6bb2965af..6033a7909f 100644 --- a/node-graph/libraries/no-std-types/src/blending.rs +++ b/node-graph/libraries/no-std-types/src/blending.rs @@ -1,12 +1,11 @@ use core::fmt::Display; -use core::hash::{Hash, Hasher}; use node_macro::BufferStruct; use num_enum::{FromPrimitive, IntoPrimitive}; #[cfg(not(feature = "std"))] use num_traits::float::Float; #[derive(Debug, Clone, Copy, PartialEq, BufferStruct)] -#[cfg_attr(feature = "std", derive(dyn_any::DynAny, serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "std", derive(dyn_any::DynAny, serde::Serialize, serde::Deserialize, graphene_hash::CacheHash))] #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] #[cfg_attr(feature = "std", serde(default))] pub struct AlphaBlending { @@ -20,14 +19,6 @@ impl Default for AlphaBlending { Self::new() } } -impl Hash for AlphaBlending { - fn hash(&self, state: &mut H) { - self.opacity.to_bits().hash(state); - self.fill.to_bits().hash(state); - self.blend_mode.hash(state); - self.clip.hash(state); - } -} impl Display for AlphaBlending { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { let round = |x: f32| (x * 1e3).round() / 1e3; @@ -71,6 +62,7 @@ impl AlphaBlending { #[repr(i32)] #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] #[cfg_attr(feature = "std", derive(dyn_any::DynAny, serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "std", derive(graphene_hash::CacheHash))] #[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash, BufferStruct, FromPrimitive, IntoPrimitive)] pub enum BlendMode { // Basic group diff --git a/node-graph/libraries/no-std-types/src/color/color_types.rs b/node-graph/libraries/no-std-types/src/color/color_types.rs index abcb1e9e86..86a4d50f58 100644 --- a/node-graph/libraries/no-std-types/src/color/color_types.rs +++ b/node-graph/libraries/no-std-types/src/color/color_types.rs @@ -2,7 +2,6 @@ use super::color_traits::{Alpha, AlphaMut, AssociatedAlpha, Luminance, Luminance use super::discrete_srgb::{float_to_srgb_u8, srgb_u8_to_float}; use bytemuck::{Pod, Zeroable}; use core::fmt::Debug; -use core::hash::Hash; use glam::Vec4; use half::f16; use node_macro::BufferStruct; @@ -220,6 +219,7 @@ impl Pixel for Luma {} #[repr(C)] #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] #[cfg_attr(feature = "std", derive(dyn_any::DynAny, serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "std", derive(graphene_hash::CacheHash))] #[derive(Debug, Default, Clone, Copy, Pod, Zeroable, BufferStruct)] pub struct Color { red: f32, @@ -236,16 +236,6 @@ impl PartialEq for Color { impl Eq for Color {} -#[allow(clippy::derived_hash_with_manual_eq)] -impl Hash for Color { - fn hash(&self, state: &mut H) { - self.red.to_bits().hash(state); - self.green.to_bits().hash(state); - self.blue.to_bits().hash(state); - self.alpha.to_bits().hash(state); - } -} - impl RGB for Color { type ColorChannel = f32; #[inline(always)] diff --git a/node-graph/libraries/raster-types/Cargo.toml b/node-graph/libraries/raster-types/Cargo.toml index 0039481667..c7377beb6e 100644 --- a/node-graph/libraries/raster-types/Cargo.toml +++ b/node-graph/libraries/raster-types/Cargo.toml @@ -14,6 +14,7 @@ wasm = ["core-types/wasm", "tsify", "wasm-bindgen"] [dependencies] # Local dependencies core-types = { workspace = true } +graphene-hash = { workspace = true } node-macro = { workspace = true } # Workspace dependencies diff --git a/node-graph/libraries/raster-types/src/image.rs b/node-graph/libraries/raster-types/src/image.rs index 420b2439af..85b57bc064 100644 --- a/node-graph/libraries/raster-types/src/image.rs +++ b/node-graph/libraries/raster-types/src/image.rs @@ -5,7 +5,6 @@ use core_types::Color; use core_types::color::float_to_srgb_u8; use core_types::table::{Table, TableRow}; // use crate::vector::Vector; // TODO: Check if Vector is actually used, if so handle differently -use core::hash::{Hash, Hasher}; use core_types::color::*; use dyn_any::{DynAny, StaticType}; use glam::{DAffine2, DVec2}; @@ -64,8 +63,10 @@ impl PartialEq for Image

{ #[derive(Debug, Clone, dyn_any::DynAny, Default, PartialEq, serde::Serialize, serde::Deserialize)] pub struct TransformImage(pub DAffine2); -impl Hash for TransformImage { - fn hash(&self, _: &mut H) {} +impl core_types::CacheHash for TransformImage { + fn cache_hash(&self, state: &mut H) { + core_types::CacheHash::cache_hash(&self.0, state); + } } impl std::fmt::Debug for Image

{ @@ -109,11 +110,11 @@ impl BitmapMut for Image

{ } } -impl Hash for Image

{ - fn hash(&self, state: &mut H) { - self.width.hash(state); - self.height.hash(state); - self.data.hash(state); +impl core_types::CacheHash for Image

{ + fn cache_hash(&self, state: &mut H) { + core_types::CacheHash::cache_hash(&self.width, state); + core_types::CacheHash::cache_hash(&self.height, state); + core_types::CacheHash::cache_hash(&self.data, state); } } @@ -220,7 +221,7 @@ impl IntoIterator for Image

{ pub fn migrate_image_frame<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result>, D::Error> { use serde::Deserialize; - #[derive(Clone, Debug, Hash, PartialEq, DynAny)] + #[derive(Clone, Debug, core_types::CacheHash, PartialEq, DynAny)] enum RasterFrame { ImageFrame(Table>), } @@ -237,7 +238,7 @@ pub fn migrate_image_frame<'de, D: serde::Deserializer<'de>>(deserializer: D) -> } } - #[derive(Clone, Debug, Hash, PartialEq, DynAny, serde::Serialize, serde::Deserialize)] + #[derive(Clone, Debug, core_types::CacheHash, PartialEq, DynAny, serde::Serialize, serde::Deserialize)] pub enum GraphicElement { GraphicGroup(Table), RasterFrame(RasterFrame), @@ -372,7 +373,7 @@ pub fn migrate_image_frame<'de, D: serde::Deserializer<'de>>(deserializer: D) -> pub fn migrate_image_frame_row<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result>, D::Error> { use serde::Deserialize; - #[derive(Clone, Debug, Hash, PartialEq, DynAny)] + #[derive(Clone, Debug, PartialEq, DynAny)] enum RasterFrame { /// A CPU-based bitmap image with a finite position and extent, equivalent to the SVG tag: https://developer.mozilla.org/en-US/docs/Web/SVG/Element/image ImageFrame(Table>), @@ -390,7 +391,7 @@ pub fn migrate_image_frame_row<'de, D: serde::Deserializer<'de>>(deserializer: D } } - #[derive(Clone, Debug, Hash, PartialEq, DynAny, serde::Serialize, serde::Deserialize)] + #[derive(Clone, Debug, PartialEq, DynAny, serde::Serialize, serde::Deserialize)] pub enum GraphicElement { /// Equivalent to the SVG tag: https://developer.mozilla.org/en-US/docs/Web/SVG/Element/g GraphicGroup(Table), diff --git a/node-graph/libraries/raster-types/src/raster_types.rs b/node-graph/libraries/raster-types/src/raster_types.rs index 918df0cec8..7372b3b76f 100644 --- a/node-graph/libraries/raster-types/src/raster_types.rs +++ b/node-graph/libraries/raster-types/src/raster_types.rs @@ -16,7 +16,7 @@ pub trait Storage: __private::Sealed + Clone + Debug + 'static { fn is_empty(&self) -> bool; } -#[derive(Clone, Debug, PartialEq, Hash, Default)] +#[derive(Clone, Debug, PartialEq, Default)] pub struct Raster where Raster: Storage, @@ -60,13 +60,23 @@ where } } +impl core_types::CacheHash for Raster +where + Raster: Storage, + T: core_types::CacheHash, +{ + fn cache_hash(&self, state: &mut H) { + core_types::CacheHash::cache_hash(&self.storage, state); + } +} + pub use cpu::CPU; mod cpu { use super::*; use crate::raster_types::__private::Sealed; - #[derive(Clone, Debug, Default, PartialEq, Hash, DynAny)] + #[derive(Clone, Debug, Default, PartialEq, core_types::CacheHash, DynAny)] pub struct CPU(Image); impl Sealed for Raster {} @@ -140,6 +150,13 @@ mod gpu { pub texture: wgpu::Texture, } + impl core_types::CacheHash for GPU { + fn cache_hash(&self, state: &mut H) { + use ::core::hash::Hash; + self.texture.hash(state); + } + } + impl Sealed for Raster {} impl Storage for Raster { @@ -164,7 +181,7 @@ mod gpu { use super::*; use crate::raster_types::__private::Sealed; - #[derive(Clone, Debug, PartialEq, Hash)] + #[derive(Clone, Debug, PartialEq, Hash, core_types::CacheHash)] pub struct GPU; impl Sealed for Raster {} diff --git a/node-graph/libraries/rendering/Cargo.toml b/node-graph/libraries/rendering/Cargo.toml index e33ce052fd..1926c39f0d 100644 --- a/node-graph/libraries/rendering/Cargo.toml +++ b/node-graph/libraries/rendering/Cargo.toml @@ -10,6 +10,7 @@ license = "MIT OR Apache-2.0" # Local dependencies dyn-any = { workspace = true } core-types = { workspace = true } +graphene-hash = { workspace = true } # Workspace dependencies glam = { workspace = true } diff --git a/node-graph/libraries/rendering/src/renderer.rs b/node-graph/libraries/rendering/src/renderer.rs index 6f1690ad37..4dc9d658c6 100644 --- a/node-graph/libraries/rendering/src/renderer.rs +++ b/node-graph/libraries/rendering/src/renderer.rs @@ -1,5 +1,6 @@ use crate::render_ext::RenderExt; use crate::to_peniko::BlendModeExt; +use core_types::CacheHash; use core_types::blending::BlendMode; use core_types::bounds::{BoundingBox, RenderBoundingBox}; use core_types::color::{Alpha, Color}; @@ -10,6 +11,7 @@ use core_types::transform::{Footprint, Transform}; use core_types::uuid::{NodeId, generate_uuid}; use dyn_any::DynAny; use glam::{DAffine2, DVec2}; +use graphene_hash::CacheHashWrapper; use graphic_types::Vector; use graphic_types::raster_types::{BitmapMut, CPU, GPU, Image, Raster}; use graphic_types::vector_types::gradient::{GradientStops, GradientType}; @@ -21,7 +23,6 @@ use kurbo::{Affine, Cap, Join, Shape}; use num_traits::Zero; use std::collections::{HashMap, HashSet}; use std::fmt::Write; -use std::hash::{Hash, Hasher}; use std::ops::Deref; use std::sync::{Arc, LazyLock}; use vector_types::gradient::GradientSpreadMethod; @@ -95,7 +96,7 @@ pub struct SvgRender { pub svg: Vec, pub svg_defs: String, pub transform: DAffine2, - pub image_data: HashMap, u64>, + pub image_data: HashMap>, u64>, indent: usize, } @@ -191,7 +192,7 @@ pub struct RenderContext { pub resource_overrides: Vec<(peniko::ImageBrush, wgpu::Texture)>, } -#[derive(Default, Clone, Copy, Hash)] +#[derive(Default, Clone, Copy, Hash, graphene_hash::CacheHash)] pub enum RenderOutputType { #[default] Svg, @@ -199,12 +200,13 @@ pub enum RenderOutputType { } /// Static state used whilst rendering -#[derive(Default, Clone)] +#[derive(Default, Clone, CacheHash)] pub struct RenderParams { pub render_mode: RenderMode, pub footprint: Footprint, /// Ratio of physical pixels to logical pixels. `scale := physical_pixels / logical_pixels` /// Ignored when rendering to SVG. + #[cache_hash(skip)] pub scale: f64, pub render_output_type: RenderOutputType, pub thumbnail: bool, @@ -223,25 +225,6 @@ pub struct RenderParams { pub viewport_zoom: f64, } -impl Hash for RenderParams { - fn hash(&self, state: &mut H) { - self.render_mode.hash(state); - self.footprint.hash(state); - self.render_output_type.hash(state); - self.thumbnail.hash(state); - self.hide_artboards.hash(state); - self.for_export.hash(state); - self.for_mask.hash(state); - if let Some(x) = self.alignment_parent_transform { - x.to_cols_array().iter().for_each(|x| x.to_bits().hash(state)) - } - self.aligned_strokes.hash(state); - self.override_paint_order.hash(state); - self.artboard_background.hash(state); - self.viewport_zoom.to_bits().hash(state); - } -} - impl RenderParams { pub fn for_clipper(&self) -> Self { Self { for_mask: true, ..*self } @@ -1426,7 +1409,7 @@ impl Render for Table> { if render_params.to_canvas() { let mut image_copy = image.clone(); image_copy.data_mut().map_pixels(|p| p.to_unassociated_alpha()); - let id = *render.image_data.entry(image_copy.into_data()).or_insert_with(generate_uuid); + let id = *render.image_data.entry(CacheHashWrapper(image_copy.into_data())).or_insert_with(generate_uuid); render.parent_tag( "foreignObject", diff --git a/node-graph/libraries/vector-types/Cargo.toml b/node-graph/libraries/vector-types/Cargo.toml index 5212fb2386..7b64a9c039 100644 --- a/node-graph/libraries/vector-types/Cargo.toml +++ b/node-graph/libraries/vector-types/Cargo.toml @@ -13,6 +13,7 @@ wasm = ["core-types/wasm", "tsify", "wasm-bindgen"] [dependencies] # Local dependencies core-types = { workspace = true } +graphene-hash = { workspace = true } node-macro = { workspace = true } # Workspace dependencies diff --git a/node-graph/libraries/vector-types/src/gradient.rs b/node-graph/libraries/vector-types/src/gradient.rs index f5c241c2f0..39e8d7701e 100644 --- a/node-graph/libraries/vector-types/src/gradient.rs +++ b/node-graph/libraries/vector-types/src/gradient.rs @@ -3,7 +3,7 @@ use dyn_any::DynAny; use glam::{DAffine2, DVec2}; #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Hash, serde::Serialize, serde::Deserialize, DynAny, node_macro::ChoiceType)] +#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Hash, graphene_hash::CacheHash, serde::Serialize, serde::Deserialize, DynAny, node_macro::ChoiceType)] #[widget(Radio)] pub enum GradientType { #[default] @@ -15,7 +15,7 @@ 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, serde::Serialize, DynAny)] +#[derive(Debug, Clone, PartialEq, graphene_hash::CacheHash, serde::Serialize, DynAny)] pub struct GradientStops { /// The position of this stop, a factor from 0-1 along the length of the full gradient. pub position: Vec, @@ -60,17 +60,6 @@ impl<'de> serde::Deserialize<'de> for GradientStops { } } -impl std::hash::Hash for GradientStops { - fn hash(&self, state: &mut H) { - self.position.len().hash(state); - for i in 0..self.position.len() { - self.position[i].to_bits().hash(state); - self.midpoint[i].to_bits().hash(state); - self.color[i].hash(state); - } - } -} - impl Default for GradientStops { fn default() -> Self { Self { @@ -336,7 +325,7 @@ impl GradientStops { #[repr(C)] #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Hash, serde::Serialize, serde::Deserialize, DynAny, node_macro::ChoiceType)] +#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Hash, graphene_hash::CacheHash, serde::Serialize, serde::Deserialize, DynAny, node_macro::ChoiceType)] #[widget(Radio)] pub enum GradientSpreadMethod { #[default] @@ -360,7 +349,7 @@ 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, serde::Serialize, serde::Deserialize, DynAny)] +#[derive(Debug, Clone, PartialEq, graphene_hash::CacheHash, serde::Serialize, serde::Deserialize, DynAny)] pub struct Gradient { pub stops: GradientStops, pub gradient_type: GradientType, @@ -382,21 +371,6 @@ impl Default for Gradient { } } -impl std::hash::Hash for Gradient { - fn hash(&self, state: &mut H) { - self.stops.len().hash(state); - [].iter() - .chain(self.start.to_array().iter()) - .chain(self.end.to_array().iter()) - .chain(self.stops.position.iter()) - .chain(self.stops.midpoint.iter()) - .for_each(|x| x.to_bits().hash(state)); - self.stops.color.iter().for_each(|color| color.hash(state)); - self.gradient_type.hash(state); - self.spread_method.hash(state); - } -} - impl std::fmt::Display for Gradient { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let round = |x: f64| (x * 1e3).round() / 1e3; diff --git a/node-graph/libraries/vector-types/src/subpath/mod.rs b/node-graph/libraries/vector-types/src/subpath/mod.rs index 1e50e32f40..80ea7cd241 100644 --- a/node-graph/libraries/vector-types/src/subpath/mod.rs +++ b/node-graph/libraries/vector-types/src/subpath/mod.rs @@ -13,7 +13,7 @@ use std::ops::{Index, IndexMut}; pub use structs::*; /// Structure used to represent a path composed of [Bezier] curves. -#[derive(Clone, PartialEq, Hash)] +#[derive(Clone, PartialEq, graphene_hash::CacheHash)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Subpath { manipulator_groups: Vec>, diff --git a/node-graph/libraries/vector-types/src/subpath/structs.rs b/node-graph/libraries/vector-types/src/subpath/structs.rs index 1498402292..11d72b0e2b 100644 --- a/node-graph/libraries/vector-types/src/subpath/structs.rs +++ b/node-graph/libraries/vector-types/src/subpath/structs.rs @@ -6,12 +6,12 @@ use std::fmt::{Debug, Formatter, Result}; use std::hash::Hash; /// An id type used for each [ManipulatorGroup]. -pub trait Identifier: Sized + Clone + PartialEq + Hash + 'static { +pub trait Identifier: Sized + Clone + PartialEq + Hash + graphene_hash::CacheHash + 'static { fn new() -> Self; } /// Structure used to represent a single anchor with up to two optional associated handles along a `Subpath` -#[derive(Copy, Clone, PartialEq)] +#[derive(Copy, Clone, PartialEq, graphene_hash::CacheHash)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct ManipulatorGroup { pub anchor: DVec2, @@ -20,22 +20,6 @@ pub struct ManipulatorGroup { pub id: PointId, } -// TODO: Remove once we no longer need to hash floats in Graphite -impl Hash for ManipulatorGroup { - fn hash(&self, state: &mut H) { - self.anchor.to_array().iter().for_each(|x| x.to_bits().hash(state)); - self.in_handle.is_some().hash(state); - if let Some(in_handle) = self.in_handle { - in_handle.to_array().iter().for_each(|x| x.to_bits().hash(state)); - } - self.out_handle.is_some().hash(state); - if let Some(out_handle) = self.out_handle { - out_handle.to_array().iter().for_each(|x| x.to_bits().hash(state)); - } - self.id.hash(state); - } -} - impl Debug for ManipulatorGroup { fn fmt(&self, f: &mut Formatter<'_>) -> Result { f.debug_struct("ManipulatorGroup") @@ -119,7 +103,7 @@ pub enum AppendType { SmoothJoin(f64), } -#[derive(Copy, Clone, Eq, PartialEq, Hash)] +#[derive(Copy, Clone, Eq, PartialEq, Hash, graphene_hash::CacheHash)] pub enum ArcType { Open, Closed, @@ -127,7 +111,7 @@ pub enum ArcType { } /// Representation of the handle point(s) in a bezier segment. -#[derive(Copy, Clone, PartialEq, Debug)] +#[derive(Copy, Clone, PartialEq, Debug, graphene_hash::CacheHash)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum BezierHandles { Linear, @@ -145,17 +129,6 @@ pub enum BezierHandles { }, } -impl std::hash::Hash for BezierHandles { - fn hash(&self, state: &mut H) { - std::mem::discriminant(self).hash(state); - match self { - BezierHandles::Linear => {} - BezierHandles::Quadratic { handle } => handle.to_array().map(|v| v.to_bits()).hash(state), - BezierHandles::Cubic { handle_start, handle_end } => [handle_start, handle_end].map(|handle| handle.to_array().map(|v| v.to_bits())).hash(state), - } - } -} - impl BezierHandles { pub fn is_cubic(&self) -> bool { matches!(self, Self::Cubic { .. }) diff --git a/node-graph/libraries/vector-types/src/vector/misc.rs b/node-graph/libraries/vector-types/src/vector/misc.rs index 7f5711da75..23cc4babd2 100644 --- a/node-graph/libraries/vector-types/src/vector/misc.rs +++ b/node-graph/libraries/vector-types/src/vector/misc.rs @@ -368,7 +368,7 @@ 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, Debug, DynAny, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Copy, PartialEq, Eq, Hash, graphene_hash::CacheHash, Debug, DynAny, serde::Serialize, serde::Deserialize)] pub enum ManipulatorPointId { /// A control anchor - the start or end point of a bézier. Anchor(PointId), @@ -479,7 +479,7 @@ impl ManipulatorPointId { } /// The type of handle found on a bézier curve. -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, DynAny, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, graphene_hash::CacheHash, Debug, DynAny, 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 +488,7 @@ pub enum HandleType { } /// Represents a primary or end handle found in a particular segment. -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, DynAny, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, graphene_hash::CacheHash, Debug, DynAny, serde::Serialize, serde::Deserialize)] pub struct HandleId { pub ty: HandleType, pub segment: SegmentId, @@ -572,3 +572,16 @@ pub enum InterpolationDistribution { /// All slants (changes in skew angle) between objects are covered at a constant rate, meaning more time is spent skewing through larger changes in slant. Slants, } + +graphene_hash::impl_via_hash!( + BooleanOperation, + CentroidType, + RowsOrColumns, + GridType, + ArcType, + MergeByDistanceAlgorithm, + ExtrudeJoiningAlgorithm, + PointSpacingType, + SpiralType, + InterpolationDistribution +); diff --git a/node-graph/libraries/vector-types/src/vector/reference_point.rs b/node-graph/libraries/vector-types/src/vector/reference_point.rs index 094155918c..728070a75a 100644 --- a/node-graph/libraries/vector-types/src/vector/reference_point.rs +++ b/node-graph/libraries/vector-types/src/vector/reference_point.rs @@ -2,7 +2,7 @@ use core_types::math::bbox::AxisAlignedBbox; use glam::DVec2; #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(Clone, Copy, Debug, Default, Hash, Eq, PartialEq, dyn_any::DynAny, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Copy, Debug, Default, Hash, graphene_hash::CacheHash, Eq, PartialEq, dyn_any::DynAny, serde::Serialize, serde::Deserialize)] pub enum ReferencePoint { #[default] None, diff --git a/node-graph/libraries/vector-types/src/vector/style.rs b/node-graph/libraries/vector-types/src/vector/style.rs index 0828c4e6f2..820a2b1414 100644 --- a/node-graph/libraries/vector-types/src/vector/style.rs +++ b/node-graph/libraries/vector-types/src/vector/style.rs @@ -16,7 +16,7 @@ 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, serde::Serialize, serde::Deserialize, DynAny, Hash)] +#[derive(Default, Debug, Clone, PartialEq, graphene_hash::CacheHash, serde::Serialize, serde::Deserialize, DynAny)] pub enum Fill { #[default] None, @@ -161,7 +161,7 @@ impl From 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, serde::Serialize, serde::Deserialize, DynAny, Hash)] +#[derive(Default, Debug, Clone, PartialEq, graphene_hash::CacheHash, serde::Serialize, serde::Deserialize, DynAny)] pub enum FillChoice { #[default] None, @@ -209,7 +209,7 @@ impl From for FillChoice { #[repr(C)] #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(Debug, Clone, Copy, Default, PartialEq, serde::Serialize, serde::Deserialize, DynAny, Hash, node_macro::ChoiceType)] +#[derive(Debug, Clone, Copy, Default, PartialEq, serde::Serialize, serde::Deserialize, DynAny, Hash, graphene_hash::CacheHash, node_macro::ChoiceType)] #[widget(Radio)] pub enum FillType { #[default] @@ -220,7 +220,7 @@ 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, DynAny, node_macro::ChoiceType)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, graphene_hash::CacheHash, DynAny, node_macro::ChoiceType)] #[widget(Radio)] pub enum StrokeCap { #[default] @@ -241,7 +241,7 @@ impl StrokeCap { #[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, serde::Serialize, serde::Deserialize, Hash, graphene_hash::CacheHash, DynAny, node_macro::ChoiceType)] #[widget(Radio)] pub enum StrokeJoin { #[default] @@ -262,7 +262,7 @@ impl StrokeJoin { #[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, serde::Serialize, serde::Deserialize, Hash, graphene_hash::CacheHash, DynAny, node_macro::ChoiceType)] #[widget(Radio)] pub enum StrokeAlign { #[default] @@ -279,7 +279,7 @@ impl StrokeAlign { #[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, serde::Serialize, serde::Deserialize, Hash, graphene_hash::CacheHash, DynAny, node_macro::ChoiceType)] #[widget(Radio)] pub enum PaintOrder { #[default] @@ -299,7 +299,7 @@ fn daffine2_identity() -> DAffine2 { #[repr(C)] #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, DynAny)] +#[derive(Debug, Clone, PartialEq, graphene_hash::CacheHash, serde::Serialize, serde::Deserialize, DynAny)] #[serde(default)] pub struct Stroke { /// Stroke color @@ -322,24 +322,6 @@ pub struct Stroke { pub paint_order: PaintOrder, } -impl std::hash::Hash for Stroke { - fn hash(&self, state: &mut H) { - self.color.hash(state); - self.weight.to_bits().hash(state); - { - self.dash_lengths.len().hash(state); - self.dash_lengths.iter().for_each(|length| length.to_bits().hash(state)); - } - self.dash_offset.to_bits().hash(state); - self.cap.hash(state); - self.join.hash(state); - self.join_miter_limit.to_bits().hash(state); - self.align.hash(state); - self.transform.to_cols_array().iter().for_each(|x| x.to_bits().hash(state)); - self.paint_order.hash(state); - } -} - impl Stroke { pub const fn new(color: Option, weight: f64) -> Self { Self { @@ -512,19 +494,12 @@ impl Default for Stroke { #[repr(C)] #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize, DynAny)] +#[derive(Debug, Clone, PartialEq, Default, graphene_hash::CacheHash, serde::Serialize, serde::Deserialize, DynAny)] pub struct PathStyle { pub stroke: Option, pub fill: Fill, } -impl std::hash::Hash for PathStyle { - fn hash(&self, state: &mut H) { - self.stroke.hash(state); - self.fill.hash(state); - } -} - impl std::fmt::Display for PathStyle { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let fill = &self.fill; @@ -680,7 +655,7 @@ 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, DynAny)] +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, graphene_hash::CacheHash, DynAny)] pub enum RenderMode { /// Render with normal coloration at the current viewport resolution #[default] diff --git a/node-graph/libraries/vector-types/src/vector/vector_attributes.rs b/node-graph/libraries/vector-types/src/vector/vector_attributes.rs index a2f8edd188..063b88f767 100644 --- a/node-graph/libraries/vector-types/src/vector/vector_attributes.rs +++ b/node-graph/libraries/vector-types/src/vector/vector_attributes.rs @@ -13,7 +13,7 @@ use std::iter::zip; macro_rules! create_ids { ($($id:ident),*) => { $( - #[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Ord, Eq, Hash, DynAny)] + #[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Ord, Eq, Hash, graphene_hash::CacheHash, DynAny)] #[derive(serde::Serialize, serde::Deserialize)] /// A strongly typed ID pub struct $id(u64); @@ -79,7 +79,7 @@ impl std::hash::BuildHasher for NoHashBuilder { } } -#[derive(Clone, Debug, Default, PartialEq, DynAny, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, Default, PartialEq, graphene_hash::CacheHash, DynAny, 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, @@ -87,13 +87,6 @@ pub struct PointDomain { pub(crate) position: Vec, } -impl Hash for PointDomain { - fn hash(&self, state: &mut H) { - self.id.hash(state); - self.position.iter().for_each(|pos| pos.to_array().map(|v| v.to_bits()).hash(state)); - } -} - impl PointDomain { pub const fn new() -> Self { Self { id: Vec::new(), position: Vec::new() } @@ -212,7 +205,7 @@ impl PointDomain { } } -#[derive(Clone, Debug, Default, PartialEq, Hash, DynAny, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, Default, PartialEq, graphene_hash::CacheHash, DynAny, 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")] @@ -594,7 +587,7 @@ impl SegmentDomain { } } -#[derive(Clone, Debug, Default, PartialEq, Hash, DynAny, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, Default, PartialEq, Hash, graphene_hash::CacheHash, DynAny, 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 { @@ -849,7 +842,7 @@ struct Faces { face_start: Vec, } -#[derive(Debug, Clone, PartialEq, Hash)] +#[derive(Debug, Clone, PartialEq)] pub struct FaceIterator<'a, Upstream> { vector: &'a Vector, faces: Faces, diff --git a/node-graph/libraries/vector-types/src/vector/vector_modification.rs b/node-graph/libraries/vector-types/src/vector/vector_modification.rs index f9d094223f..45f015855e 100644 --- a/node-graph/libraries/vector-types/src/vector/vector_modification.rs +++ b/node-graph/libraries/vector-types/src/vector/vector_modification.rs @@ -1,26 +1,18 @@ use super::*; use crate::subpath::BezierHandles; use crate::vector::misc::{HandleId, HandleType, point_to_dvec2}; -use core_types::uuid::generate_uuid; use dyn_any::DynAny; use glam::DVec2; use kurbo::{BezPath, PathEl, Point}; -use std::collections::{HashMap, HashSet}; -use std::hash::BuildHasher; +use std::collections::{BTreeMap, BTreeSet, HashSet}; /// Represents a procedural change to the [`PointDomain`] in [`Vector`]. -#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, Default, PartialEq, graphene_hash::CacheHash, serde::Serialize, serde::Deserialize)] pub struct PointModification { add: Vec, - remove: HashSet, - #[serde(serialize_with = "serialize_hashmap", deserialize_with = "deserialize_hashmap")] - delta: HashMap, -} - -impl Hash for PointModification { - fn hash(&self, state: &mut H) { - generate_uuid().hash(state) - } + remove: BTreeSet, + #[serde(deserialize_with = "deserialize_btreemap")] + delta: BTreeMap, } impl PointModification { @@ -62,7 +54,7 @@ impl PointModification { pub fn create_from_vector(vector: &Vector) -> Self { Self { add: vector.point_domain.ids().to_vec(), - remove: HashSet::new(), + remove: BTreeSet::new(), delta: vector.point_domain.ids().iter().copied().zip(vector.point_domain.positions().iter().cloned()).collect(), } } @@ -80,20 +72,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, graphene_hash::CacheHash, serde::Serialize, serde::Deserialize)] pub struct SegmentModification { add: Vec, - remove: HashSet, - #[serde(serialize_with = "serialize_hashmap", deserialize_with = "deserialize_hashmap")] - start_point: HashMap, - #[serde(serialize_with = "serialize_hashmap", deserialize_with = "deserialize_hashmap")] - end_point: HashMap, - #[serde(serialize_with = "serialize_hashmap", deserialize_with = "deserialize_hashmap")] - handle_primary: HashMap>, - #[serde(serialize_with = "serialize_hashmap", deserialize_with = "deserialize_hashmap")] - handle_end: HashMap>, - #[serde(serialize_with = "serialize_hashmap", deserialize_with = "deserialize_hashmap")] - stroke: HashMap, + remove: BTreeSet, + #[serde(deserialize_with = "deserialize_btreemap")] + start_point: BTreeMap, + #[serde(deserialize_with = "deserialize_btreemap")] + end_point: BTreeMap, + #[serde(deserialize_with = "deserialize_btreemap")] + handle_primary: BTreeMap>, + #[serde(deserialize_with = "deserialize_btreemap")] + handle_end: BTreeMap>, + #[serde(deserialize_with = "deserialize_btreemap")] + stroke: BTreeMap, } impl SegmentModification { @@ -219,7 +211,7 @@ impl SegmentModification { let point_id = |(&segment, &index)| (segment, vector.point_domain.ids()[index]); Self { add: vector.segment_domain.ids().to_vec(), - remove: HashSet::new(), + remove: BTreeSet::new(), start_point: vector.segment_domain.ids().iter().zip(vector.segment_domain.start_point()).map(point_id).collect(), end_point: vector.segment_domain.ids().iter().zip(vector.segment_domain.end_point()).map(point_id).collect(), handle_primary: vector.segment_bezier_iter().map(|(id, b, _, _)| (id, b.handle_start().map(|handle| handle - b.start))).collect(), @@ -250,14 +242,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, graphene_hash::CacheHash, serde::Serialize, serde::Deserialize)] pub struct RegionModification { add: Vec, - remove: HashSet, - #[serde(serialize_with = "serialize_hashmap", deserialize_with = "deserialize_hashmap")] - segment_range: HashMap>, - #[serde(serialize_with = "serialize_hashmap", deserialize_with = "deserialize_hashmap")] - fill: HashMap, + remove: BTreeSet, + #[serde(deserialize_with = "deserialize_btreemap")] + segment_range: BTreeMap>, + #[serde(deserialize_with = "deserialize_btreemap")] + fill: BTreeMap, } impl RegionModification { @@ -286,7 +278,7 @@ impl RegionModification { pub fn create_from_vector(vector: &Vector) -> Self { Self { add: vector.region_domain.ids().to_vec(), - remove: HashSet::new(), + remove: BTreeSet::new(), segment_range: vector.region_domain.ids().iter().copied().zip(vector.region_domain.segment_range().iter().cloned()).collect(), fill: vector.region_domain.ids().iter().copied().zip(vector.region_domain.fill().iter().cloned()).collect(), } @@ -294,13 +286,13 @@ impl RegionModification { } /// Represents a procedural change to the [`Vector`]. -#[derive(Clone, Debug, Default, PartialEq, DynAny, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, Default, PartialEq, graphene_hash::CacheHash, DynAny, serde::Serialize, serde::Deserialize)] pub struct VectorModification { points: PointModification, segments: SegmentModification, regions: RegionModification, - add_g1_continuous: HashSet<[HandleId; 2]>, - remove_g1_continuous: HashSet<[HandleId; 2]>, + add_g1_continuous: BTreeSet<[HandleId; 2]>, + remove_g1_continuous: BTreeSet<[HandleId; 2]>, } /// A modification type that can be added to a [`VectorModification`]. @@ -506,25 +498,70 @@ impl VectorModification { segments: SegmentModification::create_from_vector(vector), regions: RegionModification::create_from_vector(vector), add_g1_continuous: vector.colinear_manipulators.iter().copied().collect(), - remove_g1_continuous: HashSet::new(), + remove_g1_continuous: BTreeSet::new(), } } } -impl Hash for VectorModification { - fn hash(&self, state: &mut H) { - generate_uuid().hash(state) - } -} - -// Do we want to enforce that all serialized/deserialized hashmaps are a vec of tuples? -// TODO: Eventually remove this document upgrade code -use serde::de::{SeqAccess, Visitor}; +// Deserializes a BTreeMap from either a sequence of tuples (old document format) or a standard map. +// TODO: Eventually remove this document upgrade code (the sequence-of-tuples path) +use serde::de::{MapAccess, SeqAccess, Visitor}; use serde::ser::SerializeSeq; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use std::fmt; -use std::hash::Hash; -pub fn serialize_hashmap(hashmap: &HashMap, serializer: S) -> Result +use std::hash::{BuildHasher, Hash}; +fn deserialize_btreemap<'de, K, V, D>(deserializer: D) -> Result, D::Error> +where + K: Deserialize<'de> + Ord, + V: Deserialize<'de>, + D: Deserializer<'de>, +{ + struct BTreeMapVisitor { + marker: std::marker::PhantomData BTreeMap>, + } + + impl<'de, K, V> Visitor<'de> for BTreeMapVisitor + where + K: Deserialize<'de> + Ord, + V: Deserialize<'de>, + { + type Value = BTreeMap; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a map or a sequence of key-value tuples") + } + + // Old document format: serialized as a sequence of (key, value) tuples + fn visit_seq(self, mut seq: A) -> Result + where + A: SeqAccess<'de>, + { + let mut map = BTreeMap::new(); + while let Some((key, value)) = seq.next_element()? { + map.insert(key, value); + } + Ok(map) + } + + // New format: standard map serialization from BTreeMap + fn visit_map(self, mut map_access: A) -> Result + where + A: MapAccess<'de>, + { + let mut map = BTreeMap::new(); + while let Some((key, value)) = map_access.next_entry()? { + map.insert(key, value); + } + Ok(map) + } + } + + deserializer.deserialize_any(BTreeMapVisitor { marker: std::marker::PhantomData }) +} + +// Serializes a HashMap as a sequence of (key, value) tuples for stable document serialization. +// Used externally by graph-craft and the editor for HashMap fields in serialized documents. +pub fn serialize_hashmap(hashmap: &std::collections::HashMap, serializer: S) -> Result where K: Serialize + Eq + Hash, V: Serialize, @@ -538,7 +575,7 @@ where seq.end() } -pub fn deserialize_hashmap<'de, K, V, D, H>(deserializer: D) -> Result, D::Error> +pub fn deserialize_hashmap<'de, K, V, D, H>(deserializer: D) -> Result, D::Error> where K: Deserialize<'de> + Eq + Hash, V: Deserialize<'de>, @@ -547,7 +584,7 @@ where { struct HashMapVisitor { #[allow(clippy::type_complexity)] - marker: std::marker::PhantomData HashMap>, + marker: std::marker::PhantomData std::collections::HashMap>, } impl<'de, K, V, H> Visitor<'de> for HashMapVisitor @@ -556,7 +593,7 @@ where V: Deserialize<'de>, H: BuildHasher + Default, { - type Value = HashMap; + type Value = std::collections::HashMap; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str("a sequence of tuples") @@ -566,7 +603,7 @@ where where A: SeqAccess<'de>, { - let mut hashmap = HashMap::default(); + let mut hashmap = std::collections::HashMap::default(); while let Some((key, value)) = seq.next_element()? { hashmap.insert(key, value); } diff --git a/node-graph/libraries/vector-types/src/vector/vector_types.rs b/node-graph/libraries/vector-types/src/vector/vector_types.rs index e9b8bbb670..e85d707657 100644 --- a/node-graph/libraries/vector-types/src/vector/vector_types.rs +++ b/node-graph/libraries/vector-types/src/vector/vector_types.rs @@ -54,13 +54,13 @@ impl Default for Vector { } } -impl std::hash::Hash for Vector { - fn hash(&self, state: &mut H) { - self.point_domain.hash(state); - self.segment_domain.hash(state); - self.region_domain.hash(state); - self.style.hash(state); - self.colinear_manipulators.hash(state); +impl graphene_hash::CacheHash for Vector { + fn cache_hash(&self, state: &mut H) { + self.point_domain.cache_hash(state); + self.segment_domain.cache_hash(state); + self.region_domain.cache_hash(state); + self.style.cache_hash(state); + self.colinear_manipulators.cache_hash(state); // We don't hash the upstream_data intentionally } } diff --git a/node-graph/nodes/brush/Cargo.toml b/node-graph/nodes/brush/Cargo.toml index 59e0852567..372edf0daa 100644 --- a/node-graph/nodes/brush/Cargo.toml +++ b/node-graph/nodes/brush/Cargo.toml @@ -14,6 +14,7 @@ serde = ["dep:serde"] # Local dependencies dyn-any = { workspace = true } core-types = { workspace = true } +graphene-hash = { workspace = true } raster-types = { workspace = true } raster-nodes = { workspace = true } node-macro = { workspace = true } diff --git a/node-graph/nodes/brush/src/brush_cache.rs b/node-graph/nodes/brush/src/brush_cache.rs index f90618830b..d05fa5c10d 100644 --- a/node-graph/nodes/brush/src/brush_cache.rs +++ b/node-graph/nodes/brush/src/brush_cache.rs @@ -1,5 +1,6 @@ use crate::brush_stroke::BrushStroke; use crate::brush_stroke::BrushStyle; +use core_types::graphene_hash::CacheHashWrapper; use core_types::table::TableRow; use dyn_any::DynAny; use raster_types::CPU; @@ -31,7 +32,7 @@ struct BrushCacheImpl { // A cache for brush textures. #[serde(skip)] - brush_texture_cache: HashMap>, + brush_texture_cache: HashMap, Raster>, } impl BrushCacheImpl { @@ -165,6 +166,12 @@ impl Hash for BrushCache { } } +impl graphene_hash::CacheHash for BrushCache { + fn cache_hash(&self, state: &mut H) { + core::hash::Hash::hash(&self.0.lock().unwrap().unique_id, state); + } +} + impl BrushCache { pub fn compute_brush_plan(&self, background: TableRow>, input: &[BrushStroke]) -> BrushPlan { let mut inner = self.0.lock().unwrap(); @@ -178,11 +185,11 @@ impl BrushCache { pub fn get_cached_brush(&self, style: &BrushStyle) -> Option> { let inner = self.0.lock().unwrap(); - inner.brush_texture_cache.get(style).cloned() + inner.brush_texture_cache.get(&CacheHashWrapper(style.clone())).cloned() } pub fn store_brush(&self, style: BrushStyle, brush: Raster) { let mut inner = self.0.lock().unwrap(); - inner.brush_texture_cache.insert(style, brush); + inner.brush_texture_cache.insert(CacheHashWrapper(style), brush); } } diff --git a/node-graph/nodes/brush/src/brush_stroke.rs b/node-graph/nodes/brush/src/brush_stroke.rs index c69360148d..9ad19dbb33 100644 --- a/node-graph/nodes/brush/src/brush_stroke.rs +++ b/node-graph/nodes/brush/src/brush_stroke.rs @@ -1,12 +1,11 @@ +use core_types::CacheHash; use core_types::blending::BlendMode; use core_types::color::Color; use core_types::math::bbox::AxisAlignedBbox; use dyn_any::DynAny; use glam::DVec2; -use std::hash::{Hash, Hasher}; - /// The style of a brush. -#[derive(Clone, Debug, DynAny, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, CacheHash, DynAny, serde::Serialize, serde::Deserialize)] pub struct BrushStyle { pub color: Color, pub diameter: f64, @@ -29,17 +28,6 @@ impl Default for BrushStyle { } } -impl Hash for BrushStyle { - fn hash(&self, state: &mut H) { - self.color.hash(state); - self.diameter.to_bits().hash(state); - self.hardness.to_bits().hash(state); - self.flow.to_bits().hash(state); - self.spacing.to_bits().hash(state); - self.blend_mode.hash(state); - } -} - impl Eq for BrushStyle {} impl PartialEq for BrushStyle { @@ -54,23 +42,13 @@ impl PartialEq for BrushStyle { } /// A single sample of brush parameters across the brush stroke. -#[derive(Clone, Debug, PartialEq, DynAny, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, core_types::CacheHash, DynAny, serde::Serialize, serde::Deserialize)] pub struct BrushInputSample { - // The position of the sample in layer space, in pixels. - // The origin of layer space is not specified. pub position: DVec2, - // Future work: pressure, stylus angle, etc. -} - -impl Hash for BrushInputSample { - fn hash(&self, state: &mut H) { - self.position.x.to_bits().hash(state); - self.position.y.to_bits().hash(state); - } } /// The parameters for a single stroke brush. -#[derive(Clone, Debug, PartialEq, Hash, Default, DynAny, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, core_types::CacheHash, Default, DynAny, serde::Serialize, serde::Deserialize)] pub struct BrushStroke { pub style: BrushStyle, pub trace: Vec, diff --git a/node-graph/nodes/gcore/Cargo.toml b/node-graph/nodes/gcore/Cargo.toml index 740a021b59..31afd2f065 100644 --- a/node-graph/nodes/gcore/Cargo.toml +++ b/node-graph/nodes/gcore/Cargo.toml @@ -19,6 +19,7 @@ wasm = [ [dependencies] # Local dependencies core-types = { workspace = true } +graphene-hash = { workspace = true } raster-types = { workspace = true } graphic-types = { workspace = true } node-macro = { workspace = true } diff --git a/node-graph/nodes/gcore/src/animation.rs b/node-graph/nodes/gcore/src/animation.rs index 50c682d0b3..ffc9ed00ed 100644 --- a/node-graph/nodes/gcore/src/animation.rs +++ b/node-graph/nodes/gcore/src/animation.rs @@ -1,7 +1,7 @@ use core_types::table::Table; use core_types::transform::Footprint; use core_types::uuid::NodeId; -use core_types::{CloneVarArgs, Color, Context, Ctx, ExtractAll, ExtractAnimationTime, ExtractPointerPosition, ExtractRealTime, OwnedContextImpl}; +use core_types::{CacheHash, CloneVarArgs, Color, Context, Ctx, ExtractAll, ExtractAnimationTime, ExtractPointerPosition, ExtractRealTime, OwnedContextImpl}; use glam::{DAffine2, DVec2}; use graphic_types::vector_types::GradientStops; use graphic_types::{Artboard, Graphic, Vector}; @@ -9,7 +9,7 @@ use raster_types::{CPU, GPU, Raster}; const DAY: f64 = 1000. * 3600. * 24.; -#[derive(Debug, Clone, Copy, PartialEq, Eq, dyn_any::DynAny, Default, Hash, node_macro::ChoiceType, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, dyn_any::DynAny, Default, Hash, CacheHash, node_macro::ChoiceType, serde::Serialize, serde::Deserialize)] pub enum RealTimeMode { #[label("UTC")] Utc, diff --git a/node-graph/nodes/gcore/src/extract_xy.rs b/node-graph/nodes/gcore/src/extract_xy.rs index ffe13e26f2..6f686dd512 100644 --- a/node-graph/nodes/gcore/src/extract_xy.rs +++ b/node-graph/nodes/gcore/src/extract_xy.rs @@ -1,4 +1,4 @@ -use core_types::Ctx; +use core_types::{CacheHash, Ctx}; use dyn_any::DynAny; use glam::{DVec2, IVec2, UVec2}; @@ -15,7 +15,7 @@ fn extract_xy>(_: impl Ctx, #[implementations(DVec2, IVec2, UVec2 /// The X or Y component of a vec2. #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, CacheHash, DynAny, node_macro::ChoiceType, serde::Serialize, serde::Deserialize)] #[widget(Radio)] pub enum XY { #[default] diff --git a/node-graph/nodes/gcore/src/memo.rs b/node-graph/nodes/gcore/src/memo.rs index e57d7755b9..eaacfc0ebd 100644 --- a/node-graph/nodes/gcore/src/memo.rs +++ b/node-graph/nodes/gcore/src/memo.rs @@ -1,7 +1,8 @@ use core_types::WasmNotSend; +use core_types::graphene_hash::CacheHash; use core_types::memo::*; use std::hash::DefaultHasher; -use std::hash::{Hash, Hasher}; +use std::hash::Hasher; use std::sync::Arc; use std::sync::Mutex; @@ -13,9 +14,9 @@ use std::sync::Mutex; /// /// Currently, only one input-output pair is cached. Subsequent calls with different inputs will overwrite the previous cache. #[node_macro::node(category(""), path(graphene_core::memo), skip_impl)] -async fn memo(input: I, #[data] cache: Arc>>, node: impl Node) -> T { +async fn memo(input: I, #[data] cache: Arc>>, node: impl Node) -> T { let mut hasher = DefaultHasher::new(); - input.hash(&mut hasher); + input.cache_hash(&mut hasher); let hash = hasher.finish(); if let Some(data) = cache.lock().as_ref().unwrap().as_ref().and_then(|data| (data.0 == hash).then_some(data.1.clone())) { diff --git a/node-graph/nodes/graphic/src/graphic.rs b/node-graph/nodes/graphic/src/graphic.rs index c370b25a5f..8a6c72e8f9 100644 --- a/node-graph/nodes/graphic/src/graphic.rs +++ b/node-graph/nodes/graphic/src/graphic.rs @@ -80,7 +80,7 @@ pub fn omit_element( } #[node_macro::node(category("General"))] -async fn map( +async fn map( ctx: impl Ctx + CloneVarArgs + ExtractAll, #[implementations( Table, diff --git a/node-graph/nodes/gstd/src/render_node.rs b/node-graph/nodes/gstd/src/render_node.rs index 734420732c..7d8bcd9387 100644 --- a/node-graph/nodes/gstd/src/render_node.rs +++ b/node-graph/nodes/gstd/src/render_node.rs @@ -21,7 +21,7 @@ use wgpu_executor::RenderContext; pub use crate::render_cache::render_output_cache; /// List of (canvas id, image data) pairs for embedding images as canvases in the final SVG string. -type ImageData = HashMap, u64>; +type ImageData = HashMap>, u64>; #[derive(Clone, dyn_any::DynAny)] pub enum RenderIntermediateType { @@ -191,7 +191,7 @@ async fn render<'a: 'n>(ctx: impl Ctx + ExtractFootprint + ExtractVarArgs, edito rendering.wrap_with_transform(footprint.transform, Some(logical_resolution)); RenderOutputType::Svg { svg: rendering.svg.to_svg_string(), - image_data: rendering.image_data.into_iter().map(|(image, id)| (id, image)).collect(), + image_data: rendering.image_data.into_iter().map(|(image, id)| (id, image.0)).collect(), } } (RenderOutputTypeRequest::Vello, RenderIntermediateType::Vello(vello_data)) => { diff --git a/node-graph/nodes/raster/Cargo.toml b/node-graph/nodes/raster/Cargo.toml index 55c7e06a03..781131ee3b 100644 --- a/node-graph/nodes/raster/Cargo.toml +++ b/node-graph/nodes/raster/Cargo.toml @@ -15,6 +15,7 @@ shader-nodes = ["std", "dep:raster-nodes-shaders", "dep:wgpu-executor"] std = [ "dep:core-types", "dep:dyn-any", + "dep:graphene-hash", "dep:raster-types", "dep:vector-types", "dep:image", @@ -41,6 +42,7 @@ node-macro = { workspace = true } # Local std dependencies dyn-any = { workspace = true, optional = true } core-types = { workspace = true, optional = true } +graphene-hash = { workspace = true, optional = true } raster-types = { workspace = true, optional = true } vector-types = { workspace = true, optional = true } wgpu-executor = { workspace = true, optional = true } diff --git a/node-graph/nodes/raster/src/adjustments.rs b/node-graph/nodes/raster/src/adjustments.rs index 2dd182c06a..f6a9666950 100644 --- a/node-graph/nodes/raster/src/adjustments.rs +++ b/node-graph/nodes/raster/src/adjustments.rs @@ -1015,3 +1015,20 @@ fn exposure>( }); input } + +#[cfg(feature = "std")] +mod _graphene_hash_impls { + use super::{CellularDistanceFunction, CellularReturnType, DomainWarpType, FractalType, LuminanceCalculation, NoiseType, RedGreenBlue, RedGreenBlueAlpha, RelativeAbsolute, SelectiveColorChoice}; + graphene_hash::impl_via_hash!( + LuminanceCalculation, + RedGreenBlue, + RedGreenBlueAlpha, + NoiseType, + FractalType, + CellularDistanceFunction, + CellularReturnType, + DomainWarpType, + RelativeAbsolute, + SelectiveColorChoice + ); +} diff --git a/node-graph/nodes/raster/src/curve.rs b/node-graph/nodes/raster/src/curve.rs index 2ba1d84cbf..6e9ffac4cf 100644 --- a/node-graph/nodes/raster/src/curve.rs +++ b/node-graph/nodes/raster/src/curve.rs @@ -1,11 +1,10 @@ use core_types::Node; use core_types::color::{Channel, Linear, LuminanceMut}; use dyn_any::{DynAny, StaticType, StaticTypeSized}; -use std::hash::{Hash, Hasher}; use std::ops::{Add, Mul, Sub}; #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(Debug, Clone, PartialEq, DynAny, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, PartialEq, core_types::CacheHash, DynAny, serde::Serialize, serde::Deserialize)] pub struct Curve { #[serde(rename = "manipulatorGroups")] pub manipulator_groups: Vec, @@ -25,28 +24,13 @@ impl Default for Curve { } } -impl Hash for Curve { - fn hash(&self, state: &mut H) { - self.manipulator_groups.hash(state); - [self.first_handle, self.last_handle].iter().flatten().for_each(|f| f.to_bits().hash(state)); - } -} - #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(Debug, Clone, Copy, PartialEq, DynAny, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, core_types::CacheHash, DynAny, serde::Serialize, serde::Deserialize)] pub struct CurveManipulatorGroup { pub anchor: [f32; 2], pub handles: [[f32; 2]; 2], } -impl Hash for CurveManipulatorGroup { - fn hash(&self, state: &mut H) { - for c in self.handles.iter().chain([&self.anchor]).flatten() { - c.to_bits().hash(state); - } - } -} - pub struct ValueMapperNode { lut: Vec, } diff --git a/node-graph/nodes/text/Cargo.toml b/node-graph/nodes/text/Cargo.toml index e5558c741d..adf8a467c8 100644 --- a/node-graph/nodes/text/Cargo.toml +++ b/node-graph/nodes/text/Cargo.toml @@ -13,6 +13,7 @@ wasm = ["core-types/wasm", "tsify", "wasm-bindgen"] [dependencies] # Local dependencies core-types = { workspace = true } +graphene-hash = { workspace = true } raster-types = { workspace = true } vector-types = { workspace = true } node-macro = { workspace = true } diff --git a/node-graph/nodes/text/src/font_cache.rs b/node-graph/nodes/text/src/font_cache.rs index 58111bda21..9a07e3c599 100644 --- a/node-graph/nodes/text/src/font_cache.rs +++ b/node-graph/nodes/text/src/font_cache.rs @@ -1,3 +1,4 @@ +use core_types::graphene_hash::CacheHash; use dyn_any::DynAny; use parley::fontique::Blob; use std::collections::HashMap; @@ -23,6 +24,14 @@ impl std::hash::Hash for Font { } } +impl CacheHash for Font { + fn cache_hash(&self, state: &mut H) { + self.font_family.cache_hash(state); + self.font_style.cache_hash(state); + // Don't consider `font_style_to_restore` in the HashMaps + } +} + impl PartialEq for Font { fn eq(&self, other: &Self) -> bool { // Don't consider `font_style_to_restore` in the HashMaps diff --git a/node-graph/nodes/text/src/lib.rs b/node-graph/nodes/text/src/lib.rs index e877c1b89b..55640e1cf7 100644 --- a/node-graph/nodes/text/src/lib.rs +++ b/node-graph/nodes/text/src/lib.rs @@ -7,6 +7,7 @@ mod to_path; use convert_case::{Boundary, Converter, pattern}; use core_types::Color; +use core_types::graphene_hash::CacheHash; use core_types::registry::types::{SignedInteger, TextArea}; use core_types::table::Table; use core_types::{CloneVarArgs, Context, Ctx, ExtractAll, ExtractVarArgs, OwnedContextImpl}; @@ -25,7 +26,7 @@ pub use vector_types; /// Alignment of lines of type within a text block. #[repr(C)] #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, node_macro::ChoiceType)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, CacheHash, DynAny, node_macro::ChoiceType)] #[widget(Radio)] pub enum TextAlign { #[default] @@ -116,7 +117,7 @@ fn escape_string(input: String) -> String { result } -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, dyn_any::DynAny, node_macro::ChoiceType, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, CacheHash, dyn_any::DynAny, node_macro::ChoiceType, serde::Serialize, serde::Deserialize)] #[widget(Dropdown)] pub enum StringCapitalization { /// "on the origin of species" — Converts all letters to lower case. diff --git a/node-graph/nodes/vector/Cargo.toml b/node-graph/nodes/vector/Cargo.toml index bd976b84e7..3f5df26780 100644 --- a/node-graph/nodes/vector/Cargo.toml +++ b/node-graph/nodes/vector/Cargo.toml @@ -13,6 +13,7 @@ wasm = ["core-types/wasm", "tsify", "wasm-bindgen"] [dependencies] # Local dependencies core-types = { workspace = true } +graphene-hash = { workspace = true } vector-types = { workspace = true } graphic-types = { workspace = true } node-macro = { workspace = true } diff --git a/node-graph/nodes/vector/src/generator_nodes.rs b/node-graph/nodes/vector/src/generator_nodes.rs index 66c8009e02..73564c75e3 100644 --- a/node-graph/nodes/vector/src/generator_nodes.rs +++ b/node-graph/nodes/vector/src/generator_nodes.rs @@ -1,6 +1,6 @@ -use core_types::Ctx; use core_types::registry::types::{Angle, PixelLength, PixelSize}; use core_types::table::Table; +use core_types::{CacheHash, Ctx}; use dyn_any::DynAny; use glam::DVec2; use graphic_types::Vector; @@ -188,7 +188,7 @@ fn star( } #[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, serde::Serialize, serde::Deserialize, Hash, CacheHash, DynAny, node_macro::ChoiceType)] #[widget(Radio)] pub enum QRCodeErrorCorrectionLevel { /// Allows recovery from up to 7% data loss.